<<Win32多线程程序设计>>读书笔记

常见问题集

01:合作型(cooperative)多任务与抢先式(preemptive)多任务有何不同?(太老的东西,无用)

答:Microsoft Windows的前三个版本都允许同时执行多个程序,但分享CPU是程序(而非操作系统)的责任。如果有一个程序咬住CPU不放,其他程序就停摆了。

后者意思是操作系统能够强迫应用程序把CPU分享给其他人,程序员不需要什么额外的努力。

 

02:我可以在Win32s中使用多个线程吗?(太老的东西,无用)

答:Win32 API被移植到Windows3.1上,称为Win32s。不幸的是许多功能无法移植。Win32s既不支持抢先式多任务也不支持多线程。

 

03.线程和进程有何不同?

答:没多大实践意思,可以说一大堆出来。

 

04:线程在操作系统中携带多少“行李”?

答:线程在任何时刻的状态被定义在线程上下文中(不断在内存和寄存器中切换)

 

05:Context Switch是怎么发生的?

答:当硬件计时器认为某个线程已经执行够久了,就会发出一个中断,于是CPU取得目前这个线程的当前状态,也就是把所有寄存器内容拷贝到堆栈之中,再把它从堆栈拷贝到一个CONTEXT结构。

为什么不从寄存器直接拷贝到CONTEXT结构呢?

 

06:为什么我应该调用CloseHandle()?

答:如果一个进程常常产生“工作线程”而老是不关闭线程的handle,那么这个进程可能最终有数百甚至上千个开始的“线程内核对象”留给操作系统去清理,这样的资源泄露可能会对效率带来负面的影响。

 

07:为什么可以在不结束线程的情况下关闭其handle?

答:线程对象的默认引用计数是2.当你调用CloseHandle()时,引用计数下降1,当线程结束时,引用计数再降1.只有当两件事情都发生了(不管顺序如何)的时候,这个对象才会被真正清除。

如果线程还未结束,但是我调用了两次CloseHandle,第二次会失败,在通过代码调试时会弹出0xC0000008Invalid Handle异常,但是程序还可以继续运行,所以在Release下看不出来。(我已验证)

 

08:如果线程还在运行而我的程序结束了,会怎样?

答:当然,所有线程被强迫结束。

 

09:什么是MTVERIFY?

答:作者自己定义的一个宏,可以不用太关注。

 

10:我如何得知一个内核对象是否处于激活状态?

答:关于dwMilliseconds,有一个特别重要的用途,但很少被人注意。设定值为0,使你能够检查handle的状态并立刻返回,没有片刻停留。如果handle已经备妥,那么这个函数返回成功并传回WAIT_OBJECT_0.否则这个函数立刻返回并传回WAIT_TIMEOUT.

用在什么场景下?

 

13:我如何在主线程中等待一个handle?

答:使用MsgWaitForMultiObjects修改主消息循环。

用在什么时候?

 

14:如果线程在critical section中停很久,会怎样?

答:不会怎样,操作系统不会当掉。用户不会获得任何错误信息。最坏你情况是主线程需要使用这被锁定的资源时,程序会挂在那儿,动也不动。

 

15:如果线程在critical section中结束,会怎样?

答:临界段资源一直不能被释放,可能出现死锁。

 

16:我如何避免死锁?

答:采用一次等两个资源的办法(即要不统统获得,要不统统没有)

 

17:我能够等待一个以上的critical section吗?

答:不能。

 

18:谁才拥有semaphore?

答:如果锁定成功,你也不会收到semaphore的拥有权。因为可以有一个以上的线程同时锁定一个semaphore,所以谈semaphore的拥有权并没有多少意义,即semaphore没有所谓的“独占锁定”,没有拥有权的观念。

 

19:Event Object有什么用途?

答:略,多用用就清楚了。

 

20:如果我对着一个event对象调用PulseEvent()并且没有线程在正在等待,会怎样?

答:产生的这次事件会被忽略。

 

21:什么是overlapped I/O?

答:一个简单的回答:overlapped I/O是Win32的一项技术,你可以要求操作系统为你传送数据,并且在传送完毕时通知你。这项技术使你的程序在I/O进行过程中仍然能继续处理事务。

 

22:Overlapped I/O在Windows 95上有什么限制?(现在谁还用95?!可以忽略)

答:只适用于named pipes,mailslots,serial I/O,以及socket()或accept()传回来的sockets,它并不支持磁盘或光盘中的文件操作。

 

23:我能够以C runtime library使用overlapped I/O吗?

答:你将不可能藉由C runtime library中的stdio.h函数而使用overlappedI/O。例如你不可能使用fwrite,而只能使用WriteFile实现overlapped I/O。看看参数就知道了,呵呵。

 

24:Overlapped I/O总是异步地执行吗?

答:不一定。如果数据已经被放进cache中,或操作系统认为它可以快速地完成I/O操作,那么文件操作就会在ReadFile或WriteFile返回之前完成,而ReadFile或WriteFile返回TRUE。这种情况相当于就是同步操作了,但是这种情况下,文件handle处于激发状态,对文件的操作可被视为像overlapped一样。

 

25:我应该如何为overlapped I/O产生一个event对象?

答:以文件handle作为激发机制,有一个明显的限制,那就是没办法说出到底是哪一个overlapped操作完成了。因为可能有多个线程操作同一个文件。

OVERLAPPED结构中的最后一个栏位,是一个event handle。如果你使用文件handle作为激发对象,那么此栏位可为NULL。当这个栏位被设定为一个event对象时,系统内核对象会在overlapped操作完成的时候,自动将此event对象给激发起来。由于每一个overlapped操作都有它自己独一无二的OVERLAPPED结构,所以每一个结构都有它自己独一无二的一个event对象,用以代表该操作。

有一件事很重要:你所使用的event对象必须是手动重置而非自动重置的。如果你使用自动重置,可能还没到Wait...的时候事件就已经触发了,等到Wait...执行的时候,激发状态被遗失了,导致等待函数一直不返回。但是一个手动重置的event,一旦激发,就一直处于激发状态,直到你动手将它改变。

 

26:ReadFileEx和WriteFileEx的优点是什么?

答:使用overlapped I/O并搭配event对象,会产生两个基础性问题。第一个问题是,使用WaitForMultiObjects(),你只能够等待最多达MAXIMUM_WAIT_OBJECTS个对象。第二个问题是,你必须不断根据“哪一个handle被激发”而计算如何反应。你必须有一个分派表格和WaitForMultiObjects的handles数据结合起来。

这两个问题可以靠一个所谓的异步过程调用(APC)解决。只要使用“Ex”版的ReadFile和WriteFile,你就可以调用这个机制。

 

27:一个I/O completion何时被调用?

答:接26。然而,Windows不会贸然中断你的程序,然后调用你提供的这个回调函数。系统只有在线程说“好,现在是个安全时机”的时候才调用你的回调函数。以Windows的说话就是:你的线程必须在所谓的“alertable”状态之下才行。如果有一个I/O完成,而线程不处于“alertable”状态,对I/O completion routine(即对我们提供的回调函数)的调用就会暂时被保留下来。因此,当一个线程终于进入“alertable”状态时,可能已经有一大堆储备的APCs等待被处理。

如果线程因为以下五个函数而处于等待状态,而其“alertable”标记被设置为TRUE,则该线程就是处于“alertable”状态:

SleepEx、WaitForSingleObjectEx、WaitForMultiObjectsEx、MsgWaitForMultiObjectsEx、SignalObjectAndWait。

 

用于overlapped I/O的APCs是一种所谓的user mode APCs(即上面的这种)。WindowsNT另有一种所谓的kernel mode APCs。kernel mode APCs也会像user mode APCs一样被保存起来,但一个kernelmode APCs一定会在下一个时间片被调用,不管线程当时正在做什么。Kernel mode APCs用来处理系统机能,不在应用程序的控制之中。

 

我有个问题,上面说的线程是不是指调用ReadFileEx和WriteFileEx的线程?这个回调函数被调起来的时候,属于哪个线程?如果还有APCs未执行,线程结束了,会出现什么情况?如果线程一直不处于alertable状态,会出现什么情况?(待验证)

 

 

 

28.我如何把一个用户自定义数据传递给I/O completion routine?

答:I/O completion routine需要一些东西以了解其环境。如果它不知道I/O操作完成了什么,它也就很难决定对此数据做些什么。使用APCs时,OVERLAPPED结构中的hEvent栏位不需要用来放置一个eventhandle。Win32文件上说此时,hEvent栏位可以由程序员自由运用。那么最大的用途就是:首先配置一个结构,描述数据来自哪里,或是要对数据进行一些什么操作,然后将hEvent栏位设定指向该结构。

也就是说,可以通过hEvent将用户自定义数据传递给I/O completion routine,标明具体发生的事。

 

29.我如何把C++成员函数当作一个I/O completion routine?

答:方法跟C++成员函数当作线程函数一样。将静态成员函数作为I/O completionroutine,并设法将对象指针this当作参数传进去,然后在回调函数中通过传进去的对象指针调用成员函数。

 

30.在一个高效率服务器上我应该怎么进行I/O?

答:虽然APCs是完成overlapped I/O的一个非常便捷的方法,但它们还是有它们的缺点。最大的问题就是,有好几个I/OAPIs并不支持APCs,如listen()和WaitCommEvent()便是两个例子。APCs的另一个问题是,只有发出overlapped请求的那个线程才能提供callback函数。然而在一个“scalable”系统中,最好任何线程都能够提供服务events。

所谓“scalable”系统,是指借助RAM或磁盘空间或CPU个数的增加而能够提升应用程序效能的一个系统。

在Windows NT 3.5中有第四种overlappedI/O的存在,称为输入输出完成端口。靠着“一大堆线程服务一大堆events”的性质,输入输出完成端口比较容易建立起“scalable”服务器。

尽管名称类似,I/O completion ports和APCs中所用到的I/Ocompletion routine没有任何关联。

I/O completion ports解决了我们截至目前看到的所有问题:

与WaitForMultiObjects不同,这里不限制handles的个数。

允许一个线程将一个请求暂时保存下来,而由另一个线程为它做实际服务。(是不是指蓝色字体的那一个问题?估计是)

默认支持scalable架构。

 

四种overlapped I/O是指通过等待文件句柄,等待event,借助回调,借助输入输出完成端口以得到I/O完成的事件通知。(通过书上本章的总结,已确认是这样)

 

现在终于明白为什么叫overlappedI/O了,简单一点说它实际上就是异步输入输出还好理解一些。对于多个线程同时异步操作同一个文件,这些操作就会排队起来一个一个处理,换句话说就是操作叠起来放着,一个个拿出来操作,所以是叫做overlapped。

 

31.为什么一个I/O completion ports是如此特殊?

答:我大略可以这样描述一个completion port:它是一个机制,用来管理一堆线程如何为completedoverlapped I/O服务。然而,completion port远比一个简单的分派器丰富得多,I/O completion port也像一个活门一样,保持一个CPU或多个CPUs

尽可能的忙碌,但也避免它们被太多的线程淹没。I/O completion port企图保持并行处理的线程个数在某个数字左右。一般而言你希望让所有的CPUs都忙碌,所以默认情况下并行处理的线程个数是CPUs的个数。

 

简单一点理解输入输出完成端口以及实现过程:(非常重要的理解)

 

我们可能存在多个线程对一个或多个可以实现异步输入输出的对象(比如文件,socket)进行异步操作,必然的就会出现很多输入输出完成的通知,因为之前的三种通知机制都有缺点,而输入输出完成端口机制呢,解决了这个问题。我们可以创建一个输入输出完成端口对象(通过CreateIoCompletionPort),然后将这些异步输入输出对象关联到这个输入输出完成端口对象(通过CreateIoCompletionPort),此时,如果我们对文件或socket进行异步输入输出操作,当操作完成时,这些输入输出完成通知就会被放到这个输入输出完成端口对象中去,为了处理这些通知呢,我们就产生一些线程,每个线程都循环不断地从这个输入输出完成端口对象中去获取通知(通过GetQueuedCompletionStatus),然后根据不同的通知进行对应的处理。实际上,这个东东跟APCs差不多,APCs是系统自动调用我们的回调函数处理,而这个地方是我们产生线程主动从完成端口对象中获取完成通知再做对应处理,最大的差别就是被动和主动的区别,当然还有之前提到的差别。

为了让这些完成通知都能得到处理,显然,我们用来从完成端口获取通知的线程得在输入输出操作发生前设置好。即

创建完成端口、创建异步输入输出的对象,并将对象关联到完成端口->创建若干线程开始从端口获取通知->异步输入输出对象上开始有输入输出操作发生

 

32.一个I/O completion port上应该安排多少个线程等待?

答:合理的线程个数应该是CPU个数的两倍再加2.(经验值,应该是说不准的)

 

33.为什么我不应该使用select()?

答:...中提到,凡使用select()的应用程序,其效率可能受损,因为每一个网络I/O call都会经过select(),因而招致严重的CPU额外负担。这种效率在CPU的使用率不是关键因素时,可以被接受,但是当需要高效率时,当然就会带来问题。

 

通过查看select函数的声明,可以知道select的功能是从一个或多个socket上等待网络I/Ocall的发生,比如收数据。

 

34.volatile如何影响编译器的最优化操作?

答:考虑这样一种我们经常用到的情况,我们通过一个bool类型的变量控制一个工作线程的循环退出,例如

DWORD ThreadProc(LPVOID lpParameter)

{

    while(!bExit)

    {

       //执行操作

    }

}

编译器最优化的结果是,设法把常用到的数据(比如bExit)放在CPU的内部寄存器中。当我想结束这个线程的时候,我在另外一个线程中改变了bExit的值,但是,因为使用bExit的线程根本不知道bExit已经改变了,还在那儿傻傻地从寄存器中取值,导致线程永远不能退出。

解决办法就是给bExit变量加上volatile声明,例如bool volatile bExit;这样,编译就不像刚才那样优化了,每次都是从内存中读取bExit的值,虽然慢一些,但是至少是正确的。

 

为什么我们经常都没用volatile,但是程序还是没问题呢?即怎么写一段代码验证一下?

 

35.什么是Readers/Writers lock?

答:也就是传说中的读写锁,需要从网上下载一个可以信任的用或者从书上抽取。

 

36.一次应该锁住多少数据?

答:只能视情况而定。一次全都锁住,不容易死锁,但是效率低。分成几次锁,容易死锁,效率高一些。

 

37.我应该使用多线程版本的C运行时库吗?

38.我如何选择一套适当的C运行时库?

39.我如何使用_beginthreadex()和endthreadex()?

40.什么时候我应该使用_beginthreadex而不是CreateThread()?

41.我如何使用Console API取代stdio.h?

42.为什么我不应该使用_beginthread?

43.我如何以一个C++成员函数当作线程的起始函数?

44.我如何以一个成员函数当作线程起始函数?

45.我如何能够阻止一个CWinThread线程对象销毁它自己?

46.CWinApp和主线程之间有什么关系?

47.我如何设定AfxBeginThread()中的pThreadClass参数?

48.我如何对一个特定的线程调试?

49.如果一个新的线程使用勒沃的DLL,我如何被告知?

51.为什么我在DllMain中启动换一个线程必须特别小心?

答:会死锁的,呵呵。

52.我如何在DLL中设定一个thread local storage(TLS)?

答:参考那几个API函数,多看看MSDN就行了。

53._declspec(thread)的限制是什么?

答:

如果一个对象拥有构造函数或析构函数,它就不能被声明为_declspec(thread)。因此,你必须在线程启动时自己动手将对象初始化。

另一个限制比较苛刻些。一个DLL如果使用了_declspec(thread),就没有办法被LoadLibrary()载入。因为线程局部节区的大小计算是在程序启动时完成的,没有办法在一个新的DLL载入时重新计算。虽然LoadLibrary()在WindowsNT中通常会成功,但是当DLL之中的函数被调用而开始执行,就会产生一个"Protection fault".

54.我应该在什么时候使用多线程?

答:线程实际应用于四个主要领域。任何应用程序都可以被归类为其中某些领域。在每个领域之中存在着或可争辩的重叠关系。

offloading time-consuming task.由主线程(GUI线程)负责,让用户界面有较好的反应。

Scalability

Fair-share resourece allocation

Simulations

55.我能够对既有程序进行多线程操作吗?

答:考虑方面较多

56.我可以在我的数据库应用程序中使用多线程吗?

答:感觉现在的应用程序都支持吧,平常涉及得较少,呵呵。书上讲起来挺复杂的。

 

 

第1章 为什么要“千头万绪”

 

这一章用了一些现实中的例子说明我们需要多线程,并讲了一下多线程的发展历史。之后再介绍了进程,线程,线程上下文切换,RaceCondition,原子操作(就是不被中断的操作)。最后说明多线程虽然有需要,但是用起来还是很有挑战的,需要小心设计。

这一章都是比较简单的概念,对我来说没有多少实践意义,可以忽略。

 

第2章 线程的第一次接触

 

1.例如,MsgWaitForMultipleObjectsEx是Win32程序的运转中心,都几乎没有任何范例程序正确使用过它。

(的确,我都从来没有用过它,看看它究竟能给我带来什么?)

 

2.多线程程序无法预期(所以很难重复其执行过程);执行次序无法保证(可能后产生的线程先运行);上下文切换可能在任何时刻任何地点发生;线程对于小的改变(比如加一行无关紧要的代码)有高度的敏感。

 

3.在多线程中,如果使用的是单线程的C运行时库中的函数,例如printf,可能会出现几个printf打印的内容混在一起,使用多线程C运行时库中的函数就没问题。

以前还不知道,有时间写个程序测试一下是不是这样的?

答:以前好像试过,确实会这样。

 

4.什么时候会用到AttachThreadInput,什么时候需要在不同线程中创建窗口?PostThreadMessage函数?

答:前两个问题参考MSDN中AttachThreadInput部分。

 

In the thread to which the message will be posted,call PeekMessage as shown here to force the system to create the message queue.

PeekMessage(&msg, NULL, WM_USER, WM_USER,PM_NOREMOVE)

The thread to which the message is postedretrieves the message by calling the GetMessage or PeekMessage function.

确认一下,调用GetMessage会不会产生消息队列?

答:会。

在SIP+RTP中,界面进程和适配器进程是通过PostThreadMessage进行通知的。

 

5.为了安全防护的缘故,你不可能根据一个线程ID而获得其句柄(仅仅针对Windows NT andWindows Me/98/95,在XP等上可以通过OpenThread获得句柄的,这也是为什么在VC6.0中默认OpenThread是未声明的原因。为什么在XP等中又允许了呢?)。ID和句柄有什么关系?

参考Thread Handles and Identifiers:

The CreateThread andCreateRemoteThread functions also return an identifier that uniquely identifies the thread throughout the system.

也就是说,这个线程ID是整个系统唯一的,比如我在A进程获取了一个线程ID,我完全可以在B进程中通过OpenThread根据这个ID获取该线程的句柄。(已验证可以这样,比如根据这个ID获取句柄,通过这个句柄暂停,结束其他进程创建的线程)。

OpenThread和OpenMutex这些函数类似,后者只不过通过全局性名字跨越进程共享内核对象,前者通过ID,因为线程就没有名字嘛。句柄是进程私有的,ID是全局的,通过ID更容易跨越进程共享内核对象(用DuplicateHandle还需要进程ID,和OpenThread比起来麻烦得多),这就是线程ID和线程句柄的关系。

 

6.GDI和内核对象的主要区别是内核对象有安全属性,并且可以跨越进程,而GDI没有安全属性,不能跨进程。

 

7.当你产生一个线程时,线程对象的默认引用计数是2,当你调用CloseHandle时,引用计数下降1,当线程真正结束时,引用计数再降1.只有当两件事情发生了(不管顺序如何)的时候,这个线程内核对象才会被真正清除。(这就是为什么可以在不结束线程的情况下关闭其句柄的原因。)

 

8.我们确实可以用GetExitCodeThread()等待一个线程结束,然而这并不是好方法。(目前我想到的正常等待的方法是WaitForSingleObject,根据书上后面讲的,这就是正确的办法。因为前者只能用循环处理,如果等待时间长,会耗费很多CPU。)

 

9.GUI线程的定义是:拥有消息队列的线程。任何一个特定窗口的消息总是被产生这一窗口的线程抓到并处理。所有对此窗口的改变也都应该由该线程完成。(即GetMessage,PeekMessage不能获取到其他线程创建的窗口的消息。)

 

10.如果一个工作线程需要输入或输出错误信息,它应该授权给UI线程来做。并且将结果通知给工作线程。

 

11.你或许已经从这一章中嗅出多线程程序困难重重的味道。这个例子可以带给你一个与本书其他较大程序一致的设计目标:

简单和安全,更甚于复杂和速度!

在整本书中,我企图告诉你如何在线程之间以最低表面积来设计程序。所谓表面积,意指必须被线程共享的数据结构。要知道,程序愈是密切地与线程有关系,愈是容易产生错误并发生racecondition。

为了符合“最低密度”这个目标,BACKPRNT的主线程负责把工作线程需要的所有信息捆绑在一起,然后产生工作线程。

 

12.成功的秘诀

1.各线程的数据要分离开来,避免使用全局变量。

2.不要在线程之间共享GDI对象。

3.确定你知道你的线程状态。不要径自结束线程而不等待它们的结束。

4.让主线程处理用户界面。

 

第3章 快跑与等待

 

1.除了常见的内核对象,还有其他的。比如用来监视目录的Change Notification(FindFirstChangeNotification),ConsoleInput(GetStdHandle,CreateFile).

以前对控制台一直比较神秘,今天试了一下,

HANDLE hConsole = GetStdHandle(STD_OUTPUT_HANDLE);

WriteFile(hConsole,"123", 3, NULL,NULL);

还真的把123输出到屏幕了,就跟printf一样。

 

2.原来以为MsgWaitForMultiObjects很有用,原来它的正确使用方式是改写消息循环,使得激发状态之handles得以想消息一样被对待。

=>我看我能用到的时候几乎没有,我们完全有其他办法解决问题,比如,主线程想知道工作线程是否结束时,完全可以将主线程阻塞死,没必要等着被通知,还得同时处理界面事件。

 

 

第4章 同步控制

 

1.我能够给你的最牢靠而最立即的警告就是,千万不要在一个critical section之中调用Sleep()或任何Wait...()API函数。

 

2.避免Dangling Critical Sections

由于Critical Section不是内核对象,如果进去Critical Section的那个线程结束了或当掉了,而没有调用LeaveCriticalSection()的话,系统没有办法将该CriticalSection清除,如果你需要那样的机能,你应该使用mutex。

用finally是否可以?

用try catch是否可以?

 

3.任何时候当一段代码需要两个或更多资源时,都有潜在性的死锁阴影。死锁的情况非常复杂,许多线程独立性彼此纠缠在一起。虽然有一些算法可以侦测并仲裁死锁状态,基本上它们仍嫌过于复杂。对大部分程序而言,最好的政策就是找出一种方法以确保死锁不会发生。稍后你会看到,强迫将资源锁定,使它们称为“all-or-nothing”(要不统统获得,要不统统没有),可以阻止死锁的发生。

 

4.mutex和临界段差不多,但它更有弹性。

如果拥有某mutex之线程结束了,该mutex会被自动清除的唯一情况是:此线程是最后一个与该mutexhandle有关联的线程(是不是指没有其他线程在用Wait...等它的情况?待验证)。否则此内核对象的引用计数仍然是比0大,其他线程(以及进程)仍然可以拥有此mutex的合法handle,然后,当线程结束没有释放某个mutex时,有一种特殊的处理方式。

在一个适当的程序中,线程绝对不应该在它即将结束前还拥有一个mutex,因为这意味着线程没有能够适当地清除其资源。不幸的是,我们并不身处一个完美的世界。为了解决这个问题,mutex有一个非常重要的特性。这性质在各种同步同步机制中是独一无二的。如果线程拥有一个mutex而在结束前没有调用ReleaseMutex,mutex不会被摧毁。取而代之的是,该mutex会被视为未被拥有以及“未被激发”,而下一个等待中的线程会被以WAIT_ABANDONED_0通知。不论线程是因为ExitThread()而结束,或是因为当掉而结束,这种情况都存在。

“知道一个mutex被舍弃”是一件简单的事情,但要知道如何应对可就比较困难了,毕竟mutex是用来确保某些操作能够自动被进行的,如果线程死于半途,很有可能被保护的哪些数据会被受到无法修复的伤害。

 

5.哲学家们可以使用WaitForSingleObject来等待吃饭,但那可就像CriticalSection一样了(同时也带来相同的死锁问题)。它们也可以使用WaitForMultiObjects()来等待,于是可以修正因EnterCriticalSection和WaitForSingleObject造成的死锁问题。(即同时等到两只筷子才算)。

任何时候只要你想锁住超过一个以上的同步对象,你就有死锁的潜在病因。如果总是在相同时间把所有对象锁住,问题可去矣!

 

6.与mutex不同的是,调用ReleaseSemaphore的那个线程,并不一定就得是Wait...()的那个线程。任何线程都可以在任何时候调用ReleaseSemaphore()解除被任何线程锁定的semaphore。

答:Semaphore用在多个线程去查询某个资源是否满的情况,比如允许接入的用户还有一个,如果不用Semaphore的话(用普通变量),当两个线程同时接入时,去查询时都认为还有一个资源,可以接入,但实际上,等接入后才发现已经超了。如果是同一个线程去查询、修改当然不用这么复杂,用普通变量计数就可以了。

 

7.Win32中最具弹性的同步机制就属event对象了,event是一种内核对象,它的唯一目的就是激发状态或未激发状态。这两种状态全由程序来控制,不会成为Wait...()函数的副作用。(是不是不像semaphore那样谁等到谁就拥有?

 

8.从这个程序的执行,我们可以发现另一个重点:操作系统会强迫让等待中的线程有轮番更替的机会。

 

9.除非有线程正在等待,否则event不会被保存下来。

 

10.这就是多线程程序设计中最常见的一种两难取舍:在最佳速度和最佳安全性之间取舍,在这里我宁愿选择比较慢但是比较安全的做法。

 

11.Interlocked Variables主要用于引用计数,允许对4字节的数值有些基本的同步操作,不需要动用到CriticalSection或mutex之类。在SMP(Symmetric Multi-Proccesors)亦可有效运作。

 

Interlocked...()函数的返回值代表计数器和0的比较结果。这一点对于实现我们曾经提过的“引用计数”非常重要。因为我们必须知道,“引用计数”何时到达0.如果没有这个比较,问题就回到了原点,你必须在增减之前先锁定该计数器,以便增减操作成为一个“不可切割”的操作。

 

12.最后一种优先级动态提升的情况可能发生在任何一个线程(不限属于哪一个进程)身上。那是在一个“等待状态”获得满足时发生的,例如有一个线程正在等待一个mutex,当Wait...()返回时,该线程的优先级会获得动态提升。这样的提升意味着CriticalSection将尽可能地被快速处理,而等待时间将尽可能缩短。

 

第5章 不要让线程成为脱缰野马

 

1.使用线程的一个常见问题就是如何能够在一个线程开始运行之前,适当地将它初始化。初始化最常见的理由就是为了调整优先级。另一个理由是为了在SMP系统中设定线程比较喜欢的CPU。第10章谈到MFC时我们会看到其他一些理由(m_bAutoDelete,即线程结束是否自动删除WinThread对象)。

基本问题在于,你需要一个线程handle,才能够调整线程的性质,但如果你以默认形式调用CreateThread(),新线程会如脱缰野马一下子就起跑了,你根本来不及进行初始化设定操作。

解决之道就是CreateThread的第5个参数,CREATE_SUSPEND.

 

2.SuspendThread的最大用途就是用来协助撰写调试器,调试器允许程序员控制之下,启动或停止任何一个线程。

 

 

第6章 Overlapped I/O,在你身后变戏法

 

这一章的核心理解已经在问答中完成了,剩下的仅仅是熟悉它,多多应用而已。

 

 

第7章 数据一致性

 

这一章首先介绍volatile关键字的使用(在问答中有分析),然后讲了一下事务的概念,并将其引入到多线程中,解决办法就是将数据一下子全部锁住,但是这样有一个缺点,可能效率较低,此时,我们又引入了读写锁,以提高效率。最后比较了一下不同锁定粒度的优缺点,并提出一些建议(我建议你的程序一开始尽量少用locks,然后,当你开始发现一些瓶颈时,再开始适量地使用locks。我有点不同意这个观点,瓶颈?)。

如果可能,我还是继续建议你避免在线程之间共享数据。

 

 

第8章 使用C Run-time Library

 

以前书上老是说要用_beginthreadex取代CreateThread,但是我一直没完全搞明白,这次一定要搞透彻。

 

1.什么时候该用_beginthreadex、什么时候该用CreateThread、什么时候该用AfxBeginThread?为什么?

答:通过下面的理解,现在终于明白了。

为了在线程函数中安全地使用C运行时库中的非线程安全函数,应该用_beginthreadex。

如果线程函数中,既没用到C运行时库中的非线程安全函数,也没有用MFC中的东西,可以放心使用CreateThread。

如果线程函数中用到MFC中的东西,就应该使用AfxBeginThread,并且,AfxBeginThread内部实际上是通过_beginthreadex实现的,使用AfxBeginThread后,你也可以放心在线程函数中调用C运行时库中的东西了。

 

2.注意,不要在一个MFC程序中使用_beginthreadex或CreateThread。为什么?

答:实际上,这样武断的说话会让别人产生误解,我就是受害者之一。好像CreateThread有什么缺陷似的,实际上,CreateThread无罪,用不同的函数创建线程,都是为了满足线程函数可以好好利用一些库中好东西。应该这样说,对于线程函数中调用了任何MFC函数或使用了MFC的任何数据的线程,应该使用AfxBeginThread或CWinThread::CreateThread()来创建线程。

 

3.为什么会出现单线程和多线程版本的C运行时库?单线程版本的C运行时库在多线程环境中会出现什么问题?微软是如何解决这个问题的?

答:因为C运行时库是在20世纪70年代产生出来的,那时内存容量还很小,多任务是个新奇观念,更别提什么多线程了,所以,刚开始的C运行时库是单线程的。

单线程的C运行时库中的某些函数使用了数个全局变量和静态变量,比如errno(相当于最后错误代码,比如一个线程设置了某个错误码,当它还未获取这个错误码时,另外一个线程改写了这个错误码,那么前一个线程还能获取得到正确的错误码吗?呵呵),strtok(这个函数处理会用到一块全局的内存,如果多个线程同时调用它,岂不坏事?呵呵),还有一些数据结构也没有做同步保护,比如fopen返回的一个FILE*,基本上它是一个指针指向C运行时库中的一个描述表格(descriptortable),如果多个线程同时使用FILE*操作文件,这个描述表格岂不被搞坏?呵呵。另外,C运行时库在进行它自己的内存配置操作时,也没有做线程保护。反正一句话,老的单线程版本的C运行时库根本不能适用于多线程环境。

Visual C++的折衷方案是提供两个版本的C运行时库。一个版本给单线程程序使用,一个版本给多线程程序。多线程版有两个很大的差别,第一,如errno之类的变量,现在变成每个线程各拥有一个(正是通过_beginthreadex函数实现的,这实际也是为什么我们要用_beginthreadex来创建线程的真正原因。不是采用的线程本地存储,而是通过在堆中分配内存实现的,参考_beginthreadex的源代码,黄建在SIP中搞的那套最后错误机制就没有考虑多线程问题,呵呵。第二,对多线程共用的数据结构以同步机制加以保护。

 

总结:基本上,对于现在的程序,我们都应该用多线程的C运行时库,难道我们还在乎使用多线程版损失的那些性能吗?呵呵,我反正不在乎。

在VC6中,当建立的是控制台程序时,默认链接到是单线程的C运行时库,在VS2005中,就算是控制台程序,默认链接到的已经改变为多线程版的C运行时库了,由此可证明上面的结论是正确的。

 

4.什么时候你必须选择使用多线程版本的C runtime library?

答:

对于Use MFC in a Shared DLL的MFC程序,必须链接到动态链接版的多线程Cruntime library。

对于Use MFC in a Static Library的MFC程序,必须链接到静态链接版的多线程Cruntime library。

也就是动态对动态,静态对静态,否则链接时就会出错。为什么?

因为在\MFC\Include\AFXVER_.H中,有如下代码:

#if defined(_AFXDLL) &&!defined(_DLL)

    #errorPlease use the /MD switch for _AFXDLL builds

#endif

 

#if defined(_AFXDLL) &&!defined(_MT)

    #errorPlease use the /MD switch (multithreaded DLL C-runtime)

#endif

也就是说,如果是动态链接版的MFC库,则必须使用动态链接版并且是多线程的C运行时库。

 

注意,d代表调试版,D才代表是DLL,记清了!

 

5.为什么说一定要用_beginthreadex取代CreateThread?为何平时我们都用CreateThread创建线程,但是仍然没有什么问题呢?何时我们可以用CreateThread创建线程?

答:通过查看_beginthreadex的源代码你可以知道,

        /* Initialize FlsGetValue function pointer */

        __set_flsgetvalue();

 

        /*

         * Allocate and initialize a per-threaddata structure for the to-

         * be-created thread.

         */

        if ( (ptd = (_ptiddata)_calloc_crt(1,sizeof(struct _tiddata))) == NULL)

               goto error_return;

 

        /*

         * Initialize the per-thread data

         */

 

        _initptd(ptd, _getptd()->ptlocinfo);

 

        ptd->_initaddr =(void *) initialcode;

        ptd->_initarg = argument;

        ptd->_thandle =(uintptr_t)(-1);

它会在堆中分配一些内存为本线程所用,以实现errno,strtok等的多线程安全,参考

struct _tiddata {

    unsigned long   _tid;       /* thread ID*/

 

 

    uintptr_t _thandle;         /* threadhandle */

 

    int     _terrno;           /* errno value */

    unsigned long   _tdoserrno;/* _doserrno value */

    unsigned int    _fpds;      /* FloatingPoint data segment */

    unsigned long   _holdrand;  /* rand() seedvalue */

    char *      _token;        /* ptr to strtok() token */

    wchar_t *   _wtoken;       /* ptr to wcstok() token */

    unsigned char * _mtoken;    /* ptr to _mbstok() token */

由此可知,由_beginthreadex创建的线程,必须用_endthreadex结束线程。书上说,当线程函数返回时,_endthreadex会被C运行时库自动调用。我跟踪了一下,确实如此。当然,你也可以在线程函数中显式调用它,以便设置线程的退出码。

 

由此可见,为了在新线程的线程函数中安全地调用C运行时库中的所有函数(我们很难保证我们新产生的线程函数中不调用C运行时库中的非线程安全函数),我们必须使用_beginthreadex创建线程。

 

平时我们用CreateThread创建线程没出现问题,那是因为在侥幸在线程函数中没有调用C运行时库中的非线程安全函数。

 

所以,除非我们能非常确定我们的线程函数肯定不会调用C运行时库的非线程安全函数(非线程安全函数有哪些?你能确定吗?呵呵,估计你不敢大声说我敢确定!),我们可以直接用CreateThread创建线程。

在书中,明确说明,如果你的线程函数中用到了malloc,free,new,delete,那么你就得使用_beginthreadex创建新线程,当然除了这个条件,还有stdio.h或io.h中声明的任何函数,浮点变量或浮点运算函数,调用任何一个使用了静态缓冲区的C运行时库函数,如asctime,strtok,rand等。我敢说,我们在线程函数中一般都会用到new这些东西,所以说,要想不用_beginthreadex还是比较难的!这说明,以前我们的代码都是有问题的。

 

 

6.我们可以不使用C运行时库吗?

答:不行。由于启动代码(启动代码是可执行程序必须要的)和一些辅助函数都存在于C运行时库中,因而我们不可能在链接器中把它们去掉。

 

7.为什么我们不用_beginthread?

答:平时连_beginthreadex都不愿意用,怎么可能用_beginthread?呵呵,不想在根本不会用的函数上花费时间,想明白自己翻书或查MSDN去。

 

8.那些C运行时库中的非线程安全函数有替换的办法吗?

答:答案是肯定的,基本上都是用API来替换。比如

wprintf,GetStdHandle,HeapAlloc等

 

除非必须,我们没有必要放弃那么多好用的C运行时库函数不用,_beginthreadex也不算长得难看,用吧,呵呵!

 

 

第9章 使用C++

 

1.如何以C++类产生多个线程?

答:实际就是CWinThread那一套。

 

2.为什么C++可以让多线程设计明显地比较容易,比较安全?

答:参考5.

 

3.如何处理有问题的_beginthreadex()函数原型?

答:

uintptr_t _beginthreadex(

   void*security,

   unsignedstack_size,

   unsigned ( *start_address )( void * ),

   void*arglist,

   unsignedinitflag,

   unsigned *thrdaddr

);

第3,6个参数,这儿是unsigned int,但CreateThread中,都是DWORD,即unsignedlong,如果你完全以CreateThread的参数传递过去,在C++编译器中会编译通不过(C编译器检查没那么严格,所以可以通过)。

 

这个问题有两个解决方案。第一是把你的变量声明为unsigned,也就是_beginthreadex()所希望的类型。这个方法最简单,但如果函数原型有一天做了修正,你又得回头把所有的相关变量改回原型。(但实际上,这种可能性很小,微软不会干这种傻事,毕竟你提供的是库函数,一个广泛应用的东西,不是你想改就改的,所以,我觉得这种方法其实是不错的)。

 

第二个解决办法是将变量声明为CreateThread()所希望的类型,然后在丢给_beginthreadex()之前,再把它强制转换类型。我将使用这个方法,而强制类型转换的工作则交给一个typedef来完成,如此一来,万一函数有所修正,我们也可以很快,很方便的应对。例如,

线程函数定义为DWORD WINAPI ThreadFunc(LPVOID);但是我们按_beginthreadex()再定义一个线程函数指针

typedef unsigned (WINAPI * PBEGINTHREAD_THREADFUNC)(LPVOIDlpThreadParameter);调用函数时,将ThreadFunc传进去,强制转换成PBEGINTHREAD_THREADFUNC类型即可。线程ID参数道理一样。

 

总结,上面的理论都是建立在_beginthreadex()可能变化的基础上,如果它不变,什么解决办法都是完美的,呵呵。我相信它不变,故我愿意采用第一种方案。

 

4.我如何以一个C++成员函数当做线程起始函数?

书上讲这么多,实际上,我们已经掌握了正确的办法,那就是线程函数用类的静态成员函数,创建线程的时候把this指针传进去,在线程函数中通过this指针调用真正的执行体成员函数。书上想要实现的功能,就跟CWinThread一样,直接参考就行了,还研究个屁呀!

 

5.建立比较安全的锁?

答:书上的方法实际上就是MFC中CSingleLock,CMultiLock那一套东西,要实现的话直接参考就行了。如果程序使用MFC开发,直接用CSingleLock,CMultiLock更容易,还自己写个屁啊!

本质就是依赖“不管是函数返回,还是出现异常,对象的析构函数都会被正确调用”这个理论。

 

总结:通过上面的我们可以看到,书上讲的这些理论在MFC中已经实现得很好了,如果基于MFC开发,最好用现成的,如果实在没办法才自己写。

从面向对象编程来说,我们应该倾向于使用CWinThread,但由于熟悉程度的限制,我们没有好好利用它,以后得考虑一下。在V9中,我在统计处理模块类中使用了一个线程,将线程使用的数据和其他数据都作为类的成员,导致成员变量好多,是不是可以将其中一些东西提取出来,抽象成一个线程处理对象,估计会好得多。请再参考第10章的3.创建工作线程需要从CWinThread继承下来吗?

 

 

第10章 MFC中的线程

 

1.如果要在MFC程序中产生一个线程,而该线程将调用MFC函数或使用MFC的任何数据,那么你必须以AfxBeginThread或CWinThread::CreateThread()来产生这些线程。

 

2.学会使用AfxBeginThread

答:只要看看其源代码,我们就知道怎么回事,也知道怎么用了。

CWinThread* AFXAPI AfxBeginThread(AFX_THREADPROCpfnThreadProc, LPVOID pParam,

    intnPriority, UINT nStackSize, DWORD dwCreateFlags,

    LPSECURITY_ATTRIBUTESlpSecurityAttrs)

{

#ifndef _MT

    pfnThreadProc;

    pParam;

    nPriority;

    nStackSize;

    dwCreateFlags;

    lpSecurityAttrs;

 

    returnNULL;

#else

    ASSERT(pfnThreadProc!= NULL);

 

    CWinThread*pThread = DEBUG_NEW CWinThread(pfnThreadProc, pParam);

    ASSERT_VALID(pThread);

 

    if(!pThread->CreateThread(dwCreateFlags|CREATE_SUSPENDED, nStackSize,

       lpSecurityAttrs))

    {

       pThread->Delete();

       returnNULL;

    }

    VERIFY(pThread->SetThreadPriority(nPriority));

    if(!(dwCreateFlags & CREATE_SUSPENDED))

       VERIFY(pThread->ResumeThread()!= (DWORD)-1);

 

    returnpThread;

#endif //!_MT)

}

 

CWinThread* AFXAPI AfxBeginThread(CRuntimeClass*pThreadClass,

    intnPriority, UINT nStackSize, DWORD dwCreateFlags,

    LPSECURITY_ATTRIBUTESlpSecurityAttrs)

{

#ifndef _MT

    pThreadClass;

    nPriority;

    nStackSize;

    dwCreateFlags;

    lpSecurityAttrs;

 

    returnNULL;

#else

    ASSERT(pThreadClass!= NULL);

    ASSERT(pThreadClass->IsDerivedFrom(RUNTIME_CLASS(CWinThread)));

 

    CWinThread*pThread = (CWinThread*)pThreadClass->CreateObject();

    if(pThread == NULL)

       AfxThrowMemoryException();

    ASSERT_VALID(pThread);

 

    pThread->m_pThreadParams= NULL;

    if(!pThread->CreateThread(dwCreateFlags|CREATE_SUSPENDED, nStackSize,

       lpSecurityAttrs))

    {

       pThread->Delete();

       returnNULL;

    }

    VERIFY(pThread->SetThreadPriority(nPriority));

    if(!(dwCreateFlags & CREATE_SUSPENDED))

       VERIFY(pThread->ResumeThread()!= (DWORD)-1);

 

    returnpThread;

#endif //!_MT

}

 

由此可见,实际上,AfxBeginThread仅仅是对CWinThread的一个包装而已,当我们不想用AfxBeginThread的时候,直接用CWinThread也行。

 

3.创建工作线程需要从CWinThread继承下来吗?

答:请参考MSDN中的一句话,

创建辅助线程是一个相对较为简单的任务。只需两步即可以使线程运行:实现控制函数和启动线程。不必从CWinThread 派生类。如果需要特殊版本的 CWinThread,可以从该类派生,但大多数简单辅助线程都不需要这样做。无需修改即可使用CWinThread。

 

4.使用AfxBeginThread必须注意自动删除?

答:使用AfxBeginThread创建的线程,默认都是自动删除CWinThread对象的,所以,当你想利用AfxBeginThread返回的对象指针做一些特别的事情时,比如WaitForSingleObject,有可能,这个对象都已经删除了,你对对象的访问,会造成程序崩溃。

解决办法就是以暂停方式创建线程,然后通过返回的对象显式将其设置为不自动删除,再恢复线程的运行。这样,你就得自己负责删除CWinThread对象,记住!

 

5.我们究竟是用AfxBeginThread还是用直接用CWinThread?

答:书上说,你可以自行使用CWinThread以获得比AfxBeginThread()更好的控制。

看起来,按作者的意思,更倾向于后者,按我的意思,也倾向于后者。但是,对于简单的工作线程,即数据比较少的,继承一次确实麻烦,所以,对于简单的,我倾向于前者。

 

6.如果用CWinThread具体怎么做?

答:

首先从CWinThread继承,按书上的说法,还要新搞一个静态成员函数当线程函数,另外搞一个成员函数做我们想要的线程函数,但是,我看源代码,可以直接重写Run函数,它就相当于我们的线程函数了(已验证确实可以)。

 

7.从具体操作上来说,创建工作线程和界面线程有什么区别?

答:因为界面线程必须重写InitInstance,ExitInstance,实现DECLARE_DYNCREATE,DECLARE_MESSAGE_MAP,所以界面必须从CWinThread继承下来。

而工作线程不一定要重写这些函数,所以,它不是必须从CWinThread继承。当然,继承也不错,但得重写Run函数,因为默认的Run函数中是一个界面线程才需要的消息循环,或者提供一个我们的线程函数,这样就不使用已有的Run函数了。具体代码证明参考UINTAPIENTRY _AfxThreadEntry(void* pParam),它实际上传递给CreateThread的线程函数。

    // first-- check for simple worker thread

    DWORDnResult = 0;

    if(pThread->m_pfnThreadProc != NULL) //如果线程对象中的线程函数成员存在,则调用我们设置的线程函数。

    {

       nResult= (*pThread->m_pfnThreadProc)(pThread->m_pThreadParams);

       ASSERT_VALID(pThread);

    }

    // else-- check for thread with message loop

    else if(!pThread->InitInstance())

    {

       ASSERT_VALID(pThread);

       nResult= pThread->ExitInstance();

    }

    else

    {

       //will stop after PostQuitMessage called

       ASSERT_VALID(pThread);

       nResult= pThread->Run();

    }

 

由此可见,界面线程和工作线程有什么区别呢,最大的区别就是界面线程有一个默认的消息循环,只要把默认的Run一重写,消息循环就没了,就成了工作线程了,呵呵!

界面线程创建方法比较固定,就只有一个(继承),而工作线程创建方式比较灵活,怎么搞都可以。

 

8.MFC多线程中最重要的提示

略,具体参考MSDN。

 

如果我的线程函数中仅仅使用MFC中的CString等简单MFC对象,是不是也必须用AfxBeginThread或CWinThread创建线程?如果CreateThread会出现问题的话,会是什么错误现象?

 

 

第11章 GDI与窗口管理

 

1.最好的建议是只有一个界面线程(主线程)处理所有的界面相关工作,其他与界面无关的工作交给多个工作线程处理。

 

2.虽然你的线程正在等待SendMessage()的返回,但它还是可以处理外界对其拥有窗口的任何SendMessage()调用操作,甚至即使线程不处于主消息循环(其中有GetMessage()和DispatchMessage()操作)之中。如果Windows不这样设计,那么该线程所拥有的其他窗口就会停止反应,并且无法回答来自外界的任何SendMessage操作。如何实现的?

 

3.当Windows必须在线程之间SendMessage消息时,不论是否这些线程位于相同进程之中,总是有这种可能:目标线程被锁死,以至于等待线程永远醒不过来。这个问题在你跨越进程SendMessage消息时特别突出,因为你没有办法保证传送对象(目标线程)的行为。Win32提供了两个函数协助解决这个问题。

第一个函数是SendMessageTimeout()。

第二个函数是SendMessageCallback()。

 

这两个函数我们用得很少,看来还得重视起来!

 

4.何时应该产生界面线程?

答:书中举了一个取消操作的例子,以为找到了产生界面线程理想情况(我以前的编程总结中对取消的解决办法也是创建界面线程,看来我还是又错了),最后分析了半天,还是不产生界面线程为妙。最后最好的办法还是好好利用PeekMessage函数,即在长时操作中过段时间调用一次PeekMessage函数。

The main difference between the two functions isthat GetMessage does not return until a message matching the filter criteria isplaced in the queue, whereas PeekMessage returns immediately regardless ofwhether a message is in the queue.

 

The following example shows how to use PeekMessageto examine a message queue for mouse clicks and keyboard input during a lengthyoperation.

 

HWND hwnd;

BOOL fDone;

MSG msg;

 

// Begin the operation andcontinue until it is complete

// or until the user clicks themouse or presses a key.

 

fDone = FALSE;

while (!fDone)

{

    fDone = DoLengthyOperation(); //application-defined function

 

    // Remove any messages that may be in thequeue. If the

    // queue contains any mouse or keyboard

    // messages, end the operation.

 

    while (PeekMessage(&msg, hwnd,  0, 0, PM_REMOVE))

    {

        switch(msg.message)

        {

            case WM_LBUTTONDOWN:

            case WM_RBUTTONDOWN:

            case WM_KEYDOWN:

                //

                // Perform any requiredcleanup.

                //

                fDone = TRUE;

        }

    }

}

while循环中,界面能有反应接受用户的界面输入吗?

 

 

5.GDI对象被每一个进程拥有,而被每一个线程锁定。即如果一个线程正在使用一个GDI对象,它将会被锁住,其他线程没有办法再使用它。

我强烈反对在不同线程之间共享GDI对象!(只要参考1的做法就不存在此问题了,像GDI这种跟界面有关的工作,全放到主线程做去)

 

 

第12章 调试

 

1.所以我的结论是:检查每一件事情。(我也知道这样比较好,但是,每个函数都检查是一件很烦人的事,特别是自己的设计函数的时候,如果里面的每个调用函数都检查,可能每个函数都有复杂的返回值。我该怎么办?)

 

2.在每一个你的假设之处做检验工作,也就是加断言,这个我早就知道。

 

3.先以不考虑多线程的方式测试你新开发的东西以确定它逻辑是正确的,然后再将它加入已经稳定的代码中。这样做的好处是,至少确定了逻辑是正确的,剩下的问题就很可能是与多线程有关了。(好像现在我们都不习惯这样,呵呵)

 

4.利用线程调试对话框,这个我早就知道。

 

5.打印日志。打印日志最困难的地方是寻找程序中打印日志的关键地方?

记住,即使在一个GUI程序中你也可以拥有一个console窗口。这意味着你可以在GUI程序中使用printf()和puts()。不要尝试使用列表框或多行文字编辑框等控件,因为它们需要仰赖消息的传递,以及适当的重绘。而这种行为在程序已经出错的情况下是很难保证的。Console窗口由系统的设备驱动程序负责,即使你的程序当掉或在调试器中停止,console窗口仍然有反应。

把运转记录输出到stdout还有另外一个利益:很容易导向到文件。

书上说TRACE宏机制比较慢,我避免使用这种方法来对多线程程序调试,因为它所带来的额外负担以及效率冲击不是十分清楚。

 

这儿我还是不清楚怎么在GUI中使用printf?目前,我很少调试多线程了,感觉打印日志没有书上说的那么多需要关注的点。

 

6.内存记号(Memory Trails)

甚至即使你把输出导向到文件中,运转记录花费的时间所带来的冲击,仍然足够改变程序执行结果。如果要改善这种情况,我必须回到一个我所谓的“Memory Trails”的低阶技术中。

为了使用memory trail,你必须产生一个全局缓冲区,以及一个指向该缓冲区的全局指针。例如

char gMemTrail[16384];

char* pMemTrail = gMemTrail;

每当想印出某些东西到屏幕上或文件中,你就写一个记号到memory trail中。例如

*pMemTrail++ = 'D';

你的程序中的每一个跟踪点都应该写一个不同的记号。不论什么时候你想要,或是在程序当掉之后,你可以利用调试器看看memorytrail的内容,分析其间到底发生了什么事。它当然不像文字那么容易阅读,但总比乱猜的好。

Memory Trail可以大量降低彼此干扰的可能性,因为它既没有用到系统函数,也没有用到同步机制。然后也由于它不是同步操作,当两个线程同时写入一笔数据,memorytrails还是有可能遗失数据。如果你有许多线程,而其中有许多断点,这可能会造成严重的问题。

 

这个技术没有什么特别的,我只要掌握其思想就行。

 

7.硬件调试寄存器(实际上也就是打当某个内存的内容发生变化或被访问时的断点,当寻找内存越界错误时比较有用)

 

8.不同的电脑,调试版还是发布版等环境的变化都可能影响多线程程序的执行效果,所以各种情况都需要我们测试。

 

 

第13章 进程之间的通信

 

1.进程之间需要通信的常用情况有哪些?

答:估计很难说清楚,呵呵!

 

2.WM_COPYDATA

缺点:效率低,必须等着处理完才返回;接收端必须要有一个窗口才行;

Windows就是以共享内存来实现的WM_COPYDATA.

 

具体参考MSDN,应该很简单。

 

3.共享内存

算是比较高级的进程间通信方法,具体参考MSDN。

 

4.Anonymous Pipes,NamedPipes,Mailslots,OLE Automation,DDE

 

总结:

这一章的内容,MSDN中有非常具体的进程间通信部分,把那儿的搞定可能比看这儿的书有用。我觉得了解一些在哪些情况下应该设计为多进程的例子,比了解具体的进程间通信方法更有用,毕竟方法可以用的时候再学都不迟。

 

第14章 建造DLLs

 

1.向工作线程发送消息

如果你在一个工作线程中调用GetMessage(),那么消息队列便会产生,纵使你并未拥有窗口,这样你就可以通过PostThreadMessage()向其发送消息。

下面是我的测试,调用GetMessage确实能产生消息队列,但是要小心的是,虽然线程已经创建了,但是工作线程有可能还未执行到GetMessage,导致消息队列未创建,此时PostThreadMessage发送消息会失败的。

DWORD ThreadProc(LPVOID lpParameter)

{

    MSG msg;

    while (GetMessage(&msg, NULL, 0, 0) !=-1)

    {

        TRACE(_T("msg.message = %u\n"), msg.message);

 

        // 为什么弹出消息框后,GetMessage收不到2和WM_USER+1消息了,并且点击确定按钮后,GetMessage收到49312消息

        // 不断点击确定不断收到,为什么?

        // MessageBox(NULL, _T(""), _T(""),MB_OK);

    }

 

    return 0;

}

 

void CgggggryDlg::OnBnClickedButton1()

{

    HANDLE hThread = CreateThread(NULL,0, (LPTHREAD_START_ROUTINE)ThreadProc, NULL,0, &uThreadID);

}

 

void CgggggryDlg::OnBnClickedButton2()

{

    PostThreadMessage(uThreadID,0, 0, 0);

    // 为什么唯独消息为出现错误ERROR_MESSAGE_SYNC_ONLY,即The message can beused only with synchronous operations。

    // PostThreadMessage(uThreadID, 1, 0, 0); 

    PostThreadMessage(uThreadID,2, 0, 0);

    PostThreadMessage(uThreadID,WM_USER+1, 0, 0);

}

 

 

不过,工作线程产生消息队列的常用情况在?

答:

 

如何在工作线程中使用定时器?

答:这个问题我们经常碰到,如果简单一点的话,就用Sleep。以前,我曾经想用定时器实现,可惜没成功。通过上面的办法,实际上是可以实现的,书上的例子就是这样。

 

在工作线程的开始调用PeekMessage以产生消息队列,然后调用SetTimer(传一个回调函数进去),在回调函数中不停向当前线程(即工作线程)发送消息,工作线程中用GetMessage()产生一个消息循环,收到消息调用相应函数进行处理。(实际上,没必要调PeekMessage,因为SetTimer属于User32.lib,书上说只要是调用了User32.dll或GDI.dll中的函数都会产生消息队列,就算不产生,后面的GetMessage照样产生消息队列,照样可以)

VOID CALLBACK TimerProc(          HWNDhwnd,

                        UINT uMsg,

                        UINT_PTR idEvent,

                        DWORD dwTime

                        )

{

    TRACE("dwTick = %u\n",GetTickCount());

}

 

 

DWORD ThreadProc(LPVOID lpParameter)

{

    SetTimer(NULL, 0,1000, TimerProc);

 

    MSG msg;

    while(GetMessage(&msg, NULL, 0, 0) !=-1)

    {

        TranslateMessage(&msg);

        DispatchMessage(&msg);  // 记得调DispatchMessage

    }

 

    return 0;

}

 

void CgggggryDlg::OnBnClickedButton1()

{

    HANDLE hThread = CreateThread(NULL,0, (LPTHREAD_START_ROUTINE)ThreadProc, NULL,0, &uThreadID);

}

 

 

2.工作线程中究竟用哪一种定时机制好些?

答:还是看需要吧,比如需要高精度就用多媒体定时器,否则其他的方式都差不多。

 

3.线程本地存储(TLS)和_declspec(thread)

答:参考MSDN。

TLS使用很简单(看看MSDN就知道了),实际上最重要的要知道TLS有什么用,用在什么地方,不使用TLS行吗。一般情况下,我们是用不到TLS的,如果多个线程都需要一个东西,我完全可以在线程函数开始的地方申请,在结束的时候释放就行,期间线程函数调用的函数要使用这个东西时,我可以通过参数传过去。当线程函数中又调用的其他函数较少时,确实可以这么干,但是当线程函数中大量的子函数都需要这个东西时,你不可能每个函数都带一个参数吧,此时,估计所有的函数都去访问一个全局的会好得多,比如最后错误代码,此时不用TLS看你怎么实现!另外,此时我们还要跨越进程内的不同模块,显然我们非常需要提供全局访问的TLS机制。

When the threads are created, the system allocatesan array of LPVOID values for TLS, which are initialized to NULL. (也就是说线程天生就有TLS,就看咱们使用不)

资源独立于线程必须通过全局去访问才行的时候,就可以考虑是不是该用TLS机制了(实际上也就是TlsAlloc、TlsSetValue、TlsGetValue、TlsFree这几个TLSAPI函数的使用。)

 

_declspec(thread)的使用更加简单,只要在一个地方定义,那么每个线程都拥有一份,比如我定义了

__declspec( thread ) int tls_i = 1;

那么在所有的线程中,只要我能使用tls_i变量的地方,我可以用,且独立于线程的。

例如:

__declspec( thread ) int tls_i = 1;

 

DWORD ThreadProc(LPVOID lpParameter)

{

    MSG msg;

    while(GetMessage(&msg, NULL, 0, 0) !=-1)

    {

        tls_i++;

    }

 

    return 0;

}

 

void CgggggryDlg::OnBnClickedButton1()

{

    HANDLE hThread = CreateThread(NULL,0, (LPTHREAD_START_ROUTINE)ThreadProc, NULL,0, &uThreadID);

}

 

void CgggggryDlg::OnBnClickedButton2()

{

    tls_i--;

}

ThreadProc和OnBnClickedButton2中使用的tls_i不是同一个。不过使用__declspec( thread )后,会有很多的限制条件。另外,不知道是否容易跨模块,如果不能跨,和TLS比起来,威力就大减了。

但是,好像想让TLS跨模块也不容易呢,难道是把全局的索引从DLL中输出?

答:不过,最后错误代码的实现看起来并不需要将全局的索引暴露给每个模块,只需要向外暴露GetLastError和SetLastError就行,在这两个函数中才会直接使用全局索引,这主要得益于每个slot可以放四个字节的内容,刚好可以用来放最后错误代码,不需要另外申请资源。否则这个索引可能就需要暴露每个模块(线程函数)才行。

__declspec( thread )的变量能输出吗?

 

总结:这一章介绍的DLL知识,线程本地存储等在MSDN和<<Windows核心编程>>上都有更详细的说明,这儿就不用详看了。只需要记住,如果一个程序加载一个DLL若干次,DLL只有一份,后面的加载只是增加DLL的引用计数而已。而多个进程的同一个DLL之间不会互相影响,不要担心。

 

 

第三篇 真实世界中的多线程应用程序

第15章 规划一个应用程序

 

第16章 ISAPI

the Internet Server API (ISAPI) functionality

 

第17章 OLE,ActiveX,COM

 

总结,这几章中的东西不是概念性的就是平常很少用的东西,价值不大,可以不太关注。

 

 

 

 

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值