VC进程间通信, 线程间通信,线程同步的方式
原文地址:https://blog.csdn.net/qq_41786318/article/details/79539287
进程通信是讲的比较多的,很多教科书上也有,包括下面几种:
1.管道
2.信号(系统内的一些信号,复杂,与信号量不同)
3.消息队列
4.共享内存
5.信号量(进程线程同步互斥的方法)
6.套接字
VC 线程间通信的三种方式:
实现线程间通信的方法有很多,常用的主要是通过全局变量、自定义消息和事件对象等来实现的。其中又以对全局变量的使用最为简洁。该方法将全局变量作为线程监视的对象,并通过在主线程对此变量值的改变而实现对子线程的控制。
由于这里的全局变量需要在使用它的线程之外对其值进行改变,这就需要通过volatile关键字对此变量进行说明。使用全局变量进行线程通信的方法非常简单,通过下面给出的示例代码能够对其有一个基本的认识。
// 线程通信用全局变量
volatile bool g_bDo = false;
……
//线程处理函数
UINT ThreadProc5(LPVOID pParam)
{
//根据全局变量g_bDo的取值来决定线程的运行
while (g_bDo)
{
Sleep(2000);
AfxMessageBox("线程正在运行!");
}
AfxMessageBox("线程终止");
return 0;
}
……
void CSample06View::OnGlobalStart()
{
// 通过全局变量通知线程执行
g_bDo = true;
// 启动线程
AfxBeginThread(ThreadProc5, NULL);
}
void CSample06View::OnGlobalEnd()
{
// 通过全局变量通知线程结束
g_bDo = false;
}
2.利用自定义消息(可适用于窗体)
全局变量在线程通信中的应用多用在主线程对子线程的控制上,而从子线程向主线程的信息反馈则多采用自定义消息的方式来进行。这里对自定义消息的使用同使用普通自定义消息非常相似,只不过消息的发送是在子线程函数中进行的。该方法的主体是自定义消息,应首先定义自定义消息并添加对消息的响应代码。
// 自定义消息
#define WM_USER_MSG WM_USER + 101
……
//消息响应函数在头文件中的定义:
//{{AFX_MSG(CSample06View)
//}}AFX_MSG
afx_msg void OnUserMsg(WPARAM wParam, LPARAM lParam);
DECLARE_MESSAGE_MAP()
……
//消息映射
BEGIN_MESSAGE_MAP(CSample06View, CView)
//{{AFX_MSG_MAP(CSample06View)
//}}AFX_MSG_MAP
ON_MESSAGE(WM_USER_MSG, OnUserMsg)
END_MESSAGE_MAP()
……
//消息响应函数
void CSample06View::OnUserMsg(WPARAM wParam, LPARAM lParam)
{
// 报告消息
AfxMessageBox("线程已退出!");
}
此后,在子线程函数需要向主线程发送消息的地方调用PostMessage()或SendMessage()消息传递函数将消息发送给主线程即可。由于消息发送函数是在线程中被调用,因此需要指出接受窗口句柄,可通过线程参数将其传递进线程函数。
UINT ThreadProc6(LPVOID pParam)
{
// 延迟一秒
Sleep(1000);
// 向主线程发送自定义消息
::PostMessage((HWND)pParam, WM_USER_MSG, 0, 0);
return 0;
}
……
void CSample06View::OnUseMessage()
{
// 获取窗口句柄
HWND hWnd = GetSafeHwnd();
// 启动线程
AfxBeginThread(ThreadProc6, hWnd);
}
3.使用事件内核对象(相当好用)
利用事件(Event)内核对象对线程的通信要复杂些,主要通过对事件对象的监视来实现线程间的通信。事件对象由CreateEvent()函数来创建,具有两种存在状态:置位与复位,分别由SetEvent()和ResetEvent()来产生。事件的置位将通过 WaitForSingleObject()或WaitForMultipleObjects()之类的通知等待函数继续执行。
// 事件句柄
HANDLE hEvent = NULL;
UINT ThreadProc7(LPVOID pParam)
{
while(true)
{
// 等待事件发生
DWORD dwRet = WaitForSingleObject(hEvent, 0);
// 如果事件置位则退出线程,否则将继续执行
if (dwRet == WAIT_OBJECT_0)
break;
else
{
Sleep(2000);
AfxMessageBox("线程正在运行!");
}
}
AfxMessageBox("线程终止运行!");
return 0;
}
……
void CSample06View::OnEventStart()
{
// 创建事件
hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
// 启动线程
AfxBeginThread(ThreadProc7, NULL);
}
void CSample06View::OnEventEnd()
{
// 事件置位
SetEvent(hEvent);
}
上面这段代码展示了事件对象在线程通信中的作用。在创建线程前首先创建一个事件对象hEvent,这里CreateEvent()函数所采用的四个参数分别表示句柄不能被继承、事件在置位后将由系统自动进行复位、事件对象初始状态为复位状态和不指定事件名。在创建的子线程中使用 WaitForSingleObject()对hEvent进行监视。WaitForSingleObject()的函数原型为:
DWORD WaitForSingleObject(
HANDLE hHandle, //等待对象的句柄
DWORD dwMilliseconds //超过时间间隔
);
函数将在hHandle对象有信号时或是在等待时间超出由dwMilliseconds设定的超时时间间隔返回。其返回值可以为 WAIT_ABANDONED、WAIT_OBJECT_0和WAIT_TIMEOUT,分别表示被等待的互斥量(Mutex)对象没有被释放、等待的对象信号置位和超时。通过对返回值的判断可以区分出引起WaitForSingleObject()函数返回的原因。在本例中只关心 WAIT_OBJECT_0的返回值,当通过SetEvent()将hEvent置位后即可使WaitForSingleObject()立即返回并通过跳出循环而结束线程。
线程的同步机制:
1、 Event
用事件(Event)来同步线程是最具弹性的了。一个事件有两种状态:激发状态和未激发状态。也称有信号状态和无信号状态。事件又分两种类型:手动重置事件和自动重置事件。手动重置事件被设置为激发状态后,会唤醒所有等待的线程,而且一直保持为激发状态,直到程序重新把它设置为未激发状态。自动重置事件被设置为激发状态后,会唤醒“一个”等待中的线程,然后自动恢复为未激发状态。所以用自动重置事件来同步两个线程比较理想。MFC中对应的类为 CEvent.。CEvent的构造函数默认创建一个自动重置的事件,而且处于未激发状态。共有三个函数来改变事件的状态:SetEvent,ResetEvent和PulseEvent。用事件来同步线程是一种比较理想的做法,但在实际的使用过程中要注意的是,对自动重置事件调用SetEvent和PulseEvent有可能会引起死锁,必须小心。
2、 Critical Section
CRITICAL_SECTION是最快的。其他内核锁(事件、互斥体),每进一次内核,都需要上千个CPU周期。
使用临界区域的第一个忠告就是不要长时间锁住一份资源。这里的长时间是相对的,视不同程序而定。对一些控制软件来说,可能是数毫秒,但是对另外一些程序来说,可以长达数分钟。但进入临界区后必须尽快地离开,释放资源。如果不释放的话,会如何?答案是不会怎样。如果是主线程(GUI线程)要进入一个没有被释放的临界区,呵呵,程序就会挂了!临界区域的一个缺点就是:Critical Section不是一个核心对象,无法获知进入临界区的线程是生是死,如果进入临界区的线程挂了,没有释放临界资源,系统无法获知,而且没有办法释放该临界资源。这个缺点在互斥器(Mutex)中得到了弥补。Critical Section在MFC中的相应实现类是CcriticalSection。CcriticalSection::Lock()进入临界区,CcriticalSection::UnLock()离开临界区。
3、 Mutex
互斥器的功能和临界区域很相似。区别是:Mutex所花费的时间比Critical Section多的多,但是Mutex是核心对象(Event、Semaphore也是),可以跨进程使用,而且等待一个被锁住的Mutex可以设定 TIMEOUT,不会像Critical Section那样无法得知临界区域的情况,而一直死等。MFC中的对应类为CMutex。Win32函数有:创建互斥体CreateMutex() ,打开互斥体OpenMutex(),释放互斥体ReleaseMutex()。Mutex的拥有权并非属于那个产生它的线程,而是最后那个对此 Mutex进行等待操作(WaitForSingleObject等等)并且尚未进行ReleaseMutex()操作的线程。线程拥有Mutex就好像进入Critical Section一样,一次只能有一个线程拥有该Mutex。如果一个拥有Mutex的线程在返回之前没有调用ReleaseMutex(),那么这个 Mutex就被舍弃了,但是当其他线程等待(WaitForSingleObject等)这个Mutex时,仍能返回,并得到一个 WAIT_ABANDONED_0返回值。能够知道一个Mutex被舍弃是Mutex特有的。
4、 Semaphore
信号量是最具历史的同步机制。信号量是解决producer/consumer问题的关键要素。对应的MFC类是Csemaphore。Win32函数 CreateSemaphore()用来产生信号量。ReleaseSemaphore()用来解除锁定。Semaphore的现值代表的意义是目前可用的资源数,如果Semaphore的现值为1,表示还有一个锁定动作可以成功。如果现值为5,就表示还有五个锁定动作可以成功。当调用Wait…等函数要求锁定,如果Semaphore现值不为0,Wait…马上返回,资源数减1。当调用ReleaseSemaphore()资源数加1,当时不会超过初始设定的资源总数。
线程之间的通讯:
线程常常要将数据传递给另外一个线程。Worker线程可能需要告诉别人说它的工作完成了,GUI线程则可能需要交给Worker线程一件新的工作。通过PostThreadMessage(),可以将消息传递给目标线程,当然目标线程必须有消息队列。以消息当作通讯方式,比起标准技术如使用全局变量等,有很大的好处。如果对象是同一进程中的线程,可以发送自定义消息,传递数据给目标线程,如果是线程在不同的进程中,就涉及进程之间的通讯了。下面将会讲到。
①使用C++标准库的thread、mutex头文件:
1 #include <thread> 2 #include <mutex> 3 #include <iostream> 4 5 void Fun_1(); 6 void Fun_2(); 7 8 unsigned int counter = 0; 9 std::mutex mtx; 10 11 int main() 12 { 13 std::thread thrd_1(Fun_1); 14 std::thread thrd_2(Fun_2); 15 thrd_1.join(); 16 thrd_2.join(); 17 system("pause"); 18 return 0; 19 } 20 21 void Fun_1() 22 { 23 while (true) 24 { 25 std::lock_guard<std::mutex> mtx_locker(mtx); 26 ++counter; 27 if (counter < 1000) 28 { 29 std::cout << "Function 1 counting " << counter << "...\n"; 30 } 31 else 32 { 33 break; 34 } 35 } 36 } 37 38 void Fun_2() 39 { 40 while (true) 41 { 42 std::lock_guard<std::mutex> mtx_locker(mtx); 43 ++counter; 44 if (counter < 1000) 45 { 46 std::cout << "Function 2 counting " << counter << "...\n"; 47 } 48 else 49 { 50 break; 51 } 52 } 53 }
这段代码与前面一段代码唯一的区别就是在两个线程关联的函数中加了一句 std::lock_guard<std::mutex> mtx_locker(mtx); 在C++中,通过构造std::mutex的实例来创建互斥元,可通过调用其成员函数lock()和unlock()来实现加锁和解锁,然后这是不推荐的做法,因为这要求程序员在离开函数的每条代码路径上都调用unlock(),包括由于异常所导致的在内。作为替代,标准库提供了std::lock_guard类模板,实现了互斥元的RAII惯用语法(资源获取即初始化)。该对象在构造时锁定所给的互斥元,析构时解锁该互斥元,从而保证被锁定的互斥元始终被正确解锁。代码运行结果如下图所示,可见得到了正确的结果。
②使用windows API的临界区对象:
1 //header.h 2 #ifndef CRTC_SEC_H 3 #define CRTC_SEC_H 4 5 #include "windows.h" 6 7 class RAII_CrtcSec 8 { 9 private: 10 CRITICAL_SECTION crtc_sec; 11 public: 12 RAII_CrtcSec() { ::InitializeCriticalSection(&crtc_sec); } 13 ~RAII_CrtcSec() { ::DeleteCriticalSection(&crtc_sec); } 14 RAII_CrtcSec(const RAII_CrtcSec &) = delete; 15 RAII_CrtcSec & operator=(const RAII_CrtcSec &) = delete; 16 // 17 void Lock() { ::EnterCriticalSection(&crtc_sec); } 18 void Unlock() { ::LeaveCriticalSection(&crtc_sec); } 19 }; 20 21 #endif
1 //main.cpp 2 #include <windows.h> 3 #include <iostream> 4 #include "header.h" 5 6 DWORD WINAPI Fun_1(LPVOID p); 7 DWORD WINAPI Fun_2(LPVOID p); 8 9 unsigned int counter = 0; 10 RAII_CrtcSec cs; 11 12 int main() 13 { 14 HANDLE h1, h2; 15 h1 = CreateThread(nullptr, 0, Fun_1, nullptr, 0, 0); 16 std::cout << "Thread 1 started...\n"; 17 h2 = CreateThread(nullptr, 0, Fun_2, nullptr, 0, 0); 18 std::cout << "Thread 2 started...\n"; 19 CloseHandle(h1); 20 CloseHandle(h2); 21 // 22 system("pause"); 23 return 0; 24 } 25 26 DWORD WINAPI Fun_1(LPVOID p) 27 { 28 while (true) 29 { 30 cs.Lock(); 31 ++counter; 32 if (counter < 1000) 33 { 34 std::cout << "Thread 1 counting " << counter << "...\n"; 35 cs.Unlock(); 36 } 37 else 38 { 39 cs.Unlock(); 40 break; 41 } 42 } 43 return 0; 44 } 45 46 DWORD WINAPI Fun_2(LPVOID p) 47 { 48 while (true) 49 { 50 cs.Lock(); 51 ++counter; 52 if (counter < 1000) 53 { 54 std::cout << "Thread 2 counting " << counter << "...\n"; 55 cs.Unlock(); 56 } 57 else 58 { 59 cs.Unlock(); 60 break; 61 } 62 } 63 return 0; 64 }
上面的代码使用了windows提供的API中的临界区对象来实现线程同步。临界区是指一个访问共享资源的代码段,临界区对象则是指当用户使用某个线程访问共享资源时,必须使代码段独占该资源,不允许其他线程访问该资源。在该线程访问完资源后,其他线程才能对资源进行访问。Windows API提供了临界区对象的结构体CRITICAL_SECTION,对该对象的使用可总结为如下几步:
1.InitializeCriticalSection(LPCRITICAL_SECTION lpCriticalSection),该函数的作用是初始化临界区,唯一的参数是指向结构体CRITICAL_SECTION的指针变量。
2.EnterCriticalSection(LPCRITICAL_SECTION lpCriticalSection),该函数的作用是使调用该函数的线程进入已经初始化的临界区,并拥有该临界区的所有权。这是一个阻塞函数,如果线程获得临界区的所有权成功,则该函数将返回,调用线程继续执行,否则该函数将一直等待,这样会造成该函数的调用线程也一直等待。如果不想让调用线程等待(非阻塞),则应该使用TryEnterCriticalSection(LPCRITICAL_SECTION lpCriticalSection)。
3.LeaveCriticalSection(LPCRITICAL_SECTION lpCriticalSection),该函数的作用是使调用该函数的线程离开临界区并释放对该临界区的所有权,以便让其他线程也获得访问该共享资源的机会。一定要在程序不适用临界区时调用该函数释放临界区所有权,否则程序将一直等待造成程序假死。
4.DeleteCriticalSection(LPCRITICAL_SECTION lpCriticalSection),该函数的作用是删除程序中已经被初始化的临界区。如果函数调用成功,则程序会将内存中的临界区删除,防止出现内存错误。
该段代码的运行结果如下图所示:
③使用Windows API的事件对象:
1 //main.cpp 2 #include <windows.h> 3 #include <iostream> 4 5 DWORD WINAPI Fun_1(LPVOID p); 6 DWORD WINAPI Fun_2(LPVOID p); 7 8 HANDLE h_event; 9 unsigned int counter = 0; 10 11 int main() 12 { 13 h_event = CreateEvent(nullptr, true, false, nullptr); 14 SetEvent(h_event); 15 HANDLE h1 = CreateThread(nullptr, 0, Fun_1, nullptr, 0, nullptr); 16 std::cout << "Thread 1 started...\n"; 17 HANDLE h2 = CreateThread(nullptr, 0, Fun_2, nullptr, 0, nullptr); 18 std::cout << "Thread 2 started...\n"; 19 CloseHandle(h1); 20 CloseHandle(h2); 21 // 22 system("pause"); 23 return 0; 24 } 25 26 DWORD WINAPI Fun_1(LPVOID p) 27 { 28 while (true) 29 { 30 WaitForSingleObject(h_event, INFINITE); 31 ResetEvent(h_event); 32 if (counter < 1000) 33 { 34 ++counter; 35 std::cout << "Thread 1 counting " << counter << "...\n"; 36 SetEvent(h_event); 37 } 38 else 39 { 40 SetEvent(h_event); 41 break; 42 } 43 } 44 return 0; 45 } 46 47 DWORD WINAPI Fun_2(LPVOID p) 48 { 49 while (true) 50 { 51 WaitForSingleObject(h_event, INFINITE); 52 ResetEvent(h_event); 53 if (counter < 1000) 54 { 55 ++counter; 56 std::cout << "Thread 2 counting " << counter << "...\n"; 57 SetEvent(h_event); 58 } 59 else 60 { 61 SetEvent(h_event); 62 break; 63 } 64 } 65 return 0; 66 }
事件对象是一种内核对象,用户在程序中使用内核对象的有无信号状态来实现线程的同步。使用事件对象的步骤可概括如下:
1.创建事件对象,函数原型为:
1 HANDLE WINAPI CreateEvent( 2 _In_opt_ LPSECURITY_ATTRIBUTES lpEventAttributes, 3 _In_ BOOL bManualReset, 4 _In_ BOOL bInitialState, 5 _In_opt_ LPCTSTR lpName 6 );
如果该函数调用成功,则返回新创建的事件对象,否则返回NULL。函数参数的含义如下:
-lpEventAttributes:表示创建的事件对象的安全属性,若设为NULL则表示该程序使用的是默认安全属性。
-bManualReset:表示所创建的事件对象是人工重置还是自动重置。若设为true,则表示使用人工重置,在调用线程获得事件对象所有权后用户要显式地调用ResetEvent()将事件对象设置为无信号状态。
-bInitialState:表示事件对象的初始状态。如果为true,则表示该事件对象初始时为有信号状态,则线程可以使用事件对象。
-lpName:表示事件对象的名称,若为NULL,则表示创建的是匿名事件对象。
2.若事件对象初始状态设置为无信号,则需调用SetEvent(HANDLE hEvent)将其设置为有信号状态。ResetEvent(HANDLE hEvent)则用于将事件对象设置为无信号状态。
3.线程通过调用WaitForSingleObject()主动请求事件对象,该函数原型如下:
1 DWORD WINAPI WaitForSingleObject( 2 _In_ HANDLE hHandle, 3 _In_ DWORD dwMilliseconds 4 );
该函数将在用户指定的事件对象上等待。如果事件对象处于有信号状态,函数将返回。否则函数将一直等待,直到用户所指定的事件到达。
该代码的运行结果如下图所示:
④使用Windows API的互斥对象:
1 //main.cpp 2 #include <windows.h> 3 #include <iostream> 4 5 DWORD WINAPI Fun_1(LPVOID p); 6 DWORD WINAPI Fun_2(LPVOID p); 7 8 HANDLE h_mutex; 9 unsigned int counter = 0; 10 11 int main() 12 { 13 h_mutex = CreateMutex(nullptr, false, nullptr); 14 HANDLE h1 = CreateThread(nullptr, 0, Fun_1, nullptr, 0, nullptr); 15 std::cout << "Thread 1 started...\n"; 16 HANDLE h2 = CreateThread(nullptr, 0, Fun_2, nullptr, 0, nullptr); 17 std::cout << "Thread 2 started...\n"; 18 CloseHandle(h1); 19 CloseHandle(h2); 20 // 21 //CloseHandle(h_mutex); 22 system("pause"); 23 return 0; 24 } 25 26 DWORD WINAPI Fun_1(LPVOID p) 27 { 28 while (true) 29 { 30 WaitForSingleObject(h_mutex, INFINITE); 31 if (counter < 1000) 32 { 33 ++counter; 34 std::cout << "Thread 1 counting " << counter << "...\n"; 35 ReleaseMutex(h_mutex); 36 } 37 else 38 { 39 ReleaseMutex(h_mutex); 40 break; 41 } 42 } 43 return 0; 44 } 45 46 DWORD WINAPI Fun_2(LPVOID p) 47 { 48 while (true) 49 { 50 WaitForSingleObject(h_mutex, INFINITE); 51 if (counter < 1000) 52 { 53 ++counter; 54 std::cout << "Thread 2 counting " << counter << "...\n"; 55 ReleaseMutex(h_mutex); 56 } 57 else 58 { 59 ReleaseMutex(h_mutex); 60 break; 61 } 62 } 63 return 0; 64 }
原文地址:https://blog.csdn.net/qq_41786318/article/details/79539287
进程通信是讲的比较多的,很多教科书上也有,包括下面几种:
1.管道
2.信号(系统内的一些信号,复杂,与信号量不同)
3.消息队列
4.共享内存
5.信号量(进程线程同步互斥的方法)
6.套接字
VC 线程间通信的三种方式:
实现线程间通信的方法有很多,常用的主要是通过全局变量、自定义消息和事件对象等来实现的。其中又以对全局变量的使用最为简洁。该方法将全局变量作为线程监视的对象,并通过在主线程对此变量值的改变而实现对子线程的控制。
由于这里的全局变量需要在使用它的线程之外对其值进行改变,这就需要通过volatile关键字对此变量进行说明。使用全局变量进行线程通信的方法非常简单,通过下面给出的示例代码能够对其有一个基本的认识。
// 线程通信用全局变量
volatile bool g_bDo = false;
……
//线程处理函数
UINT ThreadProc5(LPVOID pParam)
{
//根据全局变量g_bDo的取值来决定线程的运行
while (g_bDo)
{
Sleep(2000);
AfxMessageBox("线程正在运行!");
}
AfxMessageBox("线程终止");
return 0;
}
……
void CSample06View::OnGlobalStart()
{
// 通过全局变量通知线程执行
g_bDo = true;
// 启动线程
AfxBeginThread(ThreadProc5, NULL);
}
void CSample06View::OnGlobalEnd()
{
// 通过全局变量通知线程结束
g_bDo = false;
}
2.利用自定义消息(可适用于窗体)
全局变量在线程通信中的应用多用在主线程对子线程的控制上,而从子线程向主线程的信息反馈则多采用自定义消息的方式来进行。这里对自定义消息的使用同使用普通自定义消息非常相似,只不过消息的发送是在子线程函数中进行的。该方法的主体是自定义消息,应首先定义自定义消息并添加对消息的响应代码。
// 自定义消息
#define WM_USER_MSG WM_USER + 101
……
//消息响应函数在头文件中的定义:
//{{AFX_MSG(CSample06View)
//}}AFX_MSG
afx_msg void OnUserMsg(WPARAM wParam, LPARAM lParam);
DECLARE_MESSAGE_MAP()
……
//消息映射
BEGIN_MESSAGE_MAP(CSample06View, CView)
//{{AFX_MSG_MAP(CSample06View)
//}}AFX_MSG_MAP
ON_MESSAGE(WM_USER_MSG, OnUserMsg)
END_MESSAGE_MAP()
……
//消息响应函数
void CSample06View::OnUserMsg(WPARAM wParam, LPARAM lParam)
{
// 报告消息
AfxMessageBox("线程已退出!");
}
此后,在子线程函数需要向主线程发送消息的地方调用PostMessage()或SendMessage()消息传递函数将消息发送给主线程即可。由于消息发送函数是在线程中被调用,因此需要指出接受窗口句柄,可通过线程参数将其传递进线程函数。
UINT ThreadProc6(LPVOID pParam)
{
// 延迟一秒
Sleep(1000);
// 向主线程发送自定义消息
::PostMessage((HWND)pParam, WM_USER_MSG, 0, 0);
return 0;
}
……
void CSample06View::OnUseMessage()
{
// 获取窗口句柄
HWND hWnd = GetSafeHwnd();
// 启动线程
AfxBeginThread(ThreadProc6, hWnd);
}
3.使用事件内核对象(相当好用)
利用事件(Event)内核对象对线程的通信要复杂些,主要通过对事件对象的监视来实现线程间的通信。事件对象由CreateEvent()函数来创建,具有两种存在状态:置位与复位,分别由SetEvent()和ResetEvent()来产生。事件的置位将通过 WaitForSingleObject()或WaitForMultipleObjects()之类的通知等待函数继续执行。
// 事件句柄
HANDLE hEvent = NULL;
UINT ThreadProc7(LPVOID pParam)
{
while(true)
{
// 等待事件发生
DWORD dwRet = WaitForSingleObject(hEvent, 0);
// 如果事件置位则退出线程,否则将继续执行
if (dwRet == WAIT_OBJECT_0)
break;
else
{
Sleep(2000);
AfxMessageBox("线程正在运行!");
}
}
AfxMessageBox("线程终止运行!");
return 0;
}
……
void CSample06View::OnEventStart()
{
// 创建事件
hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
// 启动线程
AfxBeginThread(ThreadProc7, NULL);
}
void CSample06View::OnEventEnd()
{
// 事件置位
SetEvent(hEvent);
}
上面这段代码展示了事件对象在线程通信中的作用。在创建线程前首先创建一个事件对象hEvent,这里CreateEvent()函数所采用的四个参数分别表示句柄不能被继承、事件在置位后将由系统自动进行复位、事件对象初始状态为复位状态和不指定事件名。在创建的子线程中使用 WaitForSingleObject()对hEvent进行监视。WaitForSingleObject()的函数原型为:
DWORD WaitForSingleObject(
HANDLE hHandle, //等待对象的句柄
DWORD dwMilliseconds //超过时间间隔
);
函数将在hHandle对象有信号时或是在等待时间超出由dwMilliseconds设定的超时时间间隔返回。其返回值可以为 WAIT_ABANDONED、WAIT_OBJECT_0和WAIT_TIMEOUT,分别表示被等待的互斥量(Mutex)对象没有被释放、等待的对象信号置位和超时。通过对返回值的判断可以区分出引起WaitForSingleObject()函数返回的原因。在本例中只关心 WAIT_OBJECT_0的返回值,当通过SetEvent()将hEvent置位后即可使WaitForSingleObject()立即返回并通过跳出循环而结束线程。
线程的同步机制:
1、 Event
用事件(Event)来同步线程是最具弹性的了。一个事件有两种状态:激发状态和未激发状态。也称有信号状态和无信号状态。事件又分两种类型:手动重置事件和自动重置事件。手动重置事件被设置为激发状态后,会唤醒所有等待的线程,而且一直保持为激发状态,直到程序重新把它设置为未激发状态。自动重置事件被设置为激发状态后,会唤醒“一个”等待中的线程,然后自动恢复为未激发状态。所以用自动重置事件来同步两个线程比较理想。MFC中对应的类为 CEvent.。CEvent的构造函数默认创建一个自动重置的事件,而且处于未激发状态。共有三个函数来改变事件的状态:SetEvent,ResetEvent和PulseEvent。用事件来同步线程是一种比较理想的做法,但在实际的使用过程中要注意的是,对自动重置事件调用SetEvent和PulseEvent有可能会引起死锁,必须小心。
2、 Critical Section
CRITICAL_SECTION是最快的。其他内核锁(事件、互斥体),每进一次内核,都需要上千个CPU周期。
使用临界区域的第一个忠告就是不要长时间锁住一份资源。这里的长时间是相对的,视不同程序而定。对一些控制软件来说,可能是数毫秒,但是对另外一些程序来说,可以长达数分钟。但进入临界区后必须尽快地离开,释放资源。如果不释放的话,会如何?答案是不会怎样。如果是主线程(GUI线程)要进入一个没有被释放的临界区,呵呵,程序就会挂了!临界区域的一个缺点就是:Critical Section不是一个核心对象,无法获知进入临界区的线程是生是死,如果进入临界区的线程挂了,没有释放临界资源,系统无法获知,而且没有办法释放该临界资源。这个缺点在互斥器(Mutex)中得到了弥补。Critical Section在MFC中的相应实现类是CcriticalSection。CcriticalSection::Lock()进入临界区,CcriticalSection::UnLock()离开临界区。
3、 Mutex
互斥器的功能和临界区域很相似。区别是:Mutex所花费的时间比Critical Section多的多,但是Mutex是核心对象(Event、Semaphore也是),可以跨进程使用,而且等待一个被锁住的Mutex可以设定 TIMEOUT,不会像Critical Section那样无法得知临界区域的情况,而一直死等。MFC中的对应类为CMutex。Win32函数有:创建互斥体CreateMutex() ,打开互斥体OpenMutex(),释放互斥体ReleaseMutex()。Mutex的拥有权并非属于那个产生它的线程,而是最后那个对此 Mutex进行等待操作(WaitForSingleObject等等)并且尚未进行ReleaseMutex()操作的线程。线程拥有Mutex就好像进入Critical Section一样,一次只能有一个线程拥有该Mutex。如果一个拥有Mutex的线程在返回之前没有调用ReleaseMutex(),那么这个 Mutex就被舍弃了,但是当其他线程等待(WaitForSingleObject等)这个Mutex时,仍能返回,并得到一个 WAIT_ABANDONED_0返回值。能够知道一个Mutex被舍弃是Mutex特有的。
4、 Semaphore
信号量是最具历史的同步机制。信号量是解决producer/consumer问题的关键要素。对应的MFC类是Csemaphore。Win32函数 CreateSemaphore()用来产生信号量。ReleaseSemaphore()用来解除锁定。Semaphore的现值代表的意义是目前可用的资源数,如果Semaphore的现值为1,表示还有一个锁定动作可以成功。如果现值为5,就表示还有五个锁定动作可以成功。当调用Wait…等函数要求锁定,如果Semaphore现值不为0,Wait…马上返回,资源数减1。当调用ReleaseSemaphore()资源数加1,当时不会超过初始设定的资源总数。
线程之间的通讯:
线程常常要将数据传递给另外一个线程。Worker线程可能需要告诉别人说它的工作完成了,GUI线程则可能需要交给Worker线程一件新的工作。通过PostThreadMessage(),可以将消息传递给目标线程,当然目标线程必须有消息队列。以消息当作通讯方式,比起标准技术如使用全局变量等,有很大的好处。如果对象是同一进程中的线程,可以发送自定义消息,传递数据给目标线程,如果是线程在不同的进程中,就涉及进程之间的通讯了。下面将会讲到。
①使用C++标准库的thread、mutex头文件:
1 #include <thread> 2 #include <mutex> 3 #include <iostream> 4 5 void Fun_1(); 6 void Fun_2(); 7 8 unsigned int counter = 0; 9 std::mutex mtx; 10 11 int main() 12 { 13 std::thread thrd_1(Fun_1); 14 std::thread thrd_2(Fun_2); 15 thrd_1.join(); 16 thrd_2.join(); 17 system("pause"); 18 return 0; 19 } 20 21 void Fun_1() 22 { 23 while (true) 24 { 25 std::lock_guard<std::mutex> mtx_locker(mtx); 26 ++counter; 27 if (counter < 1000) 28 { 29 std::cout << "Function 1 counting " << counter << "...\n"; 30 } 31 else 32 { 33 break; 34 } 35 } 36 } 37 38 void Fun_2() 39 { 40 while (true) 41 { 42 std::lock_guard<std::mutex> mtx_locker(mtx); 43 ++counter; 44 if (counter < 1000) 45 { 46 std::cout << "Function 2 counting " << counter << "...\n"; 47 } 48 else 49 { 50 break; 51 } 52 } 53 }
这段代码与前面一段代码唯一的区别就是在两个线程关联的函数中加了一句 std::lock_guard<std::mutex> mtx_locker(mtx); 在C++中,通过构造std::mutex的实例来创建互斥元,可通过调用其成员函数lock()和unlock()来实现加锁和解锁,然后这是不推荐的做法,因为这要求程序员在离开函数的每条代码路径上都调用unlock(),包括由于异常所导致的在内。作为替代,标准库提供了std::lock_guard类模板,实现了互斥元的RAII惯用语法(资源获取即初始化)。该对象在构造时锁定所给的互斥元,析构时解锁该互斥元,从而保证被锁定的互斥元始终被正确解锁。代码运行结果如下图所示,可见得到了正确的结果。
②使用windows API的临界区对象:
1 //header.h 2 #ifndef CRTC_SEC_H 3 #define CRTC_SEC_H 4 5 #include "windows.h" 6 7 class RAII_CrtcSec 8 { 9 private: 10 CRITICAL_SECTION crtc_sec; 11 public: 12 RAII_CrtcSec() { ::InitializeCriticalSection(&crtc_sec); } 13 ~RAII_CrtcSec() { ::DeleteCriticalSection(&crtc_sec); } 14 RAII_CrtcSec(const RAII_CrtcSec &) = delete; 15 RAII_CrtcSec & operator=(const RAII_CrtcSec &) = delete; 16 // 17 void Lock() { ::EnterCriticalSection(&crtc_sec); } 18 void Unlock() { ::LeaveCriticalSection(&crtc_sec); } 19 }; 20 21 #endif
1 //main.cpp 2 #include <windows.h> 3 #include <iostream> 4 #include "header.h" 5 6 DWORD WINAPI Fun_1(LPVOID p); 7 DWORD WINAPI Fun_2(LPVOID p); 8 9 unsigned int counter = 0; 10 RAII_CrtcSec cs; 11 12 int main() 13 { 14 HANDLE h1, h2; 15 h1 = CreateThread(nullptr, 0, Fun_1, nullptr, 0, 0); 16 std::cout << "Thread 1 started...\n"; 17 h2 = CreateThread(nullptr, 0, Fun_2, nullptr, 0, 0); 18 std::cout << "Thread 2 started...\n"; 19 CloseHandle(h1); 20 CloseHandle(h2); 21 // 22 system("pause"); 23 return 0; 24 } 25 26 DWORD WINAPI Fun_1(LPVOID p) 27 { 28 while (true) 29 { 30 cs.Lock(); 31 ++counter; 32 if (counter < 1000) 33 { 34 std::cout << "Thread 1 counting " << counter << "...\n"; 35 cs.Unlock(); 36 } 37 else 38 { 39 cs.Unlock(); 40 break; 41 } 42 } 43 return 0; 44 } 45 46 DWORD WINAPI Fun_2(LPVOID p) 47 { 48 while (true) 49 { 50 cs.Lock(); 51 ++counter; 52 if (counter < 1000) 53 { 54 std::cout << "Thread 2 counting " << counter << "...\n"; 55 cs.Unlock(); 56 } 57 else 58 { 59 cs.Unlock(); 60 break; 61 } 62 } 63 return 0; 64 }
上面的代码使用了windows提供的API中的临界区对象来实现线程同步。临界区是指一个访问共享资源的代码段,临界区对象则是指当用户使用某个线程访问共享资源时,必须使代码段独占该资源,不允许其他线程访问该资源。在该线程访问完资源后,其他线程才能对资源进行访问。Windows API提供了临界区对象的结构体CRITICAL_SECTION,对该对象的使用可总结为如下几步:
1.InitializeCriticalSection(LPCRITICAL_SECTION lpCriticalSection),该函数的作用是初始化临界区,唯一的参数是指向结构体CRITICAL_SECTION的指针变量。
2.EnterCriticalSection(LPCRITICAL_SECTION lpCriticalSection),该函数的作用是使调用该函数的线程进入已经初始化的临界区,并拥有该临界区的所有权。这是一个阻塞函数,如果线程获得临界区的所有权成功,则该函数将返回,调用线程继续执行,否则该函数将一直等待,这样会造成该函数的调用线程也一直等待。如果不想让调用线程等待(非阻塞),则应该使用TryEnterCriticalSection(LPCRITICAL_SECTION lpCriticalSection)。
3.LeaveCriticalSection(LPCRITICAL_SECTION lpCriticalSection),该函数的作用是使调用该函数的线程离开临界区并释放对该临界区的所有权,以便让其他线程也获得访问该共享资源的机会。一定要在程序不适用临界区时调用该函数释放临界区所有权,否则程序将一直等待造成程序假死。
4.DeleteCriticalSection(LPCRITICAL_SECTION lpCriticalSection),该函数的作用是删除程序中已经被初始化的临界区。如果函数调用成功,则程序会将内存中的临界区删除,防止出现内存错误。
该段代码的运行结果如下图所示:
③使用Windows API的事件对象:
1 //main.cpp 2 #include <windows.h> 3 #include <iostream> 4 5 DWORD WINAPI Fun_1(LPVOID p); 6 DWORD WINAPI Fun_2(LPVOID p); 7 8 HANDLE h_event; 9 unsigned int counter = 0; 10 11 int main() 12 { 13 h_event = CreateEvent(nullptr, true, false, nullptr); 14 SetEvent(h_event); 15 HANDLE h1 = CreateThread(nullptr, 0, Fun_1, nullptr, 0, nullptr); 16 std::cout << "Thread 1 started...\n"; 17 HANDLE h2 = CreateThread(nullptr, 0, Fun_2, nullptr, 0, nullptr); 18 std::cout << "Thread 2 started...\n"; 19 CloseHandle(h1); 20 CloseHandle(h2); 21 // 22 system("pause"); 23 return 0; 24 } 25 26 DWORD WINAPI Fun_1(LPVOID p) 27 { 28 while (true) 29 { 30 WaitForSingleObject(h_event, INFINITE); 31 ResetEvent(h_event); 32 if (counter < 1000) 33 { 34 ++counter; 35 std::cout << "Thread 1 counting " << counter << "...\n"; 36 SetEvent(h_event); 37 } 38 else 39 { 40 SetEvent(h_event); 41 break; 42 } 43 } 44 return 0; 45 } 46 47 DWORD WINAPI Fun_2(LPVOID p) 48 { 49 while (true) 50 { 51 WaitForSingleObject(h_event, INFINITE); 52 ResetEvent(h_event); 53 if (counter < 1000) 54 { 55 ++counter; 56 std::cout << "Thread 2 counting " << counter << "...\n"; 57 SetEvent(h_event); 58 } 59 else 60 { 61 SetEvent(h_event); 62 break; 63 } 64 } 65 return 0; 66 }
事件对象是一种内核对象,用户在程序中使用内核对象的有无信号状态来实现线程的同步。使用事件对象的步骤可概括如下:
1.创建事件对象,函数原型为:
1 HANDLE WINAPI CreateEvent( 2 _In_opt_ LPSECURITY_ATTRIBUTES lpEventAttributes, 3 _In_ BOOL bManualReset, 4 _In_ BOOL bInitialState, 5 _In_opt_ LPCTSTR lpName 6 );
如果该函数调用成功,则返回新创建的事件对象,否则返回NULL。函数参数的含义如下:
-lpEventAttributes:表示创建的事件对象的安全属性,若设为NULL则表示该程序使用的是默认安全属性。
-bManualReset:表示所创建的事件对象是人工重置还是自动重置。若设为true,则表示使用人工重置,在调用线程获得事件对象所有权后用户要显式地调用ResetEvent()将事件对象设置为无信号状态。
-bInitialState:表示事件对象的初始状态。如果为true,则表示该事件对象初始时为有信号状态,则线程可以使用事件对象。
-lpName:表示事件对象的名称,若为NULL,则表示创建的是匿名事件对象。
2.若事件对象初始状态设置为无信号,则需调用SetEvent(HANDLE hEvent)将其设置为有信号状态。ResetEvent(HANDLE hEvent)则用于将事件对象设置为无信号状态。
3.线程通过调用WaitForSingleObject()主动请求事件对象,该函数原型如下:
1 DWORD WINAPI WaitForSingleObject( 2 _In_ HANDLE hHandle, 3 _In_ DWORD dwMilliseconds 4 );
该函数将在用户指定的事件对象上等待。如果事件对象处于有信号状态,函数将返回。否则函数将一直等待,直到用户所指定的事件到达。
该代码的运行结果如下图所示:
④使用Windows API的互斥对象:
1 //main.cpp 2 #include <windows.h> 3 #include <iostream> 4 5 DWORD WINAPI Fun_1(LPVOID p); 6 DWORD WINAPI Fun_2(LPVOID p); 7 8 HANDLE h_mutex; 9 unsigned int counter = 0; 10 11 int main() 12 { 13 h_mutex = CreateMutex(nullptr, false, nullptr); 14 HANDLE h1 = CreateThread(nullptr, 0, Fun_1, nullptr, 0, nullptr); 15 std::cout << "Thread 1 started...\n"; 16 HANDLE h2 = CreateThread(nullptr, 0, Fun_2, nullptr, 0, nullptr); 17 std::cout << "Thread 2 started...\n"; 18 CloseHandle(h1); 19 CloseHandle(h2); 20 // 21 //CloseHandle(h_mutex); 22 system("pause"); 23 return 0; 24 } 25 26 DWORD WINAPI Fun_1(LPVOID p) 27 { 28 while (true) 29 { 30 WaitForSingleObject(h_mutex, INFINITE); 31 if (counter < 1000) 32 { 33 ++counter; 34 std::cout << "Thread 1 counting " << counter << "...\n"; 35 ReleaseMutex(h_mutex); 36 } 37 else 38 { 39 ReleaseMutex(h_mutex); 40 break; 41 } 42 } 43 return 0; 44 } 45 46 DWORD WINAPI Fun_2(LPVOID p) 47 { 48 while (true) 49 { 50 WaitForSingleObject(h_mutex, INFINITE); 51 if (counter < 1000) 52 { 53 ++counter; 54 std::cout << "Thread 2 counting " << counter << "...\n"; 55 ReleaseMutex(h_mutex); 56 } 57 else 58 { 59 ReleaseMutex(h_mutex); 60 break; 61 } 62 } 63 return 0; 64 }