多线程程序设计的相关问题

Posted on 2008-10-20 10:09 RichardHe 阅读(965) 评论(0)  编辑 收藏 引用 所属分类: [再转]
一、    什么是进程?什么是线程?
   进程是一大堆系统对象拥有权的集合。如进程拥有内存上下 文,文件句柄,可以派生出很多线程,也可以拥有很多DLL模块。在windows系统中,进程并不完成实质的工作,只是提供一个相对独立的运行环境,线程 才是完成实际工作的载体。线程从属于进程,共享进程所拥有的系统对象。线程是操作系统调度的单位。实质上,线程就是一段可执行代码。
采用多进程的优点和缺点:
优点:运行环境相对独立,某一进程的崩溃一般不会影响到其它进程的执行。
缺点:
耗时耗资源:启动一个进程需要申请大量的系统资源,其中包括虚拟内存、文件句柄以及加载各种必要的动态链接库;线程则不需要以上动作,因为它共享进程中的所有资源。
“系统准备一个进程环境可能需要好几M的空间”
通 信复杂:进程的地址空间独立,进程A的地址X,在进程B中可能是无意义的,这样,当进程间需要共享数据时,就需要特殊的机制来完成这些工作。线程则在同一 地址空间,数据共享方便快捷。“线程是一个物美价廉的选择,在一个Windows上拥有500个线程是一件很轻易的事情,但是500个进程将是难以想象的 ”。

二、    为什么需要多线程(解释何时考虑使用线程)
从用户的角度考虑,就是为了得到更好的系统服务;从程序自身的角度考虑,就是使目标任务能够尽可能快的完成,更有效的利用系统资源。综合考虑,一般以下场合需要使用多线程:
1、    程序包含复杂的计算任务时
主要是利用多线程获取更多的CPU时间(资源)。
2、    处理速度较慢的外围设备
比如:打印时。再比如网络程序,涉及数据包的收发,时间因素不定。使用独立的线程处理这些任务,可使程序无需专门等待结果。
3、    程序设计自身的需要
WINDOWS系统是基于消息循环的抢占式多任务系统,为使消息循环系统不至于阻塞,程序需要多个线程的来共同完成某些任务。
三、    使用多线程可能出现的问题(列举问题)
事 实上,单纯的使用线程不会产生任何问题,其启动、运行和结束都是非常简单的事情。在Win32环境下,启动:CreateThread,运行就是函数执行 的过程,中止就是函数返回的过程或者调用ExitThread。但是由于下列原因可能会使在使用线程的过程中带来一系列问题:
1、    版本问题
多 任务的概念是随着实际需求的提出而产生,最初的程序设计者并没有考虑到代码需要在多线程环境下运行,在多线程环境下使用这些代码无疑将产生访问冲突。最典 型的例子就是C runtime library。最早的C runtime library产生于20世纪70年代,当时连多任务都是一个新奇的概念,更别说什么多线程了,该版本的库中使用了大量全局变量和静态变量(产生竞争条件 的根源,对局部变量无此要求,因为局部变量都使用栈,每个线程都有自己的栈空间,另外在启动线程时,给线程函数的参数应该是尽量使用值,而非指针或引用, 这样可以避免因此带来的冲突问题),如在该库中统一使用一个errno变量来表明程序的错误码,如果在多线程中使用该库,并且都需要设置错误码时,此时即 产生了一个冲突。
VC为防止以上问题,提供了另外一个线程安全的C runtime library,因此在写多线程程序时,需要注意所连接库的版本是否正确(该过程一般由应用程序向导完成,因此平时编程并无此问题)。与此有关的还有一些 其它版本:单线程版、多线程版调试版和多线程发行版。
2、    线程间共享资源时形成竞争条件(race condition)
一般而 言,线程并不是单独行动,通常是多个线程分工协作,完成一个大任务中的不同小任务,此时,这些线程之间就需要共同操作一些资源,比较典型的例子是多个线程 进行文件操作或屏幕打印的情况:线程A在写文件进行了一半时,发生了context switch,另外一个线程B继续进行写文件操作,此时文件的内容将会凌乱不堪。甚至造成异常错误。典型的例子是,三个线程,线程A在堆中申请了一块内存 并填入了一个值,线程B读取了该值后将该内存释放,如果线程C还要对该内存操作时,将导致异常。
3、    线程间的通信问题
线程协作完 成某一任务时,有时还需要通信以控制任务的执行步骤,典型的例子就是读写者线程:写线程在对某内存区域写完数据后,需要通知读线程来取,读完之后又需要通 知写线程可以继续往里写入数据。更为广泛的例子是:某线程需要等待某一事件发生,以决定是否继续工作。此时,如果没有正确控制线程的执行过程,将导致不可 预料的错误发生。
4、    由于不规范的使用线程导致系统效率下降
进程中包含了一个以上的线程,这些线程可能会动态的申请某些资源,如 某些数据库线程可能会动态加载数据库方面的动态链接库,但是在该线程结束时,并没有及时释放该动态链接库即被其他线程强行终止,于是该进程中的该动态链接 库引用计数不为0,从而导致该动态链接库在该进程中存有一个副本。当这种情况频繁时,将对系统效率产生很大的影响。
四、    线程的类型(解释UI线程和WORKER线程的区别和联系)
严格说来,线程并没有什么本质区别,但是Win32编程文档中却反复强调UI线程和Worker线程的区别。并给出了它们的定义:
UI线程就是:拥有消息队列和窗口的线程,并且它的主要职责是处理窗口消息。Worker线程则没有消息队列,但是当Worker线程产生一个用户界面(消息框和模式对话框除外)时,则该线程则摇身一变,成为UI线程。
问题:
1、    线程的消息队列和窗口的消息队列
在Win32中,每个线程都有它自己专属的消息队列,而窗口并不总是有消息队列,因为一个UI线程可以创建很多个窗口。
2、    UI线程到底跟Worker线程存在什么差别?
职 责不一样:UI线程负责处理与用户界面有关的消息,一般而言,用户界面消息来自用户输入(如鼠标键盘消息)、系统消息(如WM_PAINT)以及程序产生 的用户自定义消息。因此,在该线程下一般不能存在等待(wait…)函数,这样该线程就会挂起,从而影响消息队列的处理。Worker线程不用处理用户界 面消息,而是完成一般性的计算任务,该线程等待计算过程中必要的资源时,不会影响到界面的刷新动作。
操作系统的管理不一样:对UI线程来说,产生一个UI线程实际上产生了两个线程,一个是其自身,另一个是操作系统为响应其GDI调用而产生的影子线程。
3、    Worker线程变成UI线程有什么不好?
Worker线程一般用于计算,此时如果它转换为UI线程的话,将无暇顾及用户界面的消息响应。
4、    Worker线程可否拥有自己的消息队列?
Worker线程同样可以拥有自己的消息队列,该队列一般通过PeekMessage()调用建立,通过GetMessage调用来解析。(具体实现看源码)
5、    用以下规则来管理win32中线程、消息和窗口的互动
所有传送给某一窗口的消息,将由产生该窗口的线程负责处理。
五、    线程的启动和中止(解释启动线程的不同方式及其它们的区别和实用场合)
随C Runtime Library库的更新和编程环境的不同,线程的启动方式也有所不同,以下介绍几种典型的线程启动方式。
1、_beginthread和_endthread
该 函数是C Runtime Library中的函数,它负责初始化函数库;其原型如下unsigned long _beginthread( void( __cdecl *start_address )( void * ), unsigned stack_size, void *arglist );“该函数被认为是头脑简单的函数”,使用该函数导致无法有效的控制被创建线程,如不能在启动时将该线程挂起,无法为该线程设置优先权等。另外,该函数 为隐藏Win32的实现细节,启动线程的第一件事情即将自己的Handle关闭,因此也就无法利用这个Handle来等待该线程结束等操作。该函数是早期 的C Runtime Library的产物,不提倡使用,后期的改良版本为_beginthreadex。
通过_beginthread启动的线程在应当通过调用_endthread结束,以保证清除与线程相关的资源。
2、_beginthreadex和_endthreadex
该 函数是C Runtime Library中的一个函数,用标准C实现,相比_beginthread,_beginthreadex对线程控制更为有力(比前者多三个参数),是 _beginthread的加强版。其原型为unsigned long _beginthreadex( void *security, unsigned stack_size, unsigned ( __stdcall *start_address )( void * ), void *arglist, unsigned initflag, unsigned *thrdaddr );该函数返回新线程的句柄,通过该句柄可实现对线程的控制。虽然,该函数是用标准C写的(即可不加修改就可以移植到其他系统执行),但是由于它与 Windows系统有着紧密的联系(需要手动关闭该线程产生的Handle),因此实现时,往往需要包含windows.h。
通过_beginthreadex启动的线程通过调用_endthreadex做相关清理。
3、CreateThread和ExitThread
CreateThread 是Win32 API函数集中的一个函数,其原型为HANDLE CreateThread(LPSECURITY_ATTRIBUTES lpThreadAttributes,DWORD dwStackSize,LPTHREAD_START_ROUTINE lpStartAddress,LPVOID lpParameter,DWORD wCreationFlags,LPDWORD lpThreadId);该函数使用Win32编程环境中的类型约定,只适用于Windows系统。参数形式与_beginthreadex一致,对线程 控制能力也与之一致,只是该函数与C Runtime Library没有任何关系,它不负责初始化该库,因此在多线程环境中,如果使用该函数启动线程,则不应使用C Runtime Library中的多线程版本的函数。取而代之的应该是功能相对应的 Win32 API函数;另外,应当自己手工提供线程同步的代码。
通过CreateThread创建的线程则通过ExitThread做清理工作。
4、AfxBeginThread和AfxEndThread
AfxBeginThread 是MFC提供的线程启动方式,它是个重载函数,有两种调用形式:Worker线程版和UI线程版。MFC对Win32线程做了小心的很好的封装 (CWinThread),虽然其总是调用了_beginthreadex来启动一个线程,但是其额外做的工作使得在MFC环境下,操作线程变得简单明 了,并且不需要太多的关注细节问题。MFC在线程的封装方面主要做了下列事情:
1、    自动清除CWinThread对象
2、    关闭线程handle,线程对象自动释放
3、    存储了线程相关的重要参数,即线程handle和线程ID
4、    辅之以其它MFC同步对象,方便的实现线程同步
5、    使用了严格的断言调试语句,使线程调试变得相对简单

“(C Runtime Library是用标准C开发的实用函数集)如果多线程程序中使用了标准C库函数,并用CreateThread()和ExitThread(),则会导 致内存泄漏。解决这个问题的方法是用C运行库(run-time library)函数来启动和终止线程,而不用WIN32 API定义的CreateThread()和ExitThread()。在C运行库函数中,它们的替代函数分别是_beginthreadex()和 _exitthreadex(),需要的头文件是_process.h。在VC6.0下,还需在 Project->Settings->C/C++->Code Generation中选择Multithreaded Runtime Library。当然,也可以通过避免使用C标准库函数的方法来解决上述问题,WIN32提供了一些C标准库函数的替代函数,例如,可用 wsprintf()和lstrlen()来代替sprintf()和strlen()。这样,使用CreateThread()和 ExitThread()不会出现问题。”
六、    线程的同步问题(介绍Windows的同步机制)
1、    怎样等待一个线程结束(忙等(busy loop)和高效的等(WaitForSingleObject))
1)    忙等(busy loop)
hThrd = CreateThread(NULL,0,ThreadFunc,(LPVOID)1,0,&threadId );
for (;;)
{
GetExitCodeThread(hThrd, &exitCode);
if ( exitCode != STILL_ACTIVE )
break;
}
CloseHandle(hThrd);
缺点:耗费CPU资源,且如果在UI线程中这样等待将导致窗口无法刷新。不推荐使用。
2)    高效的等待
(1)WaitForSingleObject;
关于WaitForSingleObject的参数,前者为等待的对象,后者为等待的时间,对某些执行时间较长的线程,可以设置一个合适的值,等待完这个时间后,更新界面,然后继续等待,或者强行终止线程。
将以上的等待部分的代码改为:
WaitForSingleObject(hThrd,INFINITE);
该函数相当于Sleep函数,当需要等待的对象(句柄)没有被触发时,等待的线程将被自动挂起。该方法解决了耗费CPU时间的问题,但是在UI线程中,仍不能使用该方法来等待某一线程结束。
解决方法之一:创建一个Worker管理者线程,在该线程中等待,工作者线程完成,然后由管理者线程发消息通知UI线程更新窗口。
(2)WaitForMultipleObject
该函数允许在同一时间等待多个对象,函数的原型如下:
DWORD WaitForMultipleObject(DWORD nCount,CONST HANDE *lpHandles,BOOL bWaitAll,dwMilliseconds);
第一个参数表示句柄数组的大小;等待的对象不能超过64
第二个参数为句柄数组;
第三个参数表明是否等待所有对象激发。True表示是。
第四个参数为等待时间。
关于WaitForMultipleObject的返回值:
当bWaitAll为True时,返回值为WAIT_OBJECT_0;
当bWaitAll为false时,返回值减去WAIT_OBJECT_0,就是激发对象所在的下标。
应用:
A)    解决多个工人n完成多个任务m(n<m)的问题(bWaitAll设置为false)
解决的思路如下:先从m个任务中取出n个任务,对应地用n个工人去完成,然后利用该函数等待其中任意一个工人结束任务,一旦结束则让其做另外一个任务
B)    解决等待多个资源的问题(bWaitAll设置为true)
哲学家就餐问题:5个哲学家在圆桌旁,每个哲学家左手边放着1只筷子,哲学家做两件事情,吃饭和思考,吃饭时同时需要其左右的两只筷子。
解决思路:将哲学家模拟为线程,筷子为资源,只有哲学家线程同时获得两个资源时,方可进一步动作(吃饭)。即:
WaitForMultipleObjects(2, myChopsticks, TRUE, INFINITE);
MyChopsticks是一个大小为5的核心对象数组。
        (3)MsgWaitForMultipleObjects
        原型:
DWORD MsgWaitForMultipleObjects( DWORD nCount,CONST HANDLE pHandles,BOOL fWaitAll,DWORD dwMilliseconds,DWORD dwWakeMask);
    前 几个参数含义同WaitForMultipleObject,最后一个是消息屏蔽标识,指示接收消息的类型。此外返回值也有额外的意义:当消息到达时,该 函数返回WAIT_OBJECT_0+nCount。以下是常见的使用MsgWaitForMultipleObjects的架构:
  while (!quit)
    {   // Wait for next message or object being signaled
        DWORD   dwWake;
        dwWake = MsgWaitForMultipleObjects(
                                gNumPrinting,
                                gPrintJobs,
                                FALSE,
                                INFINITE,
                                QS_ALLEVENTS);

        if (dwWake >= WAIT_OBJECT_0 && dwWake < WAIT_OBJECT_0 + gNumPrinting)
        {  
            //对象被触发
        } // end if
        else if (dwWake == WAIT_OBJECT_0 + gNumPrinting)
        {
            //有消息到达
            while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
            {   // Get Next message in queue
                 if (msg.message == WM_QUIT)
                 {
                     quit = TRUE;
                     exitCode = msg.wParam;
                     break;
                 } // end if
                 TranslateMessage(&msg);
                 DispatchMessage(&msg);
           } // end while
        }
    } // end while
2、    怎样有效的控制一个线程
在任何情况下,切记线程的核心属性为:线程的句柄,线程的ID号。因此控制一个线程也需从这两方面着手。
1)    使用能返回线程Handle的启动函数来启动线程(除_beginthread外)
2)    尽量不要使一个工作量较大的线程成为“闷葫芦”,从而使该线程能够接收外界通知消息;如下列代码:


MSG msg;
    while(1)
    {
        PeekMessage(&msg,NULL,0,0,PM_REMOVE);
        if(msg.message==WM_MY)
            break;
        Sleep(100);
    }
注:GetMessage也是用来得到消息队列中一条消息的函数,它们的区别在于GetMessage是同步的,即如果消息队列中没有消息的话,该线程将自动挂起。使用GetMessage可以使Worker线程成为一个一步一动的线程!
    MSG msg;
    while(GetMessage(&msg,NULL,0,0))
    {
        if(msg.message==WM_MY)
        {
            //Do something here
}
    }
以上的过程也可以通过事件对象予以实现。
悬而未决的问题:怎么控制一个正在等待其他事件的线程。如:一个TCP监听线程,在某一Socket上listen,此时该线程处于挂起状态!但是现在主线程又需要关闭该线程,应该怎么操作!

3、    怎样互斥访问一个资源(CMutex和Critical Section)
何时需要一个互斥对象?
常 见的情形:多个线程需要不定时的操作同一链表(锁链表的头指针);多个线程需要不定时的进行写文件或是进行屏幕输出(锁文件句柄或屏幕句柄);多个线程需 要不定时对某个计数器进行操作(锁这个变量);在多线程环境吓,凡是涉及到对全局变量、静态变量、堆中的内存进行访问时,都应该考虑,是否可能出现一个 race condition(竞争条件)。
1)    互斥器
Win32提供了对互斥资源访问的一整套机制,其中之一就是互斥器,MFC将这些API函数加以封装,形成了CMutex互斥类,使用这两种方法都能够实现对资源的互斥访问。
Win32中的API:
CreateMutex:
原型:
HANDLE CreateMutex(LPSECURITY_ATTRIBUTES lpMutexAttributes,   BOOL bInitialOwner,  LPCTSTR lpName );
第一个参数为安全属性;
第二个参数用来指示互斥器的拥有者是否为当前线程;
第三个参数为互斥器的名称;
当不再需要互斥器时,应当调用CloseHandle关闭。
约 定:互斥器产生之后,由某一线程完成锁定工作(即调用Wait…函数),此时系统将该mutex的拥有权交于该线程,然后短暂地将该对象设置为激发态,于 是Wait…函数返回,做完相应的工作之后(如:修改链表指针、修改计数器、写文件等),调用ReleaseMutex释放拥有权。周而复始。
MFC中的互斥器CMutex对象:
A、    利用其构造函数产生一个互斥器对象
HANDLE CreateMutex( LPSECURITY_ATTRIBUTES lpMutexAttributes,BOOL bInitialOwner, LPCTSTR lpName);
B、    配合CSingleLock或者CmutipleLock产生一个临时对象,对产生的互斥器进行加锁和释放的动作;
2)    临界区
另一个提供互斥访问的机制是Critical Section,该机制较前一种方法廉价,因为它不属于不是系统的核心对象;临界区可以反复进入,这一点与Mutex有所区别,这需要我们在使用临界区时,保证进入的次数要等于离开的次数。
相关函数为InitializeCriticalSection、DeleteCriticalSection、EnterCriticalSection、LeaveCriticalSection。
4、    怎样等待多个不同(或者相同)资源(WaitForMultiObject)
等待多个不同资源在多线程程序设计中时常遇到,如:等待某一线程结束和某一个资源被释放,等待缓冲区和设备准备好两个资源;这种现实情况,可以分别为不同的资源设置系统对象,然后利用WaitForMultiObject进行等待。
5、    怎样等待多个资源中的一个(使用CSemaphore)
现实中还可能出现如下情形:客人租相机的问题:有若干客人需要,租相机,总相机数为n,相机租完后,客人必须等待,只要有一个相机,则某客人就可以等到租借。还有许多问题可以用这种Producer/consumer模型加以概括。
这种情形即是等待多个资源中的一个的情况,在Win32程序设计中则经常使用信号量(Semaphore)来解决此问题。
Win32系统中,信号量具有以下特性:
一 个信号量可以被锁定N次,N一般代表可用资源的个数,上例中即可代表相机的个数,信号量初始化后,在Win32环境下调用一次Wait…操作即表示对其的 一次锁定,信号量的值相应加1,操作完后,调用ReleaseSemaphore操作,即代表资源释放(上述例子中就是归还相机)。MFC对Win32信 号量的相关API函数进行了封装(CSemaphore),配合CMultiLock 或者 CSingleLock即可实现锁定和资源释放的动作。
七、    线程间的通信
线程间的通信有许多方法可以实现,视场合不同也有不同的应用,大致可以分为两类:进程内的线程通信和进程间的通信。关于进程内线程的通信,前面所述的各种同步互斥等待机制也可归属线程间通信的范畴,
1、    使用线程消息实现线程通信
2、    使用事件对象实现线程通信
Win32还提供了一种比较灵活的核心对象,该对象完全受控于程序(只是清除的时候由系统回收),这就是Event(事件)对象。事件对象一般用于线程间的通知。下面先看事件对象的一些属性:
创建一个事件对象可以调用Win32 API函数完成,也可以使用MFC封装的事件对象。其API原型为:
HANDLE CreateEvent(LPSECURITY_ATTRIBUTES lpEventAttributes,BOOL bManualReset,BOOL bInitialState, LPCTSTR lpName );
第二个参数指示事件对象是否为手动修改状态(手动修改需要显式调用ResEvent函数);第三个参数设置事件对象的初态,true为激发态,false为非激发态。第四个参数为事件的名字。
事件对象自从创建后即在程序的控制下处于激发态和非激发态之间翻转。
八、    线程代码的调试
九、    什么是线程安全的代码
十、    多线程程序设计的几个原则

只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理


posts - 94, comments - 138, trackbacks - 0, articles - 94

Copyright © RichardHe