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

Every_dawn · · 486 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

一、两个问题

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":   "https://www.jianshu.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() {

}

有疑问加站长微信联系(非本文作者)

本文来自:简书

感谢作者:Every_dawn

查看原文:【go语言学习】标准库之sync

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

486 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传