Multithreading Applications in Win32 Faqs

来源:互联网 发布:淘宝部落冲突 编辑:程序博客网 时间:2024/05/07 20:48
到个人博客阅读 »

在《Multithreading Applications in Win32》书中旁栏附有关于主题的Faqs,这些Faqs涉及到Multithreading Programming应该引起注意的大部分,于是写下来作为笔记,希望在进行Win32 Multithreading Programming中能起到一些Effective的作用。(Effective是一类书系的名称,这类书是(C++)程序员必读书,最近在读《Effective TCP/IP Programming》)

FAQ 01:合作型(cooperative)多任务与抢先式(preemptive)多任务有何不同?

合作型多任务环境下程序对CPU具有自主占用权,OS不加干预,这样如果一个程序一直占着CPU不放,其他程序就只能一直等停了,没有执行的机会(后果不可想象!);抢先式任务系统中则由OS合理分配CPU。

FAQ 02:我可以在Win32s中使用多个线程吗?

Win32s (subset)包括Windows 3.1和3.11,不支持抢先式多任务和多线程。

FAQ 03:线程和进程有何不同?

简单说来,在Win32的世界(不同于Unix系OS),进程是一大堆对象集的拥有者,包括内存(memory context),file handles,Threads,DLL Modules(被映射到进程空间);线程是CPU调度和分配的基本单元。

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

线程自身拥有少量的资源,包括自己的堆栈和局部变量,线程间共享进程的所有资源。

FAQ 05:Context Switch是怎么发生的?

在一个抢先式多任务系统中,OS依赖硬件的协助和许多登记操作来确保每个线程都有机会执行。当硬件计时器认为某个线程已经执行够久了,就会发出一个中断,CPU取得目前这个线程的状态,也就是把所有寄存器内容拷贝到堆栈之中,再把它从堆栈拷贝到一个CONTEXT结构中,线程状态信息被保存起来。在切换不同的线程时,OS先切换该线程所隶属的进程的内存,然后恢复该线程放在CONTEXT结构中的寄存器值。

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

由于核心对象可以有一个以上的拥有者,甚至可以跨进程,OS通过引用计数来保持对每一位拥有者的追踪。每次调用返回核心对象handle的API,该核心对象的引用计数便累加1,当调用CloseHandle()时,引用计数便递减1,一旦引用计数降至0,这一核心对象即自动被销毁。因此,有没有调用CloseHandle()对决定核心对象能否正常释放。虽然OS也会把一个进程在结束前没有CloseHandle的核心对象的引用计数降1,但OS不知道对象实际代表的什么意义,所以它不可能知道解构顺序是否重要,进而可能带来负面影响。

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

线程的handle是指向“线程核心对象”,而不是指向线程本身。当传入一个handle调用CloseHandle()时只不过表示你不希望和此核心对象不再有任何瓜葛。CloseHandle()唯一做的事情就是把引用计数减1。如果该值变成0,对象会自动被OS销毁。另,“线程核心对象”引用到的那个线程也会另核心对象开启。因此,线程对象的默认引用计数是2。当调用CloseHandle()时,引用计数降1,当线程结束时,引用计数再降1。只有当这两件事情都发生了的时候,这个对象才会被真正清除。

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

程序结束,意味着主线程结束了,这使得其他所有线程都被迫结束而没有机会做清理工作。因此应该在主线程中先等待所有的线程结束。

FAQ 09:什么是MTVERIFY?

(略)

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

通过WaitForSingleObject(),将等待时间dwMilliseconds参数设为0,判断返回值:
WAIT_OBJECT_0:激发状态    WAIT_TIMEOUT:未激发状态

FAQ 11:什么是一个被激发的对象?

在Win32中,当一个对象变成激发状态,也就是它的内部状态有所改变,这对程序没有冲击,除非有一个线程正在等待此对象的激发。

FAQ 12:“激发”对于不同的核心对象有什么不同的意义?

每种核心对象“激发”状态的意义都不同:

对象被激发未激发说明Thread当线程结束时;当线程还在进行时;象由CreateThread()或CreateRemoteThread()产生。Process当进程结束时;当进程还在进行时;对象由CreateProcess()或OpenProcess()产生。Change Notification当一个特定的磁盘子目录中发生一件特别的变化时; 对象由FindFirstChangeNotification()产生。Console Input当console窗口的输入缓冲区中有数据可用时; 对象由CreateFile()或GetStdFile()产生。Event  对象状态直接受控于OS提供的3个API:SetEvent()、PulseEvent()、ResetEvent()。对象可以由CreatEvent()或OpenEvent()产生。(当用于”overlapped”操作时,也可以被OS设定状态)Mutexmutex没有被任何线程拥有;一个等待mutex的函数返回了,自动重置为未激发状态; 对象可由CreateMutex()或OpenMutex()产生。Semaphore 当semaphore的计数器大于0时;当计数器等于0时;对象可由CreateSemaphore()或OpenSemaphore产生。

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

一般主线程负责GUI程序中的主消息循环,如果使用WaitForSingleObject()或WaitForMultipleObjects()正在等待某个对象被激发,则无法回到主消息循环中去。因此,在主线程中等待一个handle时,必须使用另一个API:MsgWaitForMultipleObjects(),该函数类似WaitForMultipleObjects(),但是会在“对象被激发”或“消息到达队列”时被唤醒而返回。

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

如果资源被critical sections(或其他同步对象)锁定时,可能会阻止其他(同时需要该资源的)线程的执行,并把整个程序带到一个完全停止的状态。最坏的情况可能是,当一个带GUI的主线程需要这被锁定的资源时,程序会hang在那里,而用户得不到任何错误信息。因此记住:千万不要在一个critical sections之中调用Sleep()或任何Wait…() AP函数。

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

使用critical sections有一个严重的缺点:无法获知进入critical sections中的那个线程是生是死。从另一个角度看,由于critical section不是核心对象,如果进入critical section的那个线程结束了或当掉了,而没有调用LeaveCriticalSection()的话,系统没有办法将该critical section清除。

FAQ 16:我如何避免死锁?

任何时候当一段代码需要两个或更多资源时,都存在死锁的可能。避免死锁的一个办法是:强迫将资源锁定,使它们成为“all-or-nothing”(要不统统获得,要不统统没有)。

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

由于critical sections并不是核心对象(它没有handle),而等待函数WaitForMultipleObjects()等待的只是核心对象,因此无法做到同时等待一个以上的critical sections。

FAQ 18:谁才拥有semaphore?

semaphore没有所有权的概念。Win32中的一个semaphore可以被锁住最多n次,其中n是semaphore被产生时指定的,用来代表“可以锁住一份资源”的线程个数,一个线程可以(反复调用Wait…()函数)拥有所有的锁定。当n为1时,semaphore相当于一个mutex,不同的是,当semaphore的可用计数值降为1时,任何线程调用Wait…()必然要等待;而拥有mutex的线程不论再调用多少次Wait…(),也不会被阻塞。

FAQ 19:Event object有什么用途?

events 核心对象是Win32中最具弹性的同步机制,它唯一的目的就是成为激发状态或未激发状态,这两种状态完全由程序来控制,可运用在多种类型的高级I/O操作中(如Overlapped I/O)。

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

这个event会被遗失(在context switch的时候)。

FAQ 21:什么是overlapped I/O?

简单的说,overlapped I/O是Win32的一项技术,你可以要求OS为你传递数据,并且在传送完毕时通知你。这样你就可以在I/O进行过程中仍能够继续处理事务。

FAQ 22:Overlapped I/O在Windows 95上有什么限制?

Win95所支持的overlapped I/O限于named pipes、mailslots、serial I/O、以及socket()或accept()所传回来的sockets,而不支持磁盘或光盘中的文件操作。

FAQ 23:我能够以C RunTime Library使用overlapped I/O吗?

不能藉由调用 C Runtime Library中的stdio.h函数(如fgets()、fprintf())而使用overlapped I/O。overlapped I/O的基本型式是以ReadFile()和WriteFile()完成的。

FAQ 24:Overlapped I/O总是异步地(asynchronously)执行吗?

对于提交的overlapped操作,不一定就是overlapped。如果数据已经被放进cache中,或如果OS认为它可以很快速的取得那份数据 ,那么文件操作就会在ReadFile()返回之前完成,而ReadFile()将返回TRUE。这种情况下,文件handle处于激发状态,而对文件的操作可被视为就像overlapped一样。

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

OVERLAPPED结构中的最后一个栏位,是一个event handle。这它被设定为一个event对象时,系统核心会在overlapped操作完成的时候,自动将此event对象激活。注意,使用的这个event对象必须是manual-reset而非auto-reset。否则,将可能产生race condition。原因是系统核心有可能在你有机会等待该event对象之前先激发它,而这个激发状态是不能保留的,也就是会遗失,结果你的Wait…()将永不返回。

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

支持异步过程调用机制(APC),可以传入一个callback函数(称为I/O completion routine),当一个overlapped I/O完成时,系统会调用该callback函数。这样解决了使用overlapped I/O并搭配event对象时带来的两个问题:(1)使用WaitForMutipleObjects()最多只能够等待MAXIMUM WAIT OBJECTS个对象。在Win NT 3.x和4.0这个最大值是64;(2)你必须不断根据“哪一个handle被激发”而计算如何反应,这需要一个dispatch table和WaitForMultipleObjects()的handles数组结合起来。

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

Windows不会贸然中断你的程序,然后调用你提供的callback函数。只有当程序处于“alertable”状态时,APCs才会被调用。如果线程因为这些函数:SleepEx()、WaitForMutipleObjects()、WaitForMutipleObjectsEx()、MsgWaitForMutipleObjectsEx()、SignalObjectAndWait(),而处于等待状态,而其“alertable”标记被设为TRUE,则该线程就是“alertable”状态。

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

在使用APCs时,你可以自由使用OVERLAPPED结构中的hEvent栏位(不需要用来放置event handle),最大的用途就是配置一个自定义结构,描述数据来源,或是要进行什么操作,来传给I/O completion routine。

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

I/O completion routine同样必须是全局或static成员函数。若要使用non-static成员函数,可以通过存储一个静态this指针,然后经由该指针调用non-static成员函数。

FAQ 30:在一个高效率服务器(server)上我应该怎么进行I/O?

APCs存在一个限制:只有发出“overlapped请求”的那个线程才能够提供callback函数。而对于一个scalable(通过增加CPU或RAM或disk space而能够提升系能)系统,最好任何线程都能够服务events。这时,WinNT 3.5提供了I/O completion ports(与APCs中所用的I/O completion routines没有任何关联)机制。

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

completion port是OS提供的一种机制,用来管理一堆线程如何为completed overlapped I/O requests服务。它能够保持一个CPU或多个CPUs尽可能地忙碌,当一个线程被阻塞时,它将发出通告,并提交给另一个线程(假设有idle线程的话)。

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

当你产生一个线程时,它们都应该在completion port上等待。当线程开始为各个request服务时,池子里的线程应该是这样组织如的:目前正在执行的线程+被阻塞的线程+在completion port上等待的线程。因此,应该产生比CPU个数还多的线程。合理的个数应该是2*CPU个数+2。

FAQ 33:为什么我不应该使用select?

如果程序对性能和scalable有要求,在WinNT上应该使用completion port,select模型不能够因为增多CPUs而对程序性能有所提升。

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

volatile告诉编译器不要持有变量的临时性拷贝。

FAQ 35:什么是Readers/Writers lock?

程序中的多个线程在同一时间读取数据事实上并不会有什么问题,只要当时没有其他线程企图改变数据就好。相反,如果一个线程正在写入数据,其他线程不论读或写都应该被阻止。Readers/Writers lock就是用于这两种情形的。

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

也就是锁定粒度,这是个问题,在“尽量少用locks”的准则下去考虑。

FAQ 37:我应该使用多线程版本的C run-time library吗?

如果写的是多线程程序,而且没有使用MFC,则应该总是链接多线程版本的C run-time library,并且总是以_beginthreadex()和_endthreadex取代CreateThread()和ExitThread()。

FAQ 38:我如何选择一套适当的c run-time library?

原先的 C run-time library中使用了数个全局变量和静态变量,在多线程程序中将引起冲突,而在后来的多线程版本中保证了thread-safe。在VC++中提供了两个版本的c runtime library分别供单/多线程程序使用。

如果是单线程程序或多线程程序但没有使用涉及全局/静态变量的C函数时可使用单线程版的runtime library,否则使用多线程版。(MFC程序必须使用多线程版)

FAQ 39:我如何使用_beginthreadex()和_endthreadex()?

_beginthreadex()是对CreateThread()的wrapper,另外做了些额外的记录工作。另外_beginthreadex()的参数由Win32数据类型“净化”为标准的C数据类型,目的是跨平台。然后好心没干成好事,由于_beginthreadex()实际返回的是线程handle(需要强制装换为HANDLE类型才能使用),结束线程时还是需要调用CloseHandle(),所以还是需要包含windows.h头文件。

FAQ 40:什么时候我因该使用_beginthreadex而非CreateThread()?

要使用CreateThread(),除非不调用任何runtime library函数,否则都应该使用_beginthreadex。(不调用runtime library函数几乎是不可能的,编译器也常会代为调用一些位于runtime library中的辅助函数)

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

(略)

FAQ 42:为什么我不应该使用_beginthread()?

存在几个基本的问题:(1)没有要求获得和CreateThread()完全一样的参数,因此有些事情它办不到,如将线程产生于挂起状态;(2)被_beginthread()产生出来的线程所做的第一件事就是关闭自己的handle(为了隐藏Win32的实现细节)。因此,_beginthread()传回的handle可能在当时是不可用的,也就没办法等待这个线程的结束,以及改变其参数、或取得其结束代码。

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

(略)

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

(略)

FAQ 45:我如何能够阻止一个线程杀掉它自己?

MFC中,CWinThread对象可能在AfxBeginThread()返回时就已经被删除了,这时任何设计线程handle的操作都将使程序当掉。幸运的是,CWinThread中有一个成员变量m_bAutoDelete,可以用来阻止CWinThread对象被自动删除。要使用这个变量并且避免产生race condition,必须以挂起状态产生线程。

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

(略)

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

(略)

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

利用VC++进行调试时,可以将其他线程挂起。

FAQ 49:如果一个新的线程使用了我的DLL,我如何被告知?

当一个进程载入或卸载一个DLL时,DllMain()函数会被系统调用。当一个进程开始执行时,它所用到的每一个DLL的DllMain()都会被系统调用,并获得DLL_PROCESS_ATTACH消息;如果是线程开始执行,进程所用到的每一个DLL的DllMain()也会被系统调用,并获得DLL_THREAD_ATTACH消息。

FAQ 50:为什么我在写DLL时需要小心所谓的动态链接?

当一个DLL被LoadLibrary()或LoadLibraryEx()动态载入时,DllMain()不会收到任何正在执行线程的DLL_THREAD_ATTACH通告消息,除了那个调用LoadLibrary()的线程。不过,DllMain()会收到所有那些线程的DLL_THREAD_DETACH消息。

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

可能发生race condition。

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

(略)

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

_declspec(thread)(VC++编译器支持)可用来将一个变量或结构声明为“具有线程局部性”,但在使用C++时,如果一个对象拥有构造函数或析构函数,它就不能够被声明为_declspec(thread)。另外,一个DLL如果使用了_declspec(thread),就没有办法被LoadLibrary()载入。因为线程局部节区的大小计算是在程序启动时完成的,没有办法在一个新的DLL载入时重新计算。

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

在开发一下领域的程序时,可以考虑使用多线程。

(1)offloading time-consuming task。由主线程(GUI线程)负责,让用户界面有较好的反应;(2)Scalability;(3)Fair-share resource allocation;(4)Simulations。

FAQ 55:我能够对既有程序代码进行多线程操作吗?

评估全局/静态变量的使用度高不高。

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

(略)

欢迎指正!

原创粉丝点击