为什么直接杀死线程是不好的

/ 6评 / 0

Published by orzz.org(). (https://orzz.org/why-shouldnt-i-kill-a-thread/)

我们知道,windows里有个API叫TerminateThread,它可以干掉任何正在欢快小跑的线程。对应的,liunx里则是pthread_cancel(不是pthread_kill,这玩意本质是向线程发信号,而不是杀死线程)加上PTHREAD_CANCEL_ASYNCHRONOUS。

但是我们同时也看到,不论是哪种方法,在它们的手册里都不推荐我们使用它们。

比如微软的msdn中对TerminateThread的描述:

TerminateThread is a dangerous function that should only be used in the most extreme cases. You should call TerminateThread only if you know exactly what the target thread is doing, and you control all of the code that the target thread could possibly be running at the time of the termination.

再比如Pthread API Reference中的一段话:

It is recommended that your application not use asynchronous thread cancelation via the PTHREAD_CANCEL_ASYNCHRONOUS option of pthread_setcanceltype().

特别的,在C++11的标准库中干脆去掉了Thread的Cancellation;在某些语言中(比如Python),我们甚至无法由外部强制终止某个线程。

那么为什么直接杀死线程是不好的呢?

我们来打个比方吧。比如你是一个幼儿园老师,让班里的10个小朋友一起对着一个鸡蛋画画。你给每个小朋友分了桌子、椅子、画笔和纸,限定了画画时间是10分钟。小朋友们画啊画啊,10个小朋友有9个都画完了,可是最后还剩一个小朋友“天赋异禀”,面前的稿纸堆成了山,坚持要像达芬奇那样一丝不苟的画出一只完美的蛋。于是你快步走到他的面前,一巴掌把他拍出了教室,然后一把火把他坐过的桌子椅子,用过的画笔稿纸也都烧成了灰……
等等,这什么情况……桌子椅子画笔稿纸干嘛要烧掉嘛,明明还可以用啊……是的,没错。可这就是强杀一个线程带来的后果,所有这个线程正在使用的资源我们也别想回收了。

当然,凡事都有例外。在上面这个开玩笑试的比喻里,如果我们原本的计划是所有小朋友画完以后就炸毁幼儿园(进程退出)的话,这么玩这些小朋友似乎也不错【喂!
可是对于很多情况(应该说,大部分情况),我们是需要得到这些小朋友的绘画结果,然后请出这批小朋友,换入下一批小朋友。学校啊桌子啊椅子啊什么的,能复用是尽量要复用的。要不然少一个小朋友座位也少一个,这学校也就开不下去了。

所以一般来说,我们退出线程的手段是通知它们,“时间到啦,是时候收工啦”,然后等着它们一个个干完手头的工作,还原使用的资源。

除了资源回收的问题之外,这里还要再说一个杀线程的弊端,那就是锁。
其实严格来说,锁也算一种资源。当我们使用多个线程,去访问一个共享对象时,不可避免的要使用锁来做线程同步(当然了,你可以说用lock-free,但lock-free并不是万金油,在逻辑上必须进行条件等待的时候你还是得乖乖等待)。
当我们的一个线程获取了一个锁,正在访问某个共享方法的时候(比如调一个API啊,打印一个日志啊,balabala),还没来得及解锁就被咔嚓了,那这个锁就永远不会被解掉了,于是所有依赖这个锁的其它线程都华丽丽的死锁掉了。

我们来看看下面这个小例子:

通过在线程中不断执行一个for循环,让程序显现出线程正在printf中,突然被TerminateThread杀掉的情况。
在上面的例子中,我们可以发现TerminateThread之后,"Hello World!n"是无法被打印出来的,主线程在执行最后一句printf的时候死锁了。
这是因为printf语句内部会在输出的时候加锁,而TerminateThread却没有给printf解锁的机会。

其实在一些库的函数调用里,甚至很多我们认为绝对不会出问题的操作上,如果突然被中断都会导致后续代码的严重问题。比如把上面的DoSomething改成这样:

在TerminateThread之后执行printf时一样死锁了。应该说,不仅printf,此时所有new/delete的操作都会让整个进程死锁。

在微软TerminateThread的Remarks中,描述了强杀一个线程可能会带来的不良后果:

•If the target thread owns a critical section, the critical section will not be released.
【临界区(critical section)不会被释放】
•If the target thread is allocating memory from the heap, the heap lock will not be released.
【操作堆内存的时候被杀,会导致堆锁(heap lock)不会被释放】
•If the target thread is executing certain kernel32 calls when it is terminated, the kernel32 state for the thread's process could be inconsistent.
【调用kernel32 API的时候被杀,会导致kernel32的状态不一致】
•If the target thread is manipulating the global state of a shared DLL, the state of the DLL could be destroyed, affecting other users of the DLL.
【操作某个共享dll的全局状态时被杀,会导致DLL的状态被破坏,影响所有正在使用这个DLL的线程】

stackoverflow上也可以找到N条警告我们“Do not kill threads”的Answers(看这里这里,还有这里)。

既然上面说了这么多直接杀死线程的缺点,那么我们为什么还会有直接杀死线程的需求呢?
大致的总结一下,我们往往会在出现以下几种情况时杀死线程:

A. 线程是一个无限循环

有N种方法可以解决无限循环导致线程不会退出的情形。
最常规的方法是用一个全局标记做退出通知:

在pthread里我们有更潇洒的方法,叫“cancellation points”和pthread_testcancel(在Cygwin里可能不会调用局部对象的析构),这样可以让线程在执行到某一个取消点的时候自动退出。

如果是Windows,永远循环的是GUI线程的话,发送WM_QUIT即可(注意此时GetMessage返回0,直接用if判断的童鞋要小心了);非GUI线程的话,可以通过Event等事件通知手段模拟取消点的效果。

总之,对于无限循环的情况,我们要做的就是避免耗时的blocking操作,并能够有一个机制让我们在接到退出消息时可以及时响应。

B. 线程在执行一个耗时操作

我们处理耗时操作的时候,往往不会在主线程中进行,因为那会导致主线程的假死。如果主线程负责了GUI,那么GUI也就死掉了。因此常规的处理方法是开启一个线程专门执行耗时操作。
在这个时候,如果用户希望Cancel掉这个耗时操作,一般的做法是停止操作并退出工作线程。这就要求我们能够把一个完整的耗时操作分解成若干小粒度的,不怎么耗时的操作。
其实如果我们需要显示耗时操作的工作进度的话,很自然的就会去做这种分解。

比如说,写一个最笨的fibonacci计算程序:

代码使用pthread系列API完成多线程任务,以及全局的cancel_flag作为超时后的退出标记。当我们给个大点的数字时,输出就像这样:

在这里,calculate_fibonacci在计算的同时也将一个大任务分解成了很多个小任务。每个小任务的执行速度是非常快的,因此我们可以随时打印出任务进度,或者终止任务执行。

C. 线程死锁了

首先,产生死锁的四个必要条件如下:

  • (1) 互斥条件: 一个资源每次只能被一个进程(线程)使用。
  • (2) 请求与保持条件: 一个进程(线程)因请求资源而阻塞时,对已获得的资源保持不放。
  • (3) 不剥夺条件: 此进程(线程)已获得的资源,在末使用完之前,不能强行剥夺。
  • (4) 循环等待条件: 多个进程(线程)之间形成一种头尾相接的循环等待资源关系。

  • 假如我们的程序满足了这些条件,就会导致死锁的发生。
    我们可以看到,能够满足这些条件,十有八九是因为编写时的逻辑本身存在漏洞导致的。

    关于各种死锁的实际问题分析以及解决方案,这里就不展开了,网络上相关的文章非常多
    如果我们因为线程死锁,就简单粗暴的干掉线程而不去切实的解决它,这无疑只是在试图逃避程序本身的逻辑缺陷。

    D. 进程要退出了

    有一种说法是,我的进程要退出了,这时候我不想等,也不需要等待线程的返回,干脆利落的杀掉所有线程直接退出程序就好了。
    是的,如果我们的程序只是一个简单的小程序(比如上文的fibonacci计算),这种方式不会有什么问题。
    但是需要注意的是,在强行杀掉线程之后可能有些全局状态已经损坏了(不仅仅指自己定义的状态,见上文对TerminateThread的论述),这个时候我们唯一能做的事情就是马上退出程序。如果主线程还想在杀掉其它线程之后干些收尾工作,很可能会导致主线程死锁或程序崩溃。

    其实,我们何苦用这么脆弱的方式退出进程呢?杀死线程的目的是什么?无非就是快速退出进程罢了。那为啥不简单的TerminateProcess呢?
    这也解释了为什么std里只提供了std::terminate,却不肯给一个request_cancellation


    上面列举了4种我们需要杀死线程的情况。现实开发中,我们往往会遇到更多让我们忍不住拿起“武器”把线程杀一杀的时候。遇到这种情形,首先需要告诉自己一定要冷静,因为杀死一个线程,是我们迫不得已的最后手段。它往往只能掩盖住表面的问题,而让真正的漏洞存活下去;并且很可能引起一些其它的随机的运行错误。

    假如在设计时就决定需要强杀,而不是正常退出某个线程,那么良好的做法应当是重审我们的设计。

    当然了,如果我们只是在写一些demo、示例、临时解决方案、或者非长期稳定工作的短小进程,强杀线程也无可厚非。只是在拿起屠刀的时候,一定要清楚,自己正在做什么,以及带来的影响是什么。


    参考文章:

  • 1. <thread> - C++ Reference
  • 2. C++11 FAQ中文版:线程(thread)
  • 3. C++对象是怎么死的?POSIX线程篇
  • 4. PTHREAD_SETCANCELSTATE(3) Linux Programmer's Manual
  • 5. CloseHandle(),TerminateThread(),ExitThread()的区别
  • 6. 【服务器程序死锁 1】调用TerminateThread终止线程所导致的死锁问题
  • 7. 线程天敌TerminateThread与SuspendThread
  • 8. 预防Windows应用程序挂起
  • 9. Calling Win32 TerminateThread() via unmanaged code
  • 10. TerminateThread and Memory Leak
  • 11. Why you should never call Suspend/TerminateThread (Part I)
  • 12. 一个 Linux 上分析死锁的简单方法
  • Published by orzz.org(). (https://orzz.org/why-shouldnt-i-kill-a-thread/)

    1. sdfs公馆说道:

      progress 不用加锁么?

      • DarkCat说道:

        @sdfs公馆 需要。进程间通讯在windows下可以使用互斥量(Mutex)、信号量(Semaphore)等手段做同步。但之所以TerminateProcess比TerminateThread安全的原因在于TerminateProcess一般并不会引起资源泄漏。

    2. 妹子糗事说道:

      网站不错,雁过留痕,欢迎互访!

    3. 煜婷说道:

      我是来打酱油的

    4. 观象士说道:

      实际用的话,progress,cancel_flag都应该加锁;
      不过就简单举例来说这样就足够说明问题了

    5. Frost说道:

      。。。

    发表回复

    您的电子邮箱地址不会被公开。 必填项已用*标注

    此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据