wdk tips (7.1): 线程的创建和销毁

虽然内核开发人员从一开始就要考虑多线程的问题,但用户态开发人员曾经有过一段美好的生活:他们只需关心一条线程(多半是UI线程)并且不必在乎太多性能问题:即使你在主逻辑里嵌套了无数层循环都没关系,该死的摩尔定律替你搞定一切问题。进入多核时代后,用户态开发人员终于发现了他们忽略很久的,但及其重要的一个技术点:多线程。朋友,好生活已经结束了,欢迎你来到混乱的时代。

我知道现在来写这篇东西似乎不合时宜,因为网上已经有无数文章讨论过多线程问题了,各个社区还开发了一个又一个的线程框架帮你解决烦人的琐事,不过我今天的主要目的是为了引出某个内核开发中的棘手问题(就是7.2要讲的,先按下不表),所以各位看官先放小弟一马,让我把旧事拉出来说说完。

说到多线程,最烦人的其实是同步问题。关于这一点我很赞同osr邮件列表里的joe老师的观点:用户态程序不应该出现(自定义的)锁,任何时候你发现自己需要考虑用锁来同步了,就说明你的设计出了问题。同步这摊子事我有一堆话要说,但不是今天,今天我要说另一个比较容易被忽略的点:线程的创建和销毁。许多人不知道如何正确的创建和销毁线程,我看到过无数错误的写法,程序奇迹般的运行正常,但错的就是错的,现在不出问题,不代表以后不会。

创建

线程创建的api是CreateThread,关于这个api只有一条原则需要注意:绝对不要去用它。让我们把时间回退到上实际70年代,那时c语言刚诞生不久,c run time library也才成型,多任务还是个高级玩意儿,如果当时就有咨询公司这种东西,他们甚至可以靠培训多任务相关技术发大财。很自然的,c运行库的作者没有考虑多线程的问题,他们假设整个c语言程序只有一条线程,没有切换,自然也没有重入,所以c运行库里有数不清的全局变量,errno就是最著名的一个。后来多任务出现了,进程和线程的概念也相继登场,这些全局变量就变得棘手了:它们会被重入。仔细推敲我们可以发现,这些全局变量其实不应该是整个地址空间可见的,而应该每个线程一份拷贝才对。实际上现在的c库就是这么干的,微软的msvcr把errno等东西放在TLS(线程本地存储块)中,创建线程的时候分配,销毁线程的时候回收。但是CreateThread作为系统api才不会管这些屁事呢,人家是系统级的,c运行库跟它没关系,问题就出在这里:你敢说你写的程序不用c运行库,所有的工作都用纯api完成?别扯了,还是听话别碰CreateThread为妙。ms vc中有替代函数_beginthreadex/_endthreadex,任何时候都必须用他们创建销毁线程。如果你用的是其他厂商的c库,就用他们提供的线程函数--不管那是什么东西,有多蠢,用就对了--别碰CreateThread。

销毁

没有哪样东西比线程的销毁更恶心人了。如同上面所说,ExitThread函数绝对不能碰,除此之外还需要注意的是:唯一正确的退出方式就是让它跑完所有代码自然退出。但很多时候自然退出根本就是一个奢望,假如你的主线程需要等所有线程退出后才能做下一件事,那么加一个超时时间就是非常必要的,因为你不能让主线程等太久,况且有些线程(特别是IO相关的线程)退不退的出还是个问题。倘若超时事件真的发生,我们就不得不做一件烂事:强制线程退出。这种做法隐患多多,我能想到的大概有以下几个:

1. 资源泄漏。假如线程开始的时候申请了内存,打开了文件,或者其他任何形式的资源,并且在自然退出前释放资源,那么_endthreadexTerminateThread后这些资源就泄漏了,没有人会去回收他们。

2. 锁的状态。假如线程开始的时候获得mutex,自然退出前释放,那么_endthreadexTerminateThread后mutex就进入ABANDONED状态,其他WaitSingleObject的点会返回WAIT_ABANDONED值。仔细想想你的代码有没有处理这个返回值,多半是没有吧…

3. IO相关的问题。如果你的GetOverlapResult调用将Wait参数置为TRUE,那么在IRP被完成之前它是不会返回的,强行退出线程会引起驱动的误会,驱动以为只要Complete了这个IRP,app就会做某类事情,实际上app没做。更糟的是如果GetOverlapResult调用将Wait参数置为FALSE并在随后的代码里进行有超时的等待,等不到就CancelIO,类似这样:

res = GetOverlapResult(…, FALSE);

if( !res )

{

  if( WAIT_TIMEOUT == WaitForSingleObject(overlap.hEvent, 5000))

  {

    CancelIo(m_hDriver);

  }

}

那么强退线程后CancelIo就有可能没被执行到,IRP可能永远都不会被Complete。没有什么比系统中存在一个永远不会complete的IRP更糟糕的事了,你的进程将永远杀不掉,系统的ShutDown过程也会被挂起,恭喜你拔电源把。

4. appverifier会直接crash进程。开发过程中一直挂着appverifier跑是个好习惯,它会把各种隐患暴露给开发人员。比如强退线程这一条,裸奔的程序还能继续执行下去,最终用户不会知道发生了什么事,但挂着appverifier的程序就会爆炸,烧掉你的硬盘,并引发9级地震。好吧我开玩笑的,但你的manager一定不会容忍crash这一点。

5. 如果调用了需要SEH才能实现的功能: raise/signal
这与TLS不同, 少了一个__try块是不能再后面补上的。
这才是比较严重的问题。
但windows下开发, 使用signal的代码确实不多。
(感谢OwnWaterloo提供)

那么到底有没有办法安全的强退线程呢,其实还是有几个的,我能想到的有以下几种:

1. 设置信号通知目标线程退出。比如定义一个BOOL exitThread值,目标线程的大循环就该写成这样

while( !exitThread)

{

  …

}

主线程则是这样:

exitThread = TRUE;

WaitForSingleObject(m_hThread, INFINITE);

这种做法绝大多数情况下有效,但是有race condition,exitThread会被重入。改成这样会更好一点

while ( WaitForSingleObject(hExitEvent,0) != WAIT_TIMEOUT )

{

  …

}

主线程

SetEvent(hExitEvent);

WaitForSingleObject(hThread, INFINITE);

但还是有问题,目标线程的逻辑里如果有调用WaitForSingleObject(…, INFINITE)无限等某事件,那么它还是退不出来。

2. 在主线程里这么干:

SuspendThread(hThread);

GetThreadContext(hThread, &THREADCONTEXT);

THREADCONTEXT.eip = my_exit;

SetThreadContext(hThread, &THREADCONTEXT);

ResumeThread(hThread);

WaitForSingleObject(hThread, INFINITE);

my_exit里释放资源,退出线程。这种做法除了IO相关问题外全都有效,写起来也很有快感,强行修改线程的执行路径,跟在内核里hook system call似的,太酷了,特黑客的感嚼。我对此的建议是:绝对不要这么做。

3. 把目标线程里的WaitForSingleObject换成WaitForSingleObejectEx,把Alertable参数设成TRUE,在主线程里这么干:

QueueUserAPC( UserAPCProc, hThread, 0 );
WaitForSingleObject( hThread, INFINITE ); 

UserAPCProc啥事也不用干,空函数就行。执行完QueueUserAPC函数后,目标线程的WaitForSingleObejectEx函数会立刻被唤醒,并返回WAIT_IO_COMPLETION状态。这是windows核心编程的作者jeffrey大牛道出的天机。这段代码写着也很有快感,居然用到APC耶,我是高手有木有!但是我对此的建议还是:绝对不要这么干。

4. 把目标线程里的WaitForSingleObject换成WaitForMultipleObjects,并在目标线程里这么写:

while ( WaitForSingleObject(hExitEvent,0) != WAIT_TIMEOUT )

{

  …

  HANDLE events[2];

  events[0] = hExitEvent;

  events[1] = hYourAnotherEventThatHaveToWait;

  if( WAIT_OBJECT_0 == WaitForMultipleObjects(2, events, FALSE, INFINITE) ) // you have to exit thread

  {

    …

  }

}

主线程则是这样:

SetEvent(hExitEvent);

WaitForSingleObject(hThread, INFINITE);

这是比较标准的做法,包括IO操作相关的一系列问题都能得到解决。个人建议如果你想让目标线程主动退出,最好采用这个手段,并且确保所有非主要线程里没有WaitForSingleObject这个函数出现。如果想等,就用WaitForMultipleObjects。

到这儿为止该说的差不多说完了,唯一的隐患就在IO那里。正确的退出要求线程从wait函数返回后执行CancelIo操作取消掉你的IRP,这需要驱动配合在IRP里设置CancelRoutine。假如没有CancelRoutine,那么CancelIo操作是失败的,前面讲的那些恐怖故事还是会发生。关于这一点,我打算下次再说。

posted @ 2011-03-27 01:24 gussing 阅读(1433) 评论(6) 编辑 收藏

 回复 引用 查看   
#1楼 2011-03-27 07:51 OwnWaterloo      
不够准确。

1. _endthreadex与 ExitThread 绝对不能碰?
不碰ExitThread该怎么正常的撤销线程?
不碰_endthreadex怎么回收_beginthreadex分配的资源?

要正常撤销由_beginthreadex产生的线程,这两函数是一定得去碰的。
问题是由谁去碰而已。

新创建的线程栈上的实际代码类似:
ExitThread( thread_routine(arg) );
_endthreadex( thread_routine(arg) );
如果thread_routine能返回, thread_routine的调用者自己就会调用_endthreadex与ExitThread。
上面说了, 这是两函数是正常退出的唯一方法。

所以, 主动调用ExitThread与_endthreadex与线程函数返回后被调用的区别就是: 从调用点所在frame到ExitThread(thread_routine(arg))所在frame的余下代码(这里面就包括C++栈上对象的析构函数)不能被执行。
仅此而已

这与"是否资源泄露"是完全不相干的问题。
主动退出, 也可以写出不泄露的代码。
等线程函数返回, 也可能写出泄露的代码。


2. CreateThread 别碰? 必须_beginthreadex?
你可以试一下, ChreateThread产生的线程里面使用的errno与主线程的errno是否是分开的, strtok这样的函数是否正常工作。

c runtime, 至少ms的是这样, 在发现TLS没有被正确初始化(线程不由_beginthread/ex产生)的时候会自行初始化。
所以这并不是什么大问题。
唯二两问题就是:
i. 如果由线程函数退出, 返回到的frame是
ExitThread(thread_routine(arg));
c runtime所用的资源无法被回收

如上所说, 可自行调用_endthreadex

ii. 如果调用了需要SEH才能实现的功能: raise/signal
这与TLS不同, 少了一个__try块是不能再后面补上的。
这才是比较严重的问题。
但windows下开发, 使用signal的代码确实不多。


3. _beginthreadex的难处
CreateThread实际如下:
CreateRemoteThread(GetCurrentProcess(), ...

也就是说, 如果打算创建一个remote thread, 是没有对应的c runtime版本的。

这个时候, 只有深入了解这些函数之间的区别, 而不是笼统的说这不能用那不能碰, 才能正确完成所需功能。

 回复 引用 查看   
#2楼[楼主] 2011-03-27 10:01 gussing      
太赞了,受教!
不过有个小小的问题我得申辩下:我没说_endthreadex不能碰。
除此之外。。。你都是对的,特别是最后一句。

 回复 引用 查看   
#3楼[楼主] 2011-03-27 10:02 gussing      
你跟CU里的OwnWaterloo是同一个人吧?大牛怎么有空来cnblog玩啊
 回复 引用 查看   
#4楼 2011-03-28 11:16 OwnWaterloo      
@gussing
windows许多高级特性我也不懂, 所以文章就没细看……

是同一个人……
cnblogs注册有一段时间了, 因为blog程序比cppblog做得好。
但这里主要关注的是.net, 只好潜水了……

 回复 引用 查看   
#5楼[楼主] 2011-03-28 12:34 gussing      
@OwnWaterloo
我仔细review了一下这篇文章,发现误解主要出在“不能主动调_endthreadex"上,实际我想说的是"不能主动调TerminateThread",因为我关注的重点是一个线程被另一个线程强退时的情况。写的时候没仔细看。

 回复 引用 查看   
#6楼 2011-03-28 18:39 OwnWaterloo      
@gussing
恩,对, TerminateThread 比 ExitThread 危险太多, 杀人越货而非自杀……