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

我们知道,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),还没来得及解锁就被咔嚓了,那这个锁就永远不会被解掉了,于是全部依赖这个锁的其他线程都华丽丽的死锁掉了。

我们来看看以下这个小样例:

#include <windows.h>
#include <stdio.h>
 
DWORD __stdcall DoSomething(void*)
{
    for(int i = 0;; ++i)
    {
        ::printf("%d ", i);
    }
    return 0;
}
 
int main(void)
{
    HANDLE h = ::CreateThread(NULL, 0, DoSomething, NULL, 0, NULL);
    ::Sleep(1000); // Just wait one second
    ::TerminateThread(h, 0);
 
    ::printf("Hello World!\n");
 
    return 0;
}

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

事实上在一些库的函数调用里,甚至非常多我们觉得绝对不会出问题的操作上,假设突然被中断都会导致兴许代码的严重问题。比方把上面的DoSomething改成这样:

DWORD __stdcall DoSomething(void*)
{
    for(int i = 0;; ++i)
    {
        delete [] (new char[1024]);
    }
    return 0;
}

在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种方法能够解决无限循环导致线程不会退出的情形。
最常规的方法是用一个全局标记做退出通知:

while(!g_exit) /*Do something*/ ;

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

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

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

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

我们处理耗时操作的时候,往往不会在主线程中进行,由于那会导致主线程的假死。假设主线程负责了GUI,那么GUI也就死掉了。因此常规的处理方法是开启一个线程专门运行耗时操作。
在这个时候,假设用户希望Cancel掉这个耗时操作,一般的做法是停止操作并退出工作线程。这就要求我们可以把一个完整的耗时操作分解成若干小粒度的,不怎么耗时的操作。
事实上假设我们须要显示耗时操作的工作进度的话,非常自然的就会去做这样的分解。

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

#include <pthread.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
 
bool cancel_flag = false;
int  progress    = 0;
long result      = 0;
 
long calculate_fibonacci(int n, bool& is_exit, int& prog)
{
    if (is_exit) return 0;
 
    if (n == 1) return 1;
    if (n == 0) return 0;
 
    long r = calculate_fibonacci(n - 1, is_exit, prog)
           + calculate_fibonacci(n - 2, is_exit, prog);
 
    if (prog < n) prog = n;
 
    return r;
}
 
void* proc_fibonacci(void* p)
{
    if (!p) return NULL;
    int n = *static_cast<int*>(p);
 
    printf("Calculating...\n");
    result = calculate_fibonacci(n, cancel_flag, progress);
 
    return NULL;
}
 
void* proc_progress(void* p)
{
    if (!p) return NULL;
    int n = *static_cast<int*>(p);
    int o = 0;
 
    while (!cancel_flag)
    {
        if (o < progress)
            printf("%0.2f%%\n", double(o = progress) * 100 / n);
        if (o >= n) break;
        usleep(100000); // 100 milliseconds
    }
 
    return NULL;
}
 
int main(void)
{
    pthread_t th_f, th_p;
    int n = 0;
 
    // enter a number
    printf("Please enter a number: ");
    scanf("%d", &n);
 
    // create threads
    {
        int rc = pthread_create(&th_f, NULL, proc_fibonacci, (void *)&n);
        if (rc) exit(EXIT_FAILURE);
    }
    {
        int rc = pthread_create(&th_p, NULL, proc_progress, (void *)&n);
        if (rc) exit(EXIT_FAILURE);
    }
 
    // wait for 5 seconds
    struct timespec ts;
    {
        int rc = clock_gettime(CLOCK_REALTIME, &ts);
        if (rc) exit(EXIT_FAILURE);
    }
    ts.tv_sec += 10;
    if (pthread_timedjoin_np(th_f, NULL, &ts))
    {
        // cancel the thread
        printf("Time is out, cancel...\n");
        cancel_flag = true;
    }
 
    pthread_join(th_p, NULL);
    if (!cancel_flag)
        printf("result = %ld\n", result);
 
    return 0;
}

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

Please enter a number: 46
Calculating...
32.61%
73.91%
78.26%
80.43%
82.61%
84.78%
86.96%
89.13%
91.30%
93.48%
95.65%
Time is out, cancel...
root@mark-desktop:~/Desktop/test#

在这里,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. 【server程序死锁 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 上分析死锁的简单方法

  • 你可能感兴趣的:(线程)