Concurency in Go-并发概述

284 阅读4分钟

1. 并发概述

并发性(英语:Concurrency)是指在一个系统中,拥有多个计算,这些计算有同时执行的特性,而且他们之间有着潜在的交互。因此系统可进行的运行路径会有相当多个,而且结果可能具有不确定性。并发计算可能会在具备多核心的同一个芯片中复合运行,以优先分时线程在同一个处理器中运行,或在不同的处理器执行

2. 并发的难点

基于并发的概述,不难发现并发中可能存在的难点有竞争共享资源操作原子内存访问同步死锁等难点,具体如下:

2.1 竞争共享资源

当两个或者多个操作必须按照正确的顺序执行,而并发操作并没有保证这个顺序,就会发生竞争。具体来说,其中一个并发操作试图读取变量V, 同时,另外一个并发操作试图写入同一个变量V。
基本示例:

1. var data int
2. go func(){
3.   data++
4.}()
5. fmt.Printf("the value is %v.\n", data)

代码中,第3行和第5行都会试图去访问变量data, 但并不能保证以何种顺序进行访问,由此造成结果的不确定性。结果中可能出现 1)不打印任何结果, 2)打印结果为0,3)打印结果为1。因此在编写并发代码时必须仔细的遍历所有可能的方案,否则会出现最疯狂随机的bug,它们难以发现,并且可能会在投入生产多年之后才出现。

2.2 操作原子性

原子操作意味着在它运行的环境中,它是不可分割的或者不可中断的。如果某一个东西是原子的,即为它在并发环境中是安全的,因此保证函数,方法或者程序原子性是构成逻辑正确程序的关键。

2.3 内存访问同步

多个并发进程试图访问相同的内存区域,此块内存区域为临界区。访问此块内存时通过采取某些方式(例如加锁)来保证同一时间内只有一个程序独占内存,如此做到内存的访问同步。

2.4 死锁

上面三点是关于程序正确性的讨论,如果这些问题得到正确处理,程序将永远不会出现错误的答案。然而上保证上述正确性时可能会带来另外一个问题,即死锁。所有并发进程相互等待,在这种情况下如果没有外界的干预,程序将永远无法恢复。 死锁举例如下:

type value struct {
	mu    sync.Mutex
	value int
}
var wg sync.WaitGroup
func main() {
	printSum := func(v1, v2 *value) {
		defer wg.Done()
		v1.mu.Lock()
		defer v1.mu.Unlock()
		time.Sleep(2 * time.Second)
		v2.mu.Lock()
		defer v2.mu.Unlock()
		fmt.Printf("sum=%v\n", v1.value+v2.value)
	}
	var a, b value
	wg.Add(2)
	go printSum(&a, &b)
	go printSum(&b, &a)
	wg.Wait()
}

由于time.Sleep()的存在(此处模拟一个函数处理时间差)造成的死锁,结果产生fatal error: all goroutines are asleep - deadlock!.由此造成了死锁 引用coffman在一篇论文中提到的死锁发生的几个必要条件

相互排斥——并发程序同时拥有资源的独占权
等待条件——并发程序必须同时拥有一个资源,切等待额外的资源
没有抢占——并发进程拥有的资源只能被该进程释放
循环等待——一个并发进程(p1)必须等待另外一系列的并发进程(p2),这些并发进程同时也在等待p1,这样便满足了最终条件。 了解了这写规则,只要确保至少一个不成立,则死锁就会得到预防

3. 确保并发安全

通过在第二点的讨论,了解了并发中可能存在的问题,确保并发正确性的困难点,解决这些问题将是后续工作的重点。 最后几个问题,为下文开篇
谁负责并发?
如何利用并发原语解决这些问题?
谁来负责同步?

参考文档:
[1]hk.jwchfzu.top/baike-并发性
[2]concurrency in go
[3]代码来源均为concurrency in go