一、多线程的开发
多线程的开发,在实际场景中几乎是无法避开的。即使是前端看似没有使用线程,其实在底层的框架中也使用了线程进行了支撑。至少到现在,不管是协程还是其它什么新的编程方式,仍然无法撼动线程的主流地位。
多线程的开发中,各种复杂的场景今天不分析,只分析一种场景,就是多线程的退出,或者说整体程序的安全退出。有过开发经验的可能都遇到过,程序运行的很好,但一退出就崩溃了。甚至还有的遇到过,有些程序无法退出,只能使用一些特殊的手段暴力杀死线程才能退出。这些现象的出现,其实基本上都是多线程的协调机制(同步、通信等)没有做好。
下面就对多线程的安全退出进行分析和说明。
二、线程的运行分析
既然要线程安全的退出,就必须明白线程在运行时的工作环境。知己知彼,百战不殆。下面就分析一下涉及到线程退出时的一些重点:
1、上下文
线程在执行时,会有一些上下文环境,在操作系统里称为context,其中的内容有很多,但其实重点就是堆栈和一些存储的值,这些值包括一些文件句柄和内存等的值。这为线程的恢复提供了依据,如果上下文被破坏,可想而知结果会是什么。
2、资源
线程运行的目的就是了处理各种任务,任务中包括本身分配的资源和系统分配的资源。而多线程之所以被使用,目的就是为了处理任务及任务相关的数据,这些数据大多数的情况下是依赖和被 依赖于其它线程的(一般只有在学习时才会写一些独立的线程逻辑,与其它线程无关)。而这些数据和资源可能要和其它线程共享,也有可能返回给其它线程,甚至有可能让其它线程来负责销毁和回收。
这就出现了一个很明显的问题,如何在退出时保证资源和数据已经完整正确的发出。如果强行退出,外送的数据不正确,(包括不一致,未发送,发送错误等很多情况)会不会产生问题。举一个例子,一个数据库存储的字段,需要A线程处理,然后送给B线程再加工,然后由C线程负责存储,如果其中一个线程意外退出,那么C线程要么无数据可存储,要么是数据有问题。
同样,如果某种情况,程序退出时出现了问题,产生了僵尸进程。那么它就会占用一系列的系统的资源。同样,非安全退出导致一些系统资源未释放,如硬件IO(端口、句柄等)中的相关资源。那么,此时二次启动时程序可能就无法再次安全启动。
3、协调
多线程的开发,一般来说必然会带来线程间的协调调度,这才是多线程复杂的原因。正如很早前就分析的,如果多个线程只是自己搞自己的事情,和其它线程都没关系,那么这不是真正的多线程开发,从某种意义上来讲,它就是单线程开发。
既然涉及到协调,就有一个问题,某个线程的退出以另外一个线程设置某项条件才能退出,如果设置条件的线程意外退出,整个程序可能就无法退出。通常这也是常见的一种多线程退出的问题。
另外,多线程间的同步如果处理不好,则有可能在退出时造成死锁。同样,也可能因为线程退出的无序,导致资源和内存的泄露等等。所以,多线程的退出就是打仗一样,“进攻是很复杂的,但是撤退更复杂,撤退搞不好就会演变成为溃败!”
明白了吧,万物都是相通,就是这个道理。
三、安全退出的机制和方法
明白了多线程在运行时的情况,那么就可以有针对性的处理这些情况,从而保证多线程退出时的安全。下面就上面分析的几个方面提出几个解决思路:
1、等待机制
通过让线程睡眠或者其它的Wait(一般都有超时机制),等待所有的线程完成后,再共同退出。这种方法简单、粗暴,但对一些新手或者说应用场景简单的情况下,不失为一种解决办法。缺点就是延迟太大,可能在某些情况下仍无法正常退出。
2、轮询机制
就是设立一个标志位或者标记状态,由各个线程去反复的查询,如果到达退出的状态,即可退出。这种方法相对简单,但浪费资源且有较大的退出延迟,一般也是在简单或者特定的场景下使用。
3、消息机制
消息机制是通过多线程间消息的有序传递,来保证线程间有序的安全退出。这种方法相对来说更安全也更优雅。但其实现要相对复杂一些,而且不同的OS中消息机制都有所不同,需要开发者要有针对性的去开发。
4、事件机制
事件机制类似于消息机制,同样也很安全。其缺点与消息机制大致相同。另外,如果开发者考虑不足,还有可能陷入死循环或死锁。
针对这些思路,就有了更具体的解决办法:
1、不同的系统与库中对线程的处理有所不同,以标准库和Linux为例,它的线程创建有两种处理方式,一种是deatch方式,一种是join方式。两种方式的处理机制不同,前者主线程退出子线程也就安全的退出而后者则需要等待子线程的退出后整体程序才会退出。不同的机制有着不同的处理方式,这就看实际场景了,如果实际应用子线程中的数据没有什么意义或者对其它线程没什么影响,就可以使用前者。否则就需要使用后者。
而如果使用后者,就需要参考上面的解决思路,使用类sleep,固定量的死循环等等;或者设置状态位(原子变量或普通变量)不断轮询;或者Linux下的信号如SIGINT、SIGTERM等,Windows可以使用WM开头的系列消息;更优雅的可以使用信号量、条件变量、Windows下的Event机制等等。
2、资源的处理
不管是句柄还是内存亦或什么其它资源,简单一些还好说,一般不会有什么问题。但是多了后如何处理?首推当然是RAII,就是为了解决这类问题的。如果不想使用RAII,也可以利用损害集中管理原则,将资源的处理交到指定的对象中,集中处理。
四、总结
其实多线程如此,多进程大抵亦也如此!只不过目前来看写多进程的已经非常少了。总结到最后,多线程的安全退出只有一条原则,就是让每个线程自己正常退出,而不是暴力打断它的执行。只要贯彻了这一原则,就不会出现多线程退出的各种异常问题。
可是,实际的场景复杂又多变,有的时候确实无法等待所有的线程正常的退出,这就需要开发者自己根据情况进行取舍。有舍才有得,亘古不变的道理!