在多核并行编程的场景之中,人们一个不慎重就容易遇到死锁的问题,尤其是使用递归锁的情况,因为这会显著的放纵开发人员编写多线程下就绪代码的谨慎态度。
我不是很推荐在低U配置(就是单核)机子上面使用自旋锁这样的东西,这是因为在这些机子上面,会直接死锁并且系统几乎被完全挂起,但它并不违背锁的四个必要先决条件,仅仅只是因为两个自旋锁占用了大量的CPU,几乎没有空闲CPU时间用于等待后台事件完成。
所以表象上面就是死锁了,对于自旋锁存在一个必要的先决条件,同时等待旋转的线程数量不得等于当前CPU核心数,否则死锁,但它不违背死锁的四个先决条件。
关于死锁的四个先决条件,为:
1、循环等待
存在一个线程的资源需求循环链,每个线程都在等待下一个线程所持有的资源。
2、占有与等待(保持与等待)
一个线程可以持有一些资源并等待其它线程所持有的资源,即:线程在等待其他线程释放资源时仍然保持自己已经占有的资源不释放。
3、互斥
至少有一个资源必须处于非共享状态,即一次只能被一个线程使用,如果其它线程请求该资源,它必须等待资源被释放为止才可以。
4、不可剥夺(非抢占)
系统中的资源无法被强制性地从一个线程中剥夺,只能由持有资源的线程主动释放。
对于多核编程之中,只要注意并避免满足上述的几个必要条件,都不会导致线程被死锁,但大多数情况死锁的问题都是相互 “循环等待” 导致滴。
举个例子:
A/B两个线程都需要访问共享数据(假设该数据为链接池),由于是为了多核的效能,所以 connection 内部需要上锁(只要多个线程都需要访问都为共享数据),但共享数据也得上锁。
A线程做发送数据动作:
所以A线程的大致流就会这样:
1、获取连接池锁
2、获取链接引用
3、调用链接发送函数
4、获取链接的锁(X死锁)
B线程做关闭链接动作:
B线程已经提前拿到链接的引用,且释放了连接池的引用。
1、调用链接的 Dispose 释放函数
2、获取链接的锁
3、产生连接关闭事件,或调用连接池删除这个链接
4、获取连接池锁(X死锁)
这两个线程产生了交叉循环等待锁的问题,A持有了连接池锁等待链接锁,B持有了链接锁等待连接池的锁,两者谁一直占有资源锁而不释放它。
破坏这种问题的解决方案有很多,人们只需要打破死锁的任何一个必要条件就可以了,通常来说:对于共享数据锁的应用粒度越小越好。
例如:
如果人们只是希望从连接池中获取/删除/添加链接对象的情况下,只需要确保共享的连接池数据是在临界区。
而链接的锁是内部自治的,那么通常不会产生死锁的问题,当然,除此以外还有其它的办法。
例如:
把行为拆开投递到 strand 事件队列之中有序执行,这样也可以破坏掉产生死锁的必要条件,故而解决多线程死锁的风险问题。
当然:
出于个人原因,我更推荐大家根据具体业务来按照线程来划分每个线程应该干什么,这样可以减少因多线程之间同步带来的性能开销。
Actor 模式也是个可行的多核编程,可行的设计模式,还是要看开发人员对于多线程编程掌握程度,多线程这个东西,玩的好人会玩的非常好,玩的很差的人,其下限这块没法看。