程序员人生 网站导航

Windows下多线程编程(二)

栏目:php教程时间:2016-07-26 13:34:25

线程的分类

1.     有消息循环线程

  •  MFC中有用户界面线程,从CWinThread派生出1个新的类作为UI线程类CUIThread,然后调用AfxBeginthread(RUNTIME_CLASS(CUIThread));启动线程。UI线程可以直接创建模态对话框,而不用担心消息循环的问题,由于UI线程默许自带消息循环。
  •  MFC非用户界面线程,不能创建模态对话框,但是可以创建非模态对话框或普通窗口,但是必须自己写消息循环。

  

复制代码
MSG msg; while(GetMessage(&msg, NULL, 0, 0)) { TranslateMessage(&msg); DispatchMessage(&msg); }
复制代码

 

2.     无消息循环线程

  • MFC中的工作者线程
  • 其他没有加消息循环的普通线程。

 

线程间的通讯

1.   同享内存变量

l  由于线程是同享进程内存的,所以通过全局/静态变量来进行通讯效力最最高的。参数需要斟酌是不是加volitile。

l  通过传递的参数,如援用和指针。参数需要斟酌是不是加volitile。

2.   消息通知

  • 如果是子线程向主线程通讯,由于主线程有消息循环,所以子线程可以通过发送消息来向主线程通讯。通过消息通讯能够避免使用全局变量带来的耦合性。

SendMessage必须等待消息函数处理完成才返回,PostMessage则直接将消息放入消息队列立即返回。所以SendMessage的消息参数可以是临时变量,而PostMessage的消息参数必须保证足够的生存周期。

  • 如果子线程有自定义的消息循环,也能够通过PostThreadMessage来指定线程通讯。

       

复制代码
while(true) { if(GetMessage(&msg,0,0,0)) //get msgfrom message queue { switch(msg.message) { case MY_MSG: // Todo: break; } } };
复制代码

 

3.   其他方式

  • 所有跨进程的通讯方式,固然可以用于跨线程了。

 

线程之间的状态

1.   异步

即多个线程彼此独立,不受外部线程的影响。线程本身就是实现异步的1种方式。

2.   同步

即多个线程彼此依赖,线程A的计算结果是线程B的计算的条件,也就是说在开始线程B的计算之前必须等待线程A的计算完。

3.   互斥

即多个线程在操作同1个资源时,1个线程必须等另外一个线程结束了才能继续操作。互斥与同步不同的地方是,互斥没有前后关系。同1个资源,可以指全局变量,也能够指1个文件对象或是其他的内核对象。由于内核对象是跨进程的,所以更是跨线程的。

            等待函数

1.    概念

WaitForSingleObject函数是等待内核对象从无信号状态到有信号状态或是超时即返回。也即无信号状态时等待,有信号或超时立即返回。

WaitForMulitpleObjects函数是等待多个内核对象从无信号状态到有信号状态或是超时即返回(可以指明是所有对象或是任1对象)。

Windows具有几种内核对象可以处于已通知状态和未通知状态:进程、线程、作业、文件、控制台输入/输出/毛病流、事件、等待定时器、信号量、互斥对象。

2.    等待函数与内核对象之间的关系

对象

无信号状态

有信号状态

成功等待副作用

进程

进程活动时

进程终止时

线程

线程活动时

线程终止时

文件

I/O要求正在处理时

I/O要求结束时

控制台输入

不存在任何输入

存在输入时

文件修改通知

没有任何文件修改通知

文件系统发现修改时

重置通知

自动重置事件

ResetEvent, PulseEvent或等待成功

当调用SetEvent或PulseEvnet时

重置事件

人工重置事件

ResetEvent,或PulseEvent

当调用SetEvent或PulseEvnet时

自动重置定时器

CancelWaitableTimer或等待成功

当时间到时(SetWaitableTimer)

重置定时器

人工重置定时器

CancelWaitableTimer

当时间到时(SetWaitableTimer)

信号量

等待成功

当资源数量>0时(ReleaseSemaphore)

数量减1

互斥量

等待成功

当未被线程具有时(ReleaseMutex)

获得线程所有权

l 线程和进程创建及运行时都是无信号状态,当结束运行时变成有信号状态。

l 自动重置的事件(FALSE)对象,当等待成功的时候,会被修改成无信号状态。

l 信号量对象,当调用ReleaseSemaphore(数量加1),处于有信号状态,WaitForSingleObject会被触发并且立行将信号数量减1.

 

 

        用户模式与内核模式的优缺点

1.   用户模式

优点:线程同步机制速度快

缺点:容易堕入死锁状态多个进程之间的线程同步会出现问题。(比如竞争资源、死锁)

2.   内核模式

优点:支持多个进程之间的线程同步,避免死锁

缺点:线程同步机制速度慢,线程必须从用户模式转为内核模式。这个转换需要很大的代价:来回1次需要占用x 8 6平台上的大约1 0 0 0个C P U周期。

 

 

线程间的状态处理

1.   线程的异步

由于线程本身就是异步的。

2.   线程的同步

线程的同步主要是通过事件(Event)内核对象、信号量(Semaphore)内核对象和互斥量(Mutex)内核对象。由于都是内核对象,所以不但可以跨线程操作,还可以跨进程同步。

1.      线程的同步

线程的同步主要是通过事件(Event)内核对象、信号量(Semaphore)内核对象和互斥量(Mutex)内核对象。由于都是内核对象,所以不但可以跨线程操作,还可以跨进程同步。

事件(Event)内核对象

事件分两种类型:人工重置事件和自动重置事件,前者在触发WaitForSingleObject以后需要手动调用ResetEvent将事件设置为无信号;而后者在触发WaitForSingleObject以后自动将事件设置为无信号状态。

经常使用函数:

CreateEvent,创建事件对象。

OpenEvent,打开已创建的事件对象,可以跨进程打开。

SetEvent,将事件对象设置为有信号状态。

ResetEvent,将事件对象设置为无信号状态。

PulseEvent,将事件对象设置为有信号状态,然后又设置为无信号状态,此函数不经常使用。

复制代码
HANDELg_hEvent; int Main() { g_hEvent =CreateEvent(NULL, TRUE, FALSE, NULL); _beginthreadex(NULL,0, ThreadFun1, 0); _beginthreadex(NULL,0, ThreadFun2, 0); SetEvnet(g_hEvent);// } DWORD WINAPIThreadFun1(PVOID pParam) { WaitForSingleObject(g_hEvent); // Todo... SetEvent(g_hEvnet); return 0; } DWORD WINAPIThreadFun2(PVOID pParam) { WaitForSingleObject(g_hEvent); // Todo... SetEvent(g_hEvnet); return 0; }
复制代码

 

注意:如果上面创建的是人工重置事件,则两个线程函数都将履行。如果是自动重置事件,则只能履行1个线程,且不能保证哪个线程先履行。如果要保证1个线程先履行,可以添加事件对象用来确保指定线程已履行,不能通过代码的前后顺序确保线程已履行。

2.      信号量(Semaphore)内核对象

信号量的使用规则:

当前信号量资源数大于0,则标记为有信号状态。

当前信号量资源数为0,则标记为无信号状态。

信号量资源数不能为负,且最大不能超过指定数量。

经常使用函数:

CreateSemaphore,创建信号量对象。

OpenSemaphore,打开指定信号量对象,可以跨进程。

ReleaseSemaphoer,资源计算加1。

复制代码
HANDELg_hSema[2]; int Main() { g_hSema[0] =CreateSemaphore(NULL, 1, 1, NULL); g_hSema[1] =CreateSemaphore(NULL, 0, 1, NULL); _beginthreadex(NULL,0, ThreadFun1, 0); _beginthreadex(NULL,0, ThreadFun2, 0); } DWORD WINAPIThreadFun1(PVOID pParam) { WaitForSingleObject(g_hSema[0]); // Todo... ReleaseSemaphoer(g_hSema[1]); return 0; } DWORD WINAPIThreadFun2(PVOID pParam) { WaitForSingleObject(g_hSema[1]); // Todo... ReleaseSemaphoer(g_hSema[0]); return 0; }
复制代码

 

这样就可以够保证ThreadFun1履行完了,再履行ThreadFun2,然后再履行ThreadFun1,并且保证每一个线程函数只能被调用1次.

3.      互斥量(Mutex)内核对象

互斥量内核对象确保线程具有单个资源的互斥访问权。在行动特性上,互斥量与临界区的1样。只不过,互斥量是内核对象,使用时需要从用户模式切换到内核模式,比较耗时。但正由于是内核对象,所以互斥量能够跨进程,并且能够设置超时时间,这是它比临界区灵活的地方。

经常使用函数:

CreateMutex,创建互斥量对象。

OpenMutex,打开指定互斥量对象,可以跨进程。

ReleaseMutex,释放互斥量,对象被标记为有信号状态,触发WaitForSingleObject。

互斥量和临界区1样,具有1个线程具有权的概念,即当前互斥量和当前临界区的释放只能由当前线程释放,其他线程释放无效。由于互斥量是内核对象,如果线程已终止,但是其所属的互斥量仍然没有释放,内核管理器会自动释放。临界区没有这个功能,由于临界区不是内核对象,所以临界区如果没有正确释放会致使死锁。

HANDLECreateMutex(  LPSECURITY_ATTRIBUTESlpMutexAttributes,

  BOOL bInitialOwner,  LPCTSTR lpName);

bInitialOwner标记是不是由创建线程具有线程所有权,TRUE表示创建者具有,FALSE表示创建者不具有,则是第1个调用WaitForSingleObject的线程将取得线程所有权。

复制代码
HANDELg_hMutex; int Main() { g_hMutex =CreateMutex(NULL,FALSE); _beginthreadex(NULL,0, ThreadFun1, 0); _beginthreadex(NULL,0, ThreadFun2, 0); } DWORD WINAPIThreadFun1(PVOID pParam) { WaitForSingleObject(g_hMutex); // Todo... ReleaseMutex(g_hMutex); return 0; } DWORD WINAPIThreadFun2(PVOID pParam) { WaitForSingleObject(g_hMutex); // Todo... ReleaseMutex(g_hMutex); return 0; }
复制代码

 

两个函数谁先调用,谁即获得线程所有权。如果想指定线程先运行,需要判断指定线程已履行以后再创建新线程,不能依托线程的代码创建前后顺序。

3.   线程的互斥

像互斥量对象一样可以到达互斥的效果,只是互斥量功能更丰富,并且如果是简单的资源互斥,使用临界区的效力更优。

临界区(Critical Section)是1段供线程独占式访问的代码,也就是说若有1线程正在访问该代码段,其它线程想要访问,只能等待当前线程离开该代码段方可进入,这样保证了线程安全。他工作于用户级(相对内核级),在Window系统中CRITICAL_SECTION实现临界区相干机制。

经常使用函数:

voidInitializeCriticalSection(LPCRITICAL_SECTION lpCriticalSection)  // 初始化临界区

voidEnterCriticalSection(LPCRITICAL_SECTION lpCriticalSection)       // 进入临界区

voidLeaveCriticalSection(LPCRITICAL_SECTION lpCriticalSection)       // 离开临界区

voidDeleteCriticalSection(LPCRITICAL_SECTION lpCriticalSection)      // 释放临界区资源

由于临界区具有线程所有权这个概念,即进入临界区的线程才有权释放临界区。由于必须当前线程进入和释放,更多的时候,临界区是在1个函数里使用,为了确保不会由于中间退出函数致使没有释放,我们可以用以下方式来确保释放。

复制代码
class Mutex { public: Mutex() {InitializeCriticalSection(section); } ~Mutex() { DeleteCriticalSection(section);} void Enter() {EnterCriticalSection(section); } void Leave() {LeaveCriticalSection(section); } struct Lock; protected: Mutex(const Mutex&); Mutex& operator=(const Mutex&); CRITICAL_SECTION section; }; structMutex::Lock { Mutex& s; Lock(Mutex& s) : s(s) { s.Enter(); } ~Lock() { s.Leave(); } }; DWORD WINAPIThreadFun(PVOID pParam) { Mutex::Locklock(mutex); // Todo... return 0; }
复制代码

 

注意

1.      注意所有内核对象在结束时都需要调用closeHandle()。

2.      跨线程调用MFC对象函数都是不安全的。由于MFC对象的1些函数都与TLS有关联,  所以有些调用会出错。如UpdateData(),最好通过句柄发消息来完成相应的功能。

------分隔线----------------------------
------分隔线----------------------------

最新技术推荐