原以为学了很多东西,停止一个多线程程序很容易,信号什么的一顿上,仔细思考发现并不简单。
背景
- 现在多线程应用非常常见,功能强大的同时也有很多新问题,为了便于描述,虚构一个具体场景。
- 假设有这样一个程序:
- 一个main thread,三个worker thread。
- main thread负责初始化配置,创建其他线程,并join等待。
- worker thread中各自有自己的运行逻辑,以轮询的方式长期运行。
终止信号
- 在main thread中,程序初始化时,注册signal handler,即可捕捉到SIGTERM。
- 但需要明确一点,信号处理函数中,只能调用可重入函数,自身修改其他状态时也要格外小心。
方案一
- 在signal handler中,flush进程所有状态,最后调用exit退出。
- 优点
- 基本没什么优点,除了想法简单。
- 缺点
- 需要记住所有线程的工作逻辑,知道如何flush。
- flush是否可重入?是否有race condition。这一点上无法预测,很多逻辑是调用的第三方库。
方案二
- signal handler调用pthread_cancel。
- 优点
- 将exit过程交给目标线程处理。
- 缺点
- 引入了pthread库(这可能也不算缺点,但总的来说依赖越单一、越少,也意味着越简洁安全)
- 目标线程仍然只能使用pthread_cleanup_push来预置handler。
- cancelpoint的设定较为复杂,它大量考虑了程序状态,但真正重要的是业务逻辑状态是否处于可以cancel的位置。
方案三
- signal handler修改各个线程可见的状态量,比如一个全局数组。
- 优点
- exit过程由目标线程处理。
- 语义更强大,数组中的变量可以表示更多状态,不仅仅是cancel。
- cancelpoint交给了线程自己处理,在可以退出的位置,判断状态量。
- 缺点
- 全局变量就是有害的(迫真。
- 可扩展性虽然有,但编码难度很高。如果要增加一种状态,比如暂停线程,那么所有工作线程的代码都需要改动,或者就得容忍,不是所有线程都支持所有状态。不支持所有状态是正常的,但全局数组的设计,让程序员难以控制这个缺陷,留下了容易犯错的可能性。
方案四
- std::thread以某个对象的方法为主体,那么,让该对象提供run方法的同时,也提供一个cancel方法。内部两个函数的交互则任由线程自己实现,可以是对象私有变量,也可以是thread_local。run方法在固定的点去检查状态。
- 优点
- stop由目标线程处理。
- 支持多种状态,比如stop,restart等。
- 支持不同线程不同实现,某些线程可能不支持stop,这无所谓,直接从类的接口上就能看出。也可以用一个父类,强制子类都实现这些方法。这取决于业务逻辑和代码组织的需求。
- 缺点
- main thread需要保留其他线程的对象。
- 状态变更函数都得是可重入函数(只写不读,或者直接取消多次进入即可,感觉问题也不大)。
后记
- 起初想了很多关于Exception的问题,在信号处理函数中写代码需要非常谨慎,怕引入并发错误。
- 最后的几个实现写起来,发现要么是我处理不掉的问题,要么就简单到不会出exception。
- RAII可能还是更多用在资源安全上,线程状态维护是另一个问题了。
附录
Pthreads Cancel Coint
pthreads标准指定了几个取消点,其中包括:
- 通过pthread_testcancel调用以编程方式建立线程取消点。
- 线程等待pthread_cond_wait或pthread_cond_timewait()中的特定条件。
- 被sigwait(2)阻塞的函数。
- 一些标准的库调用。通常,这些调用包括线程可基于阻塞的函数。