目录
关于竞态我们都知道,竞态的形成即是资源的争用。而资源存在非常多的形式,比如变量,对象, CPU, buffer, 网络,磁盘(文件), 外设的所有权,服务等。
而对资源的不恰当利用可能导致程序低效的运行,资源泄漏,死机,崩溃等。
本文为笔者的一些经验总结,当然也参考了不少文档。试图详尽的介绍与此相关的问题。
当然要做到全面是非常难的,毕竟现实生活中遇到的问题五花八门,完全不知道会在什么时候会出现一个吓你一跳的黑天鹅。
并发并发,指同一个系统拥有多个计算进程(或者进程),这些进程有同时执行与的潜在交互特性,因此系统会有相当多个执行路径且结果可能具有不确定性。并发计算可能会在具备多核心的同一个芯片中交错运行,以优先分时线程在同一个处理器中执行,或在不同的处理器执行。 -- wikipedia
解决并发过程出现的竞态我所知道的有两个方法:方法之一是给资源上锁;方法二是使用异步单线程方式实现( node.js )。
异步暂时不再此文讨论范畴,因此这里简要介绍一下异步单线程编程的一些需要注意的问题。
- 无法利用多核性能。
- 需要严格控制过程的是时间片,防止某些过程获取不到资源。
- 虽然不要原子性,但是还是需要妥善的对资源进行管理。最好的办法是进入过程获取资源,退出过程释放资源。其次你需要一些管理资源的手段,方便你随时查看当前资源的使用情况(比如说状态变量,计数器);
- 不能存在 while 轮询,你只能依靠下一次 CPU 资源什么时候轮到你,因此你也不能够相对精确的在某个时间点去做某件事。这个其实和第 2 点是一致的。
我理解的异步单线程编程即单线程事件编程。
下面是一个用伪代码实现的例子:
task1_private_data task1(){ ... PUSH_TASK( taskX, taskX_private_data ) ... } INTRO(){ // do what you do PUSH_TASK( task1, task1_private_data ) } THREAD{ INTRO() LOOP{ WAIT_WAKE_UP() LOOP{ GET TASK & TASK_PRIVARE_DATA from TASK_LIST CALL TASK( TASK_PRIATE_DATA ) } } } PUSH_TASK( TASH, TASK_PRIVATE_DATA ){ PUSH TASK & TASK_PRIATE_DATA PAIRE to TASK_LIST WAKE_UP THREAD }死锁的情况 单线程死锁
首先,我们先来讨论一下死锁这个问题。我自己把死锁分成了以下这几个类别:单线程死锁,多线程死锁,不完全死锁。
首先看单线程死锁,单线程死锁的模式如下所示:
THREAD{ LOCK A ... LOCK A ... }
我觉得有一定经验的人一般不会傻到写出这种代码,但是下面这种情况嘞:
CLASS A{ LOCK self LOCK_RESOURCE(){ self_lock.lock() return .... } UNLOCK_RESOURCE(){ self_lock.unlock() } } class B{ A a; DO_PROCESS_1(){ a.LOCK_RESOURCE(); } ... DO_PROCESS_n(){ b.UNLOCK_RESOURCE(); } } // 这种例子 THREAD_1{ B b; b.DO_PROCESS_1(); ... b.DO_PROCESS_1(); } // 还有这种例子 B b; THREAD_2 or PROCESS_2{ b.DO_PROCESS_1(); ... if CONDITION_1: return; ... b.DO_PROCESS_n(); }
还有一种情况,如下所示:
// in PROCESS_1 LOCK *A = new LOCK; // in PROCESS_2 LOCK *B = A; // in PROCESS_3 delete A; // in PROCESS_4 LOCK B; // 死锁 -- 因为 B 指向的地址已经被释放,因此 B 地址指向的数据可能是任意状态的。
当然如果有这种问题,在项目前期也许很容易暴漏,但如果 THREAD_2 or PROCESS_2 中的 CONDITION_1 很难满足,而你的测试样例又没有覆盖到的话,那么这可能就成为你应用中的一个炸弹了。
上面列举的例子还仅仅是锁,锁的话发现问题了还比较方便定位。如果上面被阻塞的不是锁,而是文件(O_EXCL 或者 "wx" "wbx" "w+x" "wb+x" "w+bx" 模式打开的文件)?一个网络阻塞性的 read 函数?一个可被设置为独占的驱动,外设?那有怎么办嘞?
因此,你不仅仅是需要对你所使用的语言烂熟于心,还必须对你模块中涉及到的所有可能会阻塞或者因条件而阻塞的地方,以及模块于模块之间的业务逻辑(交互逻辑)心知肚明。
多线程死锁下面我们再来看一看多线程的例子,死锁的通用形式如下所示:
LOCK a; LOCK b; THREAD_1{ ... a.lock() ... b.lock() ... b.unlock() ... a.unlock() ... } THREAD_2{ ... b.lock(); ... a.lock(); ... a.unlock(); ... b.unlock(); }
当然,这些 lock 可能会隐藏再各种判断条件下,或者藏在各种调用的方法过程中,这些设计,可能会将这个模式隐藏德很深。甚至躲过你自信满满的,不完全的测试样例。
再看这个例子,这个例子我们看不到以一个锁,但是 THREAD_3 可能就一直停在那里,(也可能偶尔能运行一下,让你甚是糊涂):
THREAD_1:{ LOOP(){ WAIT LIST_A MESSAGE; GET msg FROM LIST_A DO SOMETHING PUSH to LIST_B } } THREAD_2:{ LOOP(){ WAIT LIST_B MESSAGE; GET msg FROM LIST_B DO SOMETHING } } THREAD_3{ PUSH msg to LIST_A WAIT msg result from B // or PUSH msg to LIST_B WAIT msg result from A }
我从这些例子里面得到的感悟是基础是
1. 千里之行,始于足下。千里之堤,溃于蚁穴。基础是很重要的,细节也很重要。我们需要不断的熟练自己的技能。才能如庖丁解牛,游刃有余。
2. 设计模式不仅仅是书上明确的那些既定的东西,它是一种思维工具, 是刀,是锯,是改锥,也是你自己总结提炼的最佳实践。良好的设计风格是非常重要,多学多想多思考,沉淀出属于自己的一套设计模式是很重要的。
3. 良好设计的关键在于对问题的深入认识,而不是提供了多少高级的特征。 -- 当然更不是不假思索的找了一个解决当前问题的方案即可。因此深入理解业务逻辑是非常重要的。
参考资料-
《深入理解并行编程》中文版 -
《LINUX设备驱动程序》 第四章