【go语言学习】标准库之sync

一、两个问题

1、同步执行问题
package main

import (
    "fmt"
    "time"
)

func main() {
    go fun1()
    go fun2()
    fmt.Println("main函数等待")
    time.Sleep(time.Second * 1)
    fmt.Println("main函数结束")
}

func fun1() {
    fmt.Println("fun1函数执行")
}

func fun2() {
    fmt.Println("fun2函数执行")
}

主线程为了等待所有的子goroutine都运行完毕,不得不在程序中使用time.Sleep() 来睡眠一段时间,等待其他线程充分运行。这种方式耗费时间,显然是不够优雅的。

2、临界资源问题

临界资源: 指并发环境中多个进程/线程/协程共享的资源。并发编程中对临界资源的处理不当, 往往会导致数据不一致的问题。

如果多个goroutine在访问同一个数据资源(临界资源)的时候,其中一个线程修改了数据,那么这个数值就被修改了,对于其他的goroutine来讲,这个数值可能是不对的。

举个例子,我们通过并发来实现火车站售票这个程序。一共有10张票,3个售票口同时出售。

package main

import (
    "fmt"
    "math/rand"
    "time"
)

//全局变量票数
var tickets = 10

func main() {

    //三个goroutine  模拟售票窗口
    go saleTickets("售票口1")
    go saleTickets("售票口2")
    go saleTickets("售票口3")

    //为了保证3个goroutine协程正常工作,先将主线程睡眠5秒
    time.Sleep(5 * time.Second)
}

func saleTickets(name string) {
    //随机数种子
    rand.Seed(time.Now().UnixNano())
    for {
        if tickets > 0 {
            //随机睡眠1~1000ms
            time.Sleep(time.Duration(rand.Intn(1000)) * time.Millisecond)
            fmt.Println(name, "余票:", tickets)
            tickets--
        } else {
            fmt.Println(name, "售罄,已无票。。")
            break
        }
    }
}

运行结果

售票口3 余票: 10
售票口2 余票: 10
售票口1 余票: 10
售票口3 余票: 7
售票口1 余票: 7
售票口3 余票: 5
售票口2 余票: 4
售票口3 余票: 3
售票口2 余票: 3
售票口1 余票: 3
售票口1 售罄,已无票。。
售票口2 余票: 0
售票口2 售罄,已无票。。
售票口3 余票: -1
售票口3 售罄,已无票。。

在以上的代码中,使用三个并发运行的go协程模拟了三个售票窗口同时售票,而由于全局变量tickets会被三个协程在一段时间内同时访问,因此tickets就是我们所说的“临界资源”。
我们可以发现:

在开始时,三个窗口同时读到信息:tickets=10,从而随机都输出了余票=10
而在结尾时,竟然出现了余票为负数的情况,其产生的原因在于,票数快要卖完时,当售票口1余票1,并且售完这一张票后,在这个时间段内,售票口2已经进入了if tickets > 0满足条件的代码块内,然而售票口1此时将最后一张票售出,tickets 由1变为0售票口2打印出来了不应该出现的结果:余票0,同理售票口3打印了不该出现的结果:余票-1。

多goroutine【多任务】,有共享资源,且多goroutine修改共享资源,出现数据不安全问题【数据错误】,保证数据安全一致,需要goroutine同步

goroutine同步方式

  • channel 【csp模型】
  • sync包提供的方法

二、sync同步等待组WaitGroup

使用等待组进行多个任务的同步,等待组可以保证在并发环境中完成指定数量的任务。
等待组的方法:

方法名 功能
(wg *WaitGroup)Add(delta int) 等待组的计数器+1
(wg *WaitGroup)Done() 等待组的计数器-1
(wg *WaitGroup)Wait() 当等待组计数器不等于0时阻塞,直到为0

代码示例:

package main

import (
    "fmt"
    "sync"
)

var wg sync.WaitGroup

func main() {
    wg.Add(1)
    go fun1()
    wg.Add(1)
    go fun2()
    fmt.Println("main函数等待")
    wg.Wait()
    fmt.Println("main函数结束")
}

func fun1() {
    fmt.Println("fun1函数执行")
    wg.Done()
}

func fun2() {
    fmt.Println("fun2函数执行")
    wg.Done()
}

运行结果

main函数等待
fun1函数执行
fun2函数执行
main函数结束

三、sync互斥锁Mutex

加锁成功则操作资源,加锁失败则等待直至锁加锁成功——所有的goroutine互斥,一个得到锁其他全部等待。

互斥锁被称为Mutex,它有2个函数,Lock()和Unlock()分别是获取锁和释放锁,如下:

type Mutex
func (m *Mutex) Lock(){}
func (m *Mutex) Unlock(){}

修改上面售票代码,解决临界资源安全问题
示例代码:

package main

import (
    "fmt"
    "math/rand"
    "sync"
    "time"
)

//全局变量票数
var tickets = 10
var mutex sync.Mutex
var wg sync.WaitGroup

func main() {

    //三个goroutine  模拟售票窗口
    wg.Add(1)
    go saleTickets("售票口1")
    wg.Add(1)
    go saleTickets("售票口2")
    wg.Add(1)
    go saleTickets("售票口3")
    wg.Wait()
}

func saleTickets(name string) {
    //随机数种子
    rand.Seed(time.Now().UnixNano())
    for {
        //上锁
        mutex.Lock()
        if tickets > 0 {
            //随机睡眠1~1000ms
            time.Sleep(time.Duration(rand.Intn(1000)) * time.Millisecond)
            fmt.Println(name, "余票:", tickets)
            tickets--
        } else {
            mutex.Unlock()
            fmt.Println(name, "售罄,已无票。。")
            break

        }
        //解锁
        mutex.Unlock()
    }
    wg.Done()
}

运行结果

售票口3 余票: 10
售票口3 余票: 9
售票口1 余票: 8
售票口2 余票: 7
售票口3 余票: 6
售票口1 余票: 5
售票口2 余票: 4
售票口3 余票: 3
售票口1 余票: 2
售票口2 余票: 1
售票口1 售罄,已无票。。
售票口2 售罄,已无票。。
售票口3 售罄,已无票。。

四、sync读写锁RWMutex

读写锁要达到的效果是同一时间可以允许多个协程读数据,但只能有且只有1个协程写数据。也就是说,读和写是互斥的,写和写也是互斥的,但读和读并不互斥。
简单来说:

  • (1)可以随便读,多个goroutine同时读。读的时候不能写。
  • (2)写的时候,啥也不能干。不能读也不能写。

读写锁是RWMutex,它有5个函数:

  • Lock()和Unlock()是给写操作用的。
  • RLock()和RUnlock()是给读操作用的。
  • RLocker()能获取读锁,然后传递给其他协程使用。使用较少。
type RWMutex
func (rw *RWMutex) Lock(){}
func (rw *RWMutex) RLock(){}
func (rw *RWMutex) RLocker() Locker{}
func (rw *RWMutex) RUnlock(){}
func (rw *RWMutex) Unlock(){}

举个例子,学生信息录入系统,录入学生信息是写操作,读取学生信息是读操作。可以使用读写锁:

package main

import (
    "fmt"
    "sync"
)

var wg sync.WaitGroup

// Student 学生信息系统
type Student struct {
    // 读写锁
    sync.RWMutex
    // 存储信息 姓名-年龄
    data map[string]int
}

// Add 增加学生信息
func (s *Student) Add(name string, age int) {
    defer wg.Done()
    s.Lock()
    defer s.Unlock()
    if _, ok := s.data[name]; !ok {
        s.data[name] = age
    }
}

// Query 读取学生信息
func (s *Student) Query(name string) {
    defer wg.Done()
    s.RLock()
    defer s.RUnlock()
    if v, ok := s.data[name]; ok {
        fmt.Printf("姓名:%s\t年龄:%d\n", name, v)
    } else {
        fmt.Println("学生信息不存在!")
    }
}

func main() {
    s := &Student{
        data: make(map[string]int),
    }
    wg.Add(4)
    s.Add("jack", 20)
    s.Add("tom", 23)
    s.Add("lili", 18)
    s.Add("lili", 20)

    nameList := []string{"jack", "tom", "lili", "xiaohua"}
    for _, v := range nameList {
        wg.Add(1)
        go s.Query(v)
    }
    wg.Wait()
}

运行结果

学生信息不存在!
姓名:jack      年龄:20
姓名:lili      年龄:18
姓名:tom       年龄:23

五、sync单次执行Once

sync.Once 是 Golang package 中使方法只执行一次的对象实现,作用与 init 函数类似。但也有所不同:

  • init 函数是在文件包首次被加载的时候执行,且只执行一次
  • sync.Once 是在代码运行中需要的时候执行,且只执行一次

当一个函数不希望程序在一开始的时候就被执行的时候,我们可以使用 sync.Once 。
sync.Once是让函数方法只被调用执行一次的实现,其最常应用于单例模式之下,例如初始化系统配置、保持数据库唯一连接等。

代码示例

package main

import (
    "sync"
)

var configs map[string]string

func loadConfig() {
    configs = map[string]string{
        "url":   "//www.greatytc.com",
        "id":    "cd41c8c3645c",
        "email": "everydawn@jianshu.com",
    }
}

// Config1 被多个goroutine调用时不是并发安全的
// 比如有两个线程都在调用Config1函数,线程A在执行到if configs==nil后
// cpu切换到线程B执行,直到线程B运行完,这时configs已经被实例化,
// 当cpu在切回到线程A继续执行的时候,对configs又执行实例化操作,
// 这时内存中已有configs的两个实例,违背了单例定义。
func Config1(name string) string {
    if configs == nil {
        loadConfig()
    }
    return configs[name]
}

var loadConfigOnce sync.Once

// Config2 是并发安全的
func Config2(name string) string {
    loadConfigOnce.Do(loadConfig)
    return configs[name]
}

func main() {

}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,012评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,628评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,653评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,485评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,574评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,590评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,596评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,340评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,794评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,102评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,276评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,940评论 5 339
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,583评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,201评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,441评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,173评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,136评论 2 352