go基础9-并发

并发

并发机制,多goroutine间的共享变量,并发问题的分析手段,解决模式,goroutine和线程区别

竞争条件

并发:无法判断多个事件执行的顺序的情形;
并发安全:函数,方法,类型在线性执行或并发执行时,都能正确的返回结果;
包级别的导出函数一般都是并发安全的.因为只要让变量不在多个goroutine内共享,它就一定是并发安全的;但是如果存在共享变量,就必须维护一个更高层的互斥不变量来保证其访问的安全性;
并发的影响有很多:如死锁(deadlock),活锁(livelock),饿死(resource starvation),竞争条件等

竞争条件:程序在多个goroutine交叉执行操作时,没有给出正确的结果;因为竞争条件的发生存在偶发性,所以发现和排查都比较困难,它会真实的对业务造成影响;
实际竞争条件就是我们常说的脏数据,并发执行业务逻辑存在数据过程中显示不准确甚至由于结果覆盖造成总体数据缺失的问题;
比较典型的就是汇款问题:

a,b同时向账户汇款,入账语句:balance=balance+amount.并发下的过程和结果可能有四种:
1. a先入账100,b再入账200,结果300
2. b先入账200,a再入账200,结果300
3. a或b同时入账,最终a覆盖了b的结果,结果为100
3. a或b同时入账,最终b覆盖了a的结果,结果为200

因为实际的入账语句非原子性操作,执行时就存在被拆分执行的可能,造成竞争条件;
数据竞争:两个以上的goroutine并发访问相同的变量且至少其中一个为写操作时发生的竞争条件;数据竞争是一种特定的竞争条件,是由于写入数据造成的数据异常;
三种避免数据竞争的方式:

1. 不写.只读是并发安全的,因为不会涉及到数据不一致;共享变量可以通过全局变量的方式来初始化,避免写操作;
2. 只用一个goroutine访问变量.避免并发的发生,顺序执行不涉及同时修改数据,可以避免写竞争;单一协程写操作,通过channel来传值
3. 互斥条件.多goroutine访问时,一次只能一个goroutine操作.这是常规的加锁方式;

sync.Mutex互斥锁

保证最多只有一个goroutine在同一时刻访问共享变量.
重入锁:同一线程如果持有锁后可以再次获取其他锁,因为同一线程不涉及变量共享问题,通过锁计数来避免持有多锁时的锁释放问题.它主要用于解决持有锁后再次获取锁时发生死锁问题.
go中没有重入锁.因为mutex的目的是确保共享变量在程序执行时的关键点上保证不变性(持有锁操作共享变量会打破不变性,引入重入代表了可以在另一个锁范围内操作共享变量,这也意味着该变量可以是另一个锁范围的变量,这样就打破了锁范围内变量被唯一修改的限制),所以应该减少锁定时间及锁数量.多个锁也容易造成bug.
重入锁的实现跟底层结构有直接关系:
其实java采用重入锁的原因是线程和主机的操作系统线程是一对一,锁和变量共享直接便捷.
但是go是多个协程对应操作系统一个线程,通过runtime来进行通信,所以线程同步会比较复杂.
它本身设计推荐通道,消息来进行通信,不想通过复杂的实现来保证重入锁.
所以正确的做法应该时拆分函数,分解不同锁.

// 重入锁
 func F() {
     mu.Lock()
     ... do some stuff ...
     G()
     ... do some more stuff ...
     mu.Unlock()
 }

 func G() {
     mu.Lock()
     ... do some stuff ...
     mu.Unlock()
 }

F()中已经有锁,但是又引用了G(),G中也有锁,就形成了单一线程同时拥有多把锁的情况.G中可能会修改F()中的变量,打破共享变量的不变性,可能引起bug;

//修正
 func F() {
     mu.Lock()
     ... do some stuff ...
     g()
     ... do some more stuff ...
     mu.Unlock()
 }
// 内部私有
 func g() {
     ... do some stuff ...
 }
// 对外使用
 func G() {
     mu.Lock()
     g()
     mu.Unlock()
 }

同时提供内外两个函数,g()用于内部使用,不进行锁控制,G()用于外部调用,通过锁保证原子性操作,通过这种方式避免重入锁问题

sync.RWMutex读写锁

多读单写锁:允许多个只读操作并行执行,写操作互斥.适用于读多写少的情形.go中通过sync.RWMutex来实现.

var mu sync.RWMutex
var balance int
func Balance() int {
    mu.RLock() // readers lock
    defer mu.RUnlock()
    return balance
}

RLock和RUlock用于获取和释放读取或共享锁;上面mu.Lock和mu.Unlock来获取和释放一个写或互斥锁;RWMutex内部记录更复杂,它比mutex慢.

内存同步

计算机中堆处理器都有对应的一个本地缓存,为了效率,该缓存中数据会在处理器中缓冲,必要时才flush到主存中.这时就会存在提交到主存的写入顺序发生变化,造成数据混乱.
在一个独立的goroutine中,可以保证每个语句的执行顺序.但是如果不适用channel和mutex显示同步操作时,多个goroutine中的执行顺序就会不同.如果赋值和打印指向不同的变量,编译器可能会断定两条语句的顺序不影响结果,并交换语句的执行顺序.
所以并发的问题都可以用一致的,简单的既定的模式来规避.应尽可能将变量限定在goroutine内部;多个goroutine共享变量应使用互斥条件来访问;

// 存在共享变量的情形
var x, y int
go func() {
x = 1 // A1
fmt.Print("y:", y, " ") // A2
}()
go func() {
y = 1 // B1
fmt.Print("x:", x, " ") // B2
}()

sync.Once懒加载

有时候初始化成本比较高,这时有两种方式:第一是提前加载,系统启动时就加载到内存,使用时直接取值就行;第二是按需加载.使用时再加载,这样资源利用效率比较高;第一种加载方式会造成启动是系统负载较高,同时这些加载资源并不一定会用到,有一些浪费.所以实际中使用第二种更有效;
由于变量在并发中初始化执行顺序无法保证,造成懒加载会存在初始化数据为空的问题.

// 1 书写代码
func loadIcons() {
    icons = map[string]image.Image{
        "spades.png":   loadIcon("spades.png"),
        "hearts.png":   loadIcon("hearts.png"),
        "diamonds.png": loadIcon("diamonds.png"),
        "clubs.png":    loadIcon("clubs.png"),
    }
}
// 2 实际的执行代码
func loadIcons() {
    icons = make(map[string]image.Image)
    icons["spades.png"] = loadIcon("spades.png")
    icons["hearts.png"] = loadIcon("hearts.png")
    icons["diamonds.png"] = loadIcon("diamonds.png")
    icons["clubs.png"] = loadIcon("clubs.png")
}
// 3 取值代码
func Icon(name string) image.Image {
    if icons == nil {
        loadIcons() // one-time initialization
    }
    return icons[name]
}

1的代码在执行时会被解析为2的样子,这部分代码在单个goroutine中可以正常执行,因为单一协程可以保证初始化操作有序.但是如果并发下,可能loadIcons()执行到make方法后就被切换到其他协程执行,并未完成完整的初始化操作,造成Icon(name string)获取数据为nil的问题.

var mu sync.RWMutex // guards icons
var icons map[string]image.Image
// Concurrency-safe.
func Icon(name string) image.Image {
    mu.RLock()
    if icons != nil {
        icon := icons[name]
        mu.RUnlock()
        return icon
    }
    mu.RUnlock()

    // acquire an exclusive lock
    mu.Lock()
    if icons == nil { // NOTE: must recheck for nil
        loadIcons()
    }
    icon := icons[name]
    mu.Unlock()
    return icon
}

上面是实际优化后的支持并发的代码.它通过mu.Lock()来保障写时互斥性;通过mu.RLock()来支持读并发,提高运行效率;但是缺点是代码比较臃肿;
而sync.Once就是解决懒加载时并发安全代码简洁的对象.

var loadIconsOnce sync.Once
var icons map[string]image.Image
// Concurrency-safe.
func Icon(name string) image.Image {
    loadIconsOnce.Do(loadIcons)
    return icons[name]
}

sync.Once通过互斥锁mutex和初始化标识boolean来实现.当调用Do方法时,boolean==false(实际是0)来进行首次初始化操作,调用mutex.Lock限制协程进入,调用loadIcons进行初始化并修改初始化标识,后续不会再次触发该初始化操作.

竞争条件检测

竞争检测器(the race detector):go的runtime和工具链提供的动态分析工具.

用法:在go build,go run,go test后加上-race标识即可.
如果存在共享变量竞争条件则会显示类似下面的警告报告:

x:0 ==================
WARNING: DATA RACE
Write at 0x00c000074090 by goroutine 7:
  main.main.func1()
      E:/go_path/src/github.com/go-action/order.go:15 +0x6d

Previous read at 0x00c000074090 by goroutine 8:
  main.main.func2()
      E:/go_path/src/github.com/go-action/order.go:21 +0x8d

Goroutine 7 (running) created at:
  main.main()
      E:/go_path/src/github.com/go-action/order.go:13 +0xe2

Goroutine 8 (running) created at:
  main.main()
      E:/go_path/src/github.com/go-action/order.go:18 +0x118

警告报告显示协程信息和变量共享变量访问记录.包括变量身份,读取和写入的goroutine中活跃的函数的调用栈.但是竞争检测器只报告目前已发生的数据竞争.只能检测到运行时的竞争条件,不能保证所有竞争条件都会被发现.???编译时也会存在竞争条件???

通道是否能解决所有并发问题?
需要维护共享变量,普通类型变量,通道会复杂化变量共享;
异常处理,超时机制,通道解决更复杂,而框架集成锁来解决就更简洁直观;

原文地址:https://www.cnblogs.com/chengmuyu/p/13715332.html