并发思想提炼(1)(理解并发,避免死锁)

并发思想提炼(1)(理解并发,避免死锁)

一直做服务器后端和基础组件平台开发,常常用到并发,故简单放些干货,一来算是总结,二来希望后人少走弯路, 写到哪儿算哪儿,不定期更新。

1.    Introduction

先来明白一些概念。Concurrency并发和Multi-thread多线程不同

你在吃饭的时候,突然来了电话。

  1. 你吃完饭再打电话,这既不并行也不多线程
  2. 你吃一口饭,再打电话说一句话,然后再吃饭,再说一句话,这是并发,但不多线程。
  3. 你有2个嘴巴。一个嘴巴吃饭,一个嘴巴打电话。这就是多线程,也是并发。

并发:表示多个任务同时执行。但是有可能在内核是串行执行的。任务被分成了多个时间片,不断切换上下文执行。

多线程:表示确实有多个处理内核,可同时处理多个任务。

世界的复杂的,世界是并发的。于是模拟这世上的业务的程序也是并发的。随着系统功能的增加,复杂度不断提高,并发特性被引入编程。

2.    最简单的并行

两个任务互不干扰,它们不会影响到系统的同一实体。每个线程只需要自己做好自己的任务即可。

两个任务会影响到同一实体,但是不会在同时访问该同一实体。这样,在这个实体上,任务是串行执行的。这样也是安全的并发。

3.    危险的并行

两个任务同时访问同一实体,脏读写的问题,设有a值为1, 两个线程先后加1,按道理说最后a值结果为3.

  1. T1线程读取数据a;T2线程读取数据a;
  2. T1线程a++, 然后反存到a,a值为2;
  3. T2线程a++, 然后反存到a, a值还是为2;

两次操作后,a值不为3,而是2。这就是并发出现的错误。a若是一个可释放(disposable)的实体. T1线程释放a,T2线程操作a,会造成更大的错误。

4.    如何避免此危险

1. 干脆就串行执行T1,T2。不过没有利用到处理器的并发特性。虽然安全,但是效率不高。

2. T1,T2并发。但是不会同时操作同一个实体(Critical Entity)。即实体不是并发的。实体是串行的。

3. 读取关键实体使用Clone,拷贝出来的实体是临时的。本次操作在该临时实体上。下次操作继续Clone该实体再使用。

经常能用到的是方法2和方法3。接下来具体说说方法2和3。

5.    让对关键实体的访问串行

方法2的核心思想是串行。不过不是任务串行,而是访问共享实体时串行。串行是人类容易思维的方式,把并发问题转变为顺序执行问题,也助于后来维护人员理解。一个最简单的实现方式就是加Lock then access。

 

6.    Lock地狱

Lock是并发程序中常用的操作,每个人都会用。。。。嗯。。。。常常会滥用。。。然后,运行一段时间。嘣,程序自爆。说说常常遇到的问题:

  1. 死锁。T1 lock A request B,T2 lock B request A。T1和T2被互相Block住。程序进行不了
  2. 递归锁。T1 lock A and lock A again。同一线程T1被自己Block住。

递归锁好解决,C++ 11中有std::recursive_mutex。再高级一点的语言自带这样的特性。比如C#中的lock就自带递归锁。一般地,递归锁通过里面添加counter实现,每次锁就counter++,每次release就counter--。Counter 为0就表示解锁。其实这就是一种信号量(semaphore)的实现方法。引申开来去,C++中的share_ptr/auto_ptr就是此类思想,这种通过引用计数来判断该对象是否被使用,若检测到不被使用从而自动的release该段内存。C#和Java中内存管理就是这个思路。不多讲了,以后单开段子讲。

7.    死锁及其防止

确切的说死锁单纯在程序这个层面难以防止,最好在设计开始就注意这个问题。就是说在程序设计的时候就搞明白那些资源会互相调用。这样的情况就要多留心眼。我工作中一直写一些基础架构API。当其它工程师调用时。。。。。嗯。。。。。不仔细看文档,在一些不适用的地方使用造成了死锁,然后来找我。。。。。这种情况下要么就多培训,多强调使用手册。要么,这样说吧,把API写得健壮点。毕竟完全不会知道用户会怎么使用。这就是我在API开发中使用静态语言的原因(其实我更喜欢动态语言python,用python做leetcode真是心情舒畅),至少在编译阶段就能有一定程度的规则控制,而不是到了运行阶段报Error。这关系到如何编写健壮的API,以后开段子讲。

回到死锁防止这个问题,关于避免死锁的API可以使用这个方法---try lock机制。简单就是说给lock一个时间锁,如果在一段时间内还是没有取得该资源的访问权就跳过,放LOG,然后执行下面的步骤。可以通过前面讲的“等待信号量”来实现此机制,其实也不用自己特别实现,各主流语言应该都有。核心思想在软件层面上是放个自旋锁和Flag量,觉得搞不明白(C++,C#,Java等该功能实现方式)的话自己也可以实现一个。

 (to be continue....)

 

你可能感兴趣的:(并发思想提炼(1)(理解并发,避免死锁))