线程间的同步方法大体可分为两类:用户模式和内核模式。顾名思义,内核模式就是指利用系统内核对象的单一性来进行同步,使用时需要切换内核态与用户态,而用户模式就是不需要切换到内核态,只在用户态完成操作。
用户模式下的方法有:原子操作(例如一个单一的全局变量),临界区。内核模式下的方法有:事件,信号量,互斥量。
下面我们来分别看一下这些方法:
Critical section(临界区)用来实现“排他性占有”。适用范围是单一进程的各线程之间。它是:
一个局部性对象,不是一个核心对象。
快速而有效率。
不能够同时有一个以上的 critical section 被等待。
无法侦测是否已被某个线程放弃。
可以实现线程间互斥,不能用来实现同步。
vc下实现:
//CritiCalSection.h
/*
临界区是指一个访问共享资源的代码段,临界区对象则是指当用户使用某个线程访问共享资源时,必须使代码段独占该资源,不允许其他线程访问该资源。在该线程访问完资源后,其他线程才能对资源进行访问。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),该函数的作用是删除程序中已经被初始化的临界区。如果函数调用成功,则程序会将内存中的临界区删除,防止出现内存错误。
*/
#ifndef CRTC_SEC_H
#define CRTC_SEC_H
#include "windows.h"
class RAII_CrtcSec
{
private:
CRITICAL_SECTION crtc_sec;
public:
RAII_CrtcSec()
{
::InitializeCriticalSection(&crtc_sec);
}
~RAII_CrtcSec()
{
::DeleteCriticalSection(&crtc_sec);
}
RAII_CrtcSec(const RAII_CrtcSec &a)
{
crtc_sec = a.crtc_sec;
}
RAII_CrtcSec & operator=(const RAII_CrtcSec &a)
{
crtc_sec = a.crtc_sec;
}
void Lock()
{
::EnterCriticalSection(&crtc_sec);
}
void Unlock()
{
::LeaveCriticalSection(&crtc_sec);
}
};
#endif
//main.cpp
//临界区
#include <windows.h>
#include <iostream>
#include "CritiCalSection.h"
DWORD WINAPI Fun_1(LPVOID p);
DWORD WINAPI Fun_2(LPVOID p);
unsigned int counter = 0;
RAII_CrtcSec cs;
int main()
{
HANDLE h1, h2;
h1 = CreateThread(nullptr, 0, Fun_1, nullptr, 0, 0);
std::cout << "Thread 1 started...\n";
h2 = CreateThread(nullptr, 0, Fun_2, nullptr, 0, 0);
std::cout << "Thread 2 started...\n";
CloseHandle(h1);
CloseHandle(h2);
//
system("pause");
return 0;
}
DWORD WINAPI Fun_1(LPVOID p)
{
while (true)
{
cs.Lock();
++counter;
if (counter < 100)
{
std::cout << "Thread 1 counting " << counter << "...\n";
cs.Unlock();
}
else
{
cs.Unlock();
break;
}
}
return 0;
}
DWORD WINAPI Fun_2(LPVOID p)
{
while (true)
{
cs.Lock();
++counter;
if (counter < 100)
{
std::cout << "Thread 2 counting " << counter << "...\n";
cs.Unlock();
}
else
{
cs.Unlock();
break;
}
}
return 0;
}
2、信号量(Semaphore):它允许多个线程在同一时刻访问同一资源,但是需要限制在同一时刻访问此资源的最大线程数目, 信号量增加了资源计数的功能,预定数目的线程允许同时进入要同步的代码。
Semaphore 被用来追踪有限的资源。它是:
一个核心对象。
没有拥有者。
可以具名,因此可以被其他进程开启。
可以被任何一个线程释放(released)。
既能实现线程间互斥,也能实现线程间同步。
在跨进程中使用时,如果拥有信号量的线程意外结束,其它进程不会收到通知。
vc下实现:
#include "windows.h"
#include "iostream"
using namespace std;
int num=1;
HANDLE hDemaphore;
unsigned long _stdcall ThreadProc1(void *lpParameter)
{
while(num<100)
{
long count;
WaitForSingleObject(hDemaphore,INFINITE);
cout<<"信号量1当前计数"<<num<<endl;
num++;
Sleep(3000);
if(!ReleaseSemaphore(hDemaphore,1,&count))
{
if (0 == GetLastError())
{
printf("当前可用资源数:10\n");
return 0;
}
}
cout<<" 信号量1当前释放"<<count<<endl;
}
return 0;
}
unsigned long _stdcall ThreadProc2(void *lpParameter)
{
while(num<100)
{
long count;
WaitForSingleObject(hDemaphore,INFINITE);
cout<<"信号量2当前计数"<<num<<endl;
num++;
Sleep(3000);
if(!ReleaseSemaphore(hDemaphore,1,&count))
{
if (0 == GetLastError())
{
printf("当前可用资源数:10\n");
return 0;
}
}
cout<<" 信号量2当前释放"<<count<<endl;
}
return 0;
}
unsigned long _stdcall ThreadProc3(void *lpParameter)
{
while(num<100)
{
long count;
WaitForSingleObject(hDemaphore,INFINITE);
cout<<"信号量3当前计数"<<num<<endl;
num++;
Sleep(3000);
if(!ReleaseSemaphore(hDemaphore,1,&count))
{
if (0 == GetLastError())
{
printf("当前可用资源数:10\n");
return 0;
}
}
cout<<" 信号量3当前释放"<<count<<endl;
}
return 0;
}
int main()
{
HANDLE hThread1=CreateThread(NULL,0,ThreadProc1,NULL,0,NULL);
HANDLE hThread2=CreateThread(NULL,0,ThreadProc2,NULL,0,NULL);
HANDLE hThread3=CreateThread(NULL,0,ThreadProc3,NULL,0,NULL);
hDemaphore=CreateSemaphore(NULL, 1, 3, L"Sem");
CloseHandle(hThread1);
CloseHandle(hThread2);
CloseHandle(hThread3);
while(true)
{
;
}
CloseHandle(hDemaphore);
return 0;
}
3、互斥量(Mutex):采用互斥对象机制。只有拥有互斥对象的线程才有访问公共资源的权限,因为互斥对象只有一个,所以能保证公共资源不会同时被多个线程访问。互斥不仅能实现同一应用程序的公共资源安全共享,还能实现不同应用程序的公共资源安全共享
互斥比较类似阻塞,关键在于互斥可以跨进程的线程同步,而且等待一个被锁住的Mutex可以设定TIMEOUT,不会像Critical Section那样无法得知临界区域的情况,而一直死等。很多只允许应用程序运行一次的实例就是用互斥方法来实现的。当一个互斥对象不再被一个线程所拥有,它就处于发信号状态。此时首先调用waitForsingleobject()的线程就成为该互斥对象的拥有者,此互斥对象设为不发信号状态。当线程调用releaseMutex()并传递一个互斥对象的句柄作为参数时,这种拥有关系就被解除,互斥对象重新进入发信号状态。
Mutex 是一个核心对象,可以在不同的线程之间实现“排他性占有”,甚
至即使那些线程分属不同进程。它是:
一个核心对象。
如果拥有 mutex 的那个线程结束,则会产生一个 “abandoned” 错误信息。
可以具名,因此可以被其他进程开启。
只能被拥有它的那个线程释放(released)。
在跨进程中使用时,如果拥有互斥器的进程意外结束,其它进程会收到一个WAIT_ABANDOEND消息。
vc下实现:
//互斥对象的使用方法和c++标准库的mutex类似,互斥对象使用完后应记得释放。
//main.cpp
#include <windows.h>
#include <iostream>
DWORD WINAPI Fun_1(LPVOID p);
DWORD WINAPI Fun_2(LPVOID p);
HANDLE h_mutex;
unsigned int counter = 0;
int main()
{
h_mutex = CreateMutex(nullptr, false, nullptr);
HANDLE h1 = CreateThread(nullptr, 0, Fun_1, nullptr, 0, nullptr);
std::cout << "Thread 1 started...\n";
HANDLE h2 = CreateThread(nullptr, 0, Fun_2, nullptr, 0, nullptr);
std::cout << "Thread 2 started...\n";
CloseHandle(h1);
CloseHandle(h2);
//
//CloseHandle(h_mutex);
system("pause");
return 0;
}
DWORD WINAPI Fun_1(LPVOID p)
{
while (true)
{
WaitForSingleObject(h_mutex, INFINITE);
if (counter < 100)
{
++counter;
std::cout << "Thread 1 counting " << counter << "...\n";
ReleaseMutex(h_mutex);
}
else
{
ReleaseMutex(h_mutex);
break;
}
}
return 0;
}
DWORD WINAPI Fun_2(LPVOID p)
{
while (true)
{
WaitForSingleObject(h_mutex, INFINITE);
if (counter < 100)
{
++counter;
std::cout << "Thread 2 counting " << counter << "...\n";
ReleaseMutex(h_mutex);
}
else
{
ReleaseMutex(h_mutex);
break;
}
}
return 0;
}
4、 事件(Event ) :通过通知操作的方式来保持线程的同步,还可以方便实现对多个线程的优先级比较的操作
Event object 通常使用于 overlapped I/O,或用来设计某些自定
义的同步对象。它是:
一个核心对象。
可是用来实现线程的互斥与同步。
可以具名,因此可以被其他进程开启。
在跨进程中使用时,如果拥有信号量的线程意外结束,其它进程也不会受到通知。
vc下实现:
/*
事件对象是一种内核对象,用户在程序中使用内核对象的有无信号状态来实现线程的同步。使用事件对象的步骤可概括如下:
1.创建事件对象,函数原型为:
HANDLE WINAPI CreateEvent(
_In_opt_ LPSECURITY_ATTRIBUTES lpEventAttributes,
_In_ BOOL bManualReset,
_In_ BOOL bInitialState,
_In_opt_ LPCTSTR lpName
);
如果该函数调用成功,则返回新创建的事件对象,否则返回NULL。函数参数的含义如下:
-lpEventAttributes:表示创建的事件对象的安全属性,若设为NULL则表示该程序使用的是默认安全属性。
-bManualReset:表示所创建的事件对象是人工重置还是自动重置。若设为true,则表示使用人工重置,在调用线程获得事件对象所有权后用户要显式地调用ResetEvent()将事件对象设置为无信号状态。
-bInitialState:表示事件对象的初始状态。如果为true,则表示该事件对象初始时为有信号状态,则线程可以使用事件对象。
-lpName:表示事件对象的名称,若为NULL,则表示创建的是匿名事件对象。
2.若事件对象初始状态设置为无信号,则需调用SetEvent(HANDLE hEvent)将其设置为有信号状态。ResetEvent(HANDLE hEvent)则用于将事件对象设置为无信号状态。
3.线程通过调用WaitForSingleObject()主动请求事件对象,该函数原型如下:
DWORD WINAPI WaitForSingleObject(
_In_ HANDLE hHandle,
_In_ DWORD dwMilliseconds
);
该函数将在用户指定的事件对象上等待。如果事件对象处于有信号状态,函数将返回。否则函数将一直等待,直到用户所指定的事件到达。
*/
#if 0
#include <windows.h>
#include <iostream>
DWORD WINAPI Fun_1(LPVOID p);
DWORD WINAPI Fun_2(LPVOID p);
HANDLE h_event;
unsigned int counter = 0;
int main()
{
h_event = CreateEvent(nullptr, true, false, nullptr);
SetEvent(h_event);
HANDLE h1 = CreateThread(nullptr, 0, Fun_1, nullptr, 0, nullptr);
std::cout << "Thread 1 started...\n";
HANDLE h2 = CreateThread(nullptr, 0, Fun_2, nullptr, 0, nullptr);
std::cout << "Thread 2 started...\n";
CloseHandle(h1);
CloseHandle(h2);
//
system("pause");
return 0;
}
DWORD WINAPI Fun_1(LPVOID p)
{
while (true)
{
WaitForSingleObject(h_event, INFINITE);
ResetEvent(h_event);
if (counter < 100)
{
++counter;
std::cout << "Thread 1 counting " << counter << "...\n";
SetEvent(h_event);
}
else
{
SetEvent(h_event);
break;
}
}
return 0;
}
DWORD WINAPI Fun_2(LPVOID p)
{
while (true)
{
WaitForSingleObject(h_event, INFINITE);
ResetEvent(h_event);
if (counter < 100)
{
++counter;
std::cout << "Thread 2 counting " << counter << "...\n";
SetEvent(h_event);
}
else
{
SetEvent(h_event);
break;
}
}
return 0;
}
#endif
就使用效率来说,临界区的效率是最高的,因为它不是内核对象,而其它的三个都是核心对象,要借助操作系统来实现,效率相对来说就比较低。但如果要跨进程使用还是要用到互斥量、事件对象和信号量。