线程的分类
1. 有消息循环线程
- MFC中有用户界面线程,从CWinThread派生出一个新的类作为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. 异步
即多个线程彼此独立,不受外部线程的影响。线程本身就是实现异步的一种方式。
2. 同步
即多个线程彼此依赖,线程A的计算结果是线程B的计算的前提,也就是说在开始线程B的计算之前必须等待线程A的计算完。
3. 互斥
即多个线程在操作同一个资源时,一个线程必须等另一个线程结束了才能继续操作。互斥与同步不同之处是,互斥没有先后关系。同一个资源,可以指全局变量,也可以指一个文件对象或是其他的内核对象。因为内核对象是跨进程的,所以更是跨线程的。
等待函数
1. 概念
WaitForSingleObject函数是等待内核对象从无信号状态到有信号状态或是超时即返回。也即无信号状态时等待,有信号或超时立即返回。
WaitForMulitpleObjects函数是等待多个内核对象从无信号状态到有信号状态或是超时即返回(可以指明是所有对象或是任一对象)。
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. 内核模式
优点:支持多个进程之间的线程同步,防止死锁
缺点:线程同步机制速度慢,线程必须从用户模式转为内核模式。这个转换需要很大的代价:往返一次需要占用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; }
注意:如果上面创建的是人工重置事件,则两个线程函数都将执行。如果是自动重置事件,则只能执行一个线程,且不能保证哪一个线程先执行。如果要保证一个线程先执行,可以添加事件对象用来确保指定线程已经执行,不能通过代码的先后顺序确保线程已经执行。
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,并且保证每个线程函数只能被调用一次.
3. 互斥量(Mutex)内核对象
互斥量内核对象确保线程拥有单个资源的互斥访问权。在行为特性上,互斥量与临界区的一样。只不过,互斥量是内核对象,使用时需要从用户模式切换到内核模式,比较耗时。但正因为是内核对象,所以互斥量能够跨进程,并且能够设置超时时间,这是它比临界区灵活的地方。
常用函数:
CreateMutex,创建互斥量对象。
OpenMutex,打开指定互斥量对象,可以跨进程。
ReleaseMutex,释放互斥量,对象被标记为有信号状态,触发WaitForSingleObject。
互斥量和临界区一样,拥有一个线程拥有权的概念,即当前互斥量和当前临界区的释放只能由当前线程释放,其他线程释放无效。因为互斥量是内核对象,如果线程已经终止,但是其所属的互斥量依然没有释放,内核管理器会自动释放。临界区没有这个功能,因为临界区不是内核对象,所以临界区如果没有正确释放会导致死锁。
HANDLECreateMutex( LPSECURITY_ATTRIBUTESlpMutexAttributes,
BOOL bInitialOwner, LPCTSTR lpName);
bInitialOwner标记是否由创建线程拥有线程所有权,TRUE表示创建者拥有,FALSE表示创建者不拥有,则是第一个调用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)是一段供线程独占式访问的代码,也就是说若有一线程正在访问该代码段,其它线程想要访问,只能等待当前线程离开该代码段方可进入,这样保证了线程安全。他工作于用户级(相对于内核级),在Window系统中CRITICAL_SECTION实现临界区相关机制。
常用函数:
voidInitializeCriticalSection(LPCRITICAL_SECTION lpCriticalSection) // 初始化临界区
voidEnterCriticalSection(LPCRITICAL_SECTION lpCriticalSection) // 进入临界区
voidLeaveCriticalSection(LPCRITICAL_SECTION lpCriticalSection) // 离开临界区
voidDeleteCriticalSection(LPCRITICAL_SECTION lpCriticalSection) // 释放临界区资源
因为临界区拥有线程所有权这个概念,即进入临界区的线程才有权释放临界区。因为必须当前线程进入和释放,更多的时候,临界区是在一个函数里使用,为了确保不会由于中间退出函数导致没有释放,我们可以用下列方式来确保释放。
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对象的一些函数都与TLS有关联, 所以有些调用会出错。如UpdateData(),最好通过句柄发消息来完成相应的功能。