Windows 中的线程同步方法分类
Windows 操作系统的运行方式(程序运行方式)是 “双模式操作”(Dual-mode Operation)。这意味着 Windows 在运行过程中存在如下两种模式。
① 用户模式: 运行应用程序的基本模式,禁止访问物理设备,而且会限制访问的内存区域。
② 内核模式: 操作系统运行时的模式,不仅不会限制访问的内存区域,而且访问的硬件设备也不会受限。
(一)用户模式同步
用户模式同步即无需操作系统的帮助而在应用程序级别进行的同步。用户模式同步最大的优点是——速度快。 但因为这种同步方法不会借助操作系统的力量,其功能上存在一定局限性。
(二)内核模式同步
内核模式同步是通过操作系统的帮助完成的同步,比用户模式同步提供的功能更多,而且可以指定超时,防止产生死锁。 与此同时,由于无法避免用户模式和内核模式之间的切换,所以性能上会受到一定影响。
用户模式同步的方法---基于 CRITICAL_SECTION
的同步
基于 CRITICAL_SECTION
的同步是一种用户模式同步的方法。使用 CRITICAL_SECTION
的同步主要涉及以下几个步骤:
(一)初始化关键区
在使用 CRITICAL_SECTION
之前,必须先初始化它。使用 InitializeCriticalSection
函数进行初始化:
# include <windows.h>
void InitializeCriticalSection(
LPCRITICAL_SECTION lpCriticalSection
);
lpCriticalSection
: 指向CRITICAL_SECTION
结构的指针,LPCRITICAL_SECTION
是CRITICAL_SECTION
指针类型。
(二)进入关键区
当一个线程希望访问受保护的共享资源时,它需要进入临界区。EnterCriticalSection
函数将使线程尝试进入,如果其他线程已经进入了临界区,则调用线程会等待,直到临界区被释放。
# include <windows.h>
void EnterCriticalSection(
LPCRITICAL_SECTION lpCriticalSection
);
(三)离开关键区
当线程完成对共享资源的访问后,必须调用 LeaveCriticalSection
释放临界区,允许其他线程进入。
# include <windows.h>
void LeaveCriticalSection(
LPCRITICAL_SECTION lpCriticalSection
);
(四)删除关键区
当不再需要临界区时,可以调用 DeleteCriticalSection
来释放其资源。
# include <windows.h>
void DeleteCriticalSection(
LPCRITICAL_SECTION lpCriticalSection
);
需要注意,DeleteCriticalSection
并不是销毁 CRITICAL_SECTION
对象的函数。该函数的作用是销毁 CRITICAL_SECTION
对象使用过的(相关的)资源。
(五)示例程序
#include <stdio.h>
#include <windows.h>
#include <process.h>
#define NUM_THREAD 50
unsigned WINAPI threadInc(void * arg);
unsigned WINAPI threadDes(void * arg);
long long num=0;
CRITICAL_SECTION cs;
int main(int argc, char *argv[])
{
HANDLE tHandles[NUM_THREAD];
int i;
InitializeCriticalSection(&cs);
for(i=0; i<NUM_THREAD; i++)
{
if(i%2)
tHandles[i]=(HANDLE)_beginthreadex(NULL, 0, threadInc, NULL, 0, NULL);
else
tHandles[i]=(HANDLE)_beginthreadex(NULL, 0, threadDes, NULL, 0, NULL);
}
WaitForMultipleObjects(NUM_THREAD, tHandles, TRUE, INFINITE);
DeleteCriticalSection(&cs);
printf("result: %lld \n", num);
return 0;
}
unsigned WINAPI threadInc(void * arg)
{
int i;
EnterCriticalSection(&cs);
for(i=0; i<50000000; i++)
num+=1;
LeaveCriticalSection(&cs);
return 0;
}
unsigned WINAPI threadDes(void * arg)
{
int i;
EnterCriticalSection(&cs);
for(i=0; i<50000000; i++)
num-=1;
LeaveCriticalSection(&cs);
return 0;
}
内核模式的同步方法
典型的内核模式同步方法有基于事件(Event)、信号量、互斥量等内核对象的同步。
(一)基于互斥量(Multual Exclusion)对象的同步
基于互斥量的同步步骤如下:
(1)CreateMutex
CreateMutex
用于创建或打开一个互斥量对象。
HANDLE CreateMutex(
LPSECURITY_ATTRIBUTES lpMutexAttributes, // 安全属性
BOOL bInitialOwner, // 初始拥有权
LPCSTR lpName // 互斥量名称
);
-
lpMutexAttributes: 指向
SECURITY_ATTRIBUTES
结构的指针,指定互斥量的安全描述符。如果为NULL
,则互斥量对象将具有默认的安全属性。 -
bInitialOwner: 如果为
TRUE
,创建互斥量的线程将拥有互斥量,同时进入 non-signaled 状态。如果为FALSE
,则创建出的互斥量不属于任何线程,此时状态为 signaled。 -
lpName: 互斥量的名称。如果为
NULL
,则创建无名互斥量。命名互斥量可以用于跨进程同步。 -
返回值: 成功时返回互斥量对象的句柄,失败时返回
NULL
。
从上述参数中可以看到,如果互斥量对象不属于任何拥有者,则将进入 signaled 状态,利用该特点可以进行同步。
(2)WaitForSingleObject
WaitForSingleObject
函数等待指定的对象(如互斥量、事件或信号量)的状态变为有信号状态。
DWORD WaitForSingleObject(
HANDLE hHandle, // 对象句柄
DWORD dwMilliseconds // 等待时间
);
-
hHandle: 对象的句柄,如互斥量、事件或信号量的句柄。
-
dwMilliseconds: 等待时间,以毫秒为单位。如果传入
INFINITE
,表示无限期等待。 -
返回值: 函数返回以下几个可能的值:
WAIT_OBJECT_0
: 对象处于有信号状态,可以继续执行。WAIT_TIMEOUT
: 超时时间到达,未获取到信号。WAIT_FAILED
: 函数调用失败。
互斥量被某一线程获取时(拥有时)为 non-signaled 状态,释放时(未拥有时)进入 signaled 状态。
WaitForSingleObject
函数的调用结果有下面2种:
① 调用后进入阻塞状态:互斥量对象已被其他线程获取,现处于 non-signaled 状态。
② 调用后直接返回:其他线程未占用互斥量对象,现处于 signaled 状态。
(3)ReleaseMutex
ReleaseMutex
用于释放拥有的互斥量,使其他等待的线程或进程可以继续。
BOOL ReleaseMutex(
HANDLE hMutex // 互斥量句柄
);
(4)CloseHandle
CloseHandle
用于关闭一个打开的对象句柄(如互斥量、线程等)。
BOOL CloseHandle(
HANDLE hObject // 对象句柄
);
(5)示例
#include <stdio.h>
#include <windows.h>
#include <process.h>
#define NUM_THREAD 50
unsigned WINAPI threadInc(void * arg);
unsigned WINAPI threadDes(void * arg);
long long num=0;
HANDLE hMutex;
int main(int argc, char *argv[])
{
HANDLE tHandles[NUM_THREAD];
int i;
hMutex=CreateMutex(NULL, FALSE, NULL);
for(i=0; i<NUM_THREAD; i++)
{
if(i%2)
tHandles[i]=(HANDLE)_beginthreadex(NULL, 0, threadInc, NULL, 0, NULL);
else
tHandles[i]=(HANDLE)_beginthreadex(NULL, 0, threadDes, NULL, 0, NULL);
}
WaitForMultipleObjects(NUM_THREAD, tHandles, TRUE, INFINITE);
CloseHandle(hMutex);
printf("result: %lld \n", num);
return 0;
}
unsigned WINAPI threadInc(void * arg)
{
int i;
WaitForSingleObject(hMutex, INFINITE);
for(i=0; i<50000000; i++)
num+=1;
ReleaseMutex(hMutex);
return 0;
}
unsigned WINAPI threadDes(void * arg)
{
int i;
WaitForSingleObject(hMutex, INFINITE);
for(i=0; i<50000000; i++)
num-=1;
ReleaseMutex(hMutex);
return 0;
}
(二)基于信号量对象的同步
(1)基本使用步骤
-
创建信号量:通过
CreateSemaphore
创建信号量对象,并设置最大资源数量。HANDLE CreateSemaphore( LPSECURITY_ATTRIBUTES lpSemaphoreAttributes, // 安全属性 LONG lInitialCount, // 初始计数 LONG lMaximumCount, // 最大计数 LPCSTR lpName // 信号量名称 );
-
lpSemaphoreAttributes: 安全属性,通常为
NULL
。 -
lInitialCount: 信号量的初始计数,表示当前有多少个资源可以被使用。
-
lMaximumCount: 信号量的最大计数,表示最多有多少个线程可以同时访问资源。
-
lpName: 信号量名称,如果跨进程同步需要为信号量命名,否则为
NULL
。
-
-
等待信号量:使用
WaitForSingleObject
等待信号量计数器为正,以便获取资源。信号量对象的值大于0时成为 signaled 状态,为0时成为 non-signaled 状态。因此,调用
WaitForSingleObject
函数时,信号量大于0的情况才会返回。 -
释放信号量:使用
ReleaseSemaphore
释放资源,增加计数器,允许其他线程获取资源。BOOL ReleaseSemaphore( HANDLE hSemaphore, // 信号量句柄 LONG lReleaseCount, // 释放资源的数量 LPLONG lpPreviousCount // 可选,用于接收释放前的计数器值 );
-
hSemaphore: 信号量句柄。
-
lReleaseCount: 要释放的资源数量。
-
lpPreviousCount: 可选参数,用于存储释放前的信号量计数值,不需要时传递 NULL。
-
-
关闭信号量句柄:在信号量使用完毕后,调用
CloseHandle
关闭信号量句柄。
(2)示例
#include <stdio.h>
#include <windows.h>
#include <process.h>
unsigned WINAPI Read(void * arg);
unsigned WINAPI Accu(void * arg);
static HANDLE semOne;
static HANDLE semTwo;
static int num;
int main(int argc, char *argv[])
{
HANDLE hThread1, hThread2;
semOne=CreateSemaphore(NULL, 0, 1, NULL);
semTwo=CreateSemaphore(NULL, 1, 1, NULL);
hThread1=(HANDLE)_beginthreadex(NULL, 0, Read, NULL, 0, NULL);
hThread2=(HANDLE)_beginthreadex(NULL, 0, Accu, NULL, 0, NULL);
WaitForSingleObject(hThread1, INFINITE);
WaitForSingleObject(hThread2, INFINITE);
CloseHandle(semOne);
CloseHandle(semTwo);
return 0;
}
unsigned WINAPI Read(void * arg)
{
int i;
for(i=0; i<5; i++)
{
fputs("Input num: ", stdout);
WaitForSingleObject(semTwo, INFINITE);
scanf("%d", &num);
ReleaseSemaphore(semOne, 1, NULL);
}
return 0;
}
unsigned WINAPI Accu(void * arg)
{
int sum=0, i;
for(i=0; i<5; i++)
{
WaitForSingleObject(semOne, INFINITE);
sum+=num;
ReleaseSemaphore(semTwo, 1, NULL);
}
printf("Result: %d \n", sum);
return 0;
}
(三)基于事件对象的同步
事件对象的主要特点是可以创建 manual-reset 模式的对象。manual-reset 是指手动重置,它有如下特点:
-
手动重置:当事件被设置为有信号状态后,除非显式调用相关函数将其重置为无信号状态,它会一直保持有信号状态。
-
通知多个线程:如果事件处于有信号状态,所有等待该事件的线程都会被唤醒。 因此,手动重置事件适用于需要同时唤醒多个线程的场景。
(1)创建事件对象
HANDLE CreateEvent(
LPSECURITY_ATTRIBUTES lpEventAttributes, // 安全属性
BOOL bManualReset, // 手动重置事件或自动重置事件
BOOL bInitialState, // 初始状态(有信号或无信号)
LPCSTR lpName // 事件名称
);
-
lpEventAttributes: 安全属性,通常为
NULL
。 -
bManualReset: 如果为
TRUE
,创建手动重置事件;如果为FALSE
,创建自动重置事件。 -
bInitialState: 指定事件的初始状态。如果为
TRUE
,事件最初处于有信号状态;如果为FALSE
,事件最初处于无信号状态。 -
lpName: 事件对象的名称。如果为
NULL
,则创建无名事件对象。
(2)更改事件对象状态
下面两个函数用于明确更改对象状态:
#include <windows.h>
BOOL SetEvent(
HANDLE hEvent // 事件对象的句柄
);
BOOL ResetEvent(
HANDLE hEvent // 事件对象的句柄
);
SetEvent()
将事件对象的状态设置为有信号状态。
ResetEvent()
将事件对象的状态设置为无信号状态(仅适用于手动重置事件)。
(3)示例
下面示例中的2个线程将同时等待输入字符串。
#include <stdio.h>
#include <windows.h>
#include <process.h>
#define STR_LEN 100
unsigned WINAPI NumberOfA(void *arg);
unsigned WINAPI NumberOfOthers(void *arg);
static char str[STR_LEN];
static HANDLE hEvent;
int main(int argc, char *argv[])
{
HANDLE hThread1, hThread2;
hEvent=CreateEvent(NULL, TRUE, FALSE, NULL);
hThread1=(HANDLE)_beginthreadex(NULL, 0, NumberOfA, NULL, 0, NULL);
hThread2=(HANDLE)_beginthreadex(NULL, 0, NumberOfOthers, NULL, 0, NULL);
fputs("Input string: ", stdout);
fgets(str, STR_LEN, stdin);
SetEvent(hEvent);
WaitForSingleObject(hThread1, INFINITE);
WaitForSingleObject(hThread2, INFINITE);
ResetEvent(hEvent);
CloseHandle(hEvent);
return 0;
}
unsigned WINAPI NumberOfA(void *arg)
{
int i, cnt=0;
WaitForSingleObject(hEvent, INFINITE);
for(i=0; str[i]!=0; i++)
{
if(str[i]=='A')
cnt++;
}
printf("Num of A: %d \n", cnt);
return 0;
}
unsigned WINAPI NumberOfOthers(void *arg)
{
int i, cnt=0;
WaitForSingleObject(hEvent, INFINITE);
for(i=0; str[i]!=0; i++)
{
if(str[i]!='A')
cnt++;
}
printf("Num of others: %d \n", cnt-1);
return 0;
}
-
事件对象创建:主线程通过
CreateEvent
创建一个手动重置事件hEvent
,初始状态为无信号,这意味着任何等待这个事件的线程都会阻塞,直到事件被设置为有信号状态。hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
-
线程创建:两个线程被创建:
- Thread 1 (
NumberOfA
) :计算输入字符串中字符'A'
的个数。 - Thread 2 (
NumberOfOthers
) :计算除'A'
之外其他字符的个数。
hThread1 = (HANDLE)_beginthreadex(NULL, 0, NumberOfA, NULL, 0, NULL); hThread2 = (HANDLE)_beginthreadex(NULL, 0, NumberOfOthers, NULL, 0, NULL);
- Thread 1 (
-
用户输入:主线程提示用户输入字符串,并通过
fgets
获取输入。输入完成后,主线程调用SetEvent(hEvent)
,将事件的状态设置为有信号状态,唤醒等待该事件的两个线程。fputs("Input string: ", stdout); fgets(str, STR_LEN, stdin); SetEvent(hEvent); // 触发事件,通知线程可以开始执行
-
线程同步:
- Thread 1 (
NumberOfA
) 和 Thread 2 (NumberOfOthers
) 都通过WaitForSingleObject
等待hEvent
的有信号状态。 - 当事件变为有信号状态时,两个线程都可以继续执行它们的任务(分别统计字符
'A'
和其他字符的数量)。
- Thread 1 (
-
主线程等待:主线程使用
WaitForSingleObject
等待两个线程的执行完成。WaitForSingleObject(hThread1, INFINITE); WaitForSingleObject(hThread2, INFINITE);
-
资源释放:主线程在两个子线程执行完毕后,重置事件并关闭事件对象的句柄。
ResetEvent(hEvent); CloseHandle(hEvent);
Windows 平台下实现多线程服务器端
下面的代码实现多线程聊天服务器端和客户端。
(一)服务器端
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <windows.h>
#include <process.h>
#define BUF_SIZE 100
#define MAX_CLNT 256
unsigned WINAPI HandleClnt(void * arg);
void SendMsg(char * msg, int len);
void ErrorHandling(char * msg);
int clntCnt=0;
SOCKET clntSocks[MAX_CLNT];
HANDLE hMutex;
int main(int argc, char *argv[])
{
WSADATA wsaData;
SOCKET hServSock, hClntSock;
SOCKADDR_IN servAdr, clntAdr;
int clntAdrSz;
HANDLE hThread;
if(argc!=2) {
printf("Usage : %s <port>\n", argv[0]);
exit(1);
}
if(WSAStartup(MAKEWORD(2, 2), &wsaData)!=0)
ErrorHandling("WSAStartup() error!");
hMutex=CreateMutex(NULL, FALSE, NULL);
hServSock=socket(PF_INET, SOCK_STREAM, 0);
memset(&servAdr, 0, sizeof(servAdr));
servAdr.sin_family=AF_INET;
servAdr.sin_addr.s_addr=htonl(INADDR_ANY);
servAdr.sin_port=htons(atoi(argv[1]));
if(bind(hServSock, (SOCKADDR*) &servAdr, sizeof(servAdr))==SOCKET_ERROR)
ErrorHandling("bind() error");
if(listen(hServSock, 5)==SOCKET_ERROR)
ErrorHandling("listen() error");
while(1)
{
clntAdrSz=sizeof(clntAdr);
hClntSock=accept(hServSock, (SOCKADDR*)&clntAdr,&clntAdrSz);
WaitForSingleObject(hMutex, INFINITE);
clntSocks[clntCnt++]=hClntSock;
ReleaseMutex(hMutex);
hThread=
(HANDLE)_beginthreadex(NULL, 0, HandleClnt, (void*)&hClntSock, 0, NULL);
printf("Connected client IP: %s \n", inet_ntoa(clntAdr.sin_addr));
}
closesocket(hServSock);
WSACleanup();
return 0;
}
unsigned WINAPI HandleClnt(void * arg)
{
SOCKET hClntSock=*((SOCKET*)arg);
int strLen=0, i;
char msg[BUF_SIZE];
while((strLen=recv(hClntSock, msg, sizeof(msg), 0))!=0)
SendMsg(msg, strLen);
WaitForSingleObject(hMutex, INFINITE);
for(i=0; i<clntCnt; i++) // remove disconnected client
{
if(hClntSock==clntSocks[i])
{
while(i++<clntCnt-1)
clntSocks[i]=clntSocks[i+1];
break;
}
}
clntCnt--;
ReleaseMutex(hMutex);
closesocket(hClntSock);
return 0;
}
void SendMsg(char * msg, int len) // send to all
{
int i;
WaitForSingleObject(hMutex, INFINITE);
for(i=0; i<clntCnt; i++)
send(clntSocks[i], msg, len, 0);
ReleaseMutex(hMutex);
}
void ErrorHandling(char * msg)
{
fputs(msg, stderr);
fputc('\n', stderr);
exit(1);
}
(二)客户端
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <windows.h>
#include <process.h>
#define BUF_SIZE 100
#define NAME_SIZE 20
unsigned WINAPI SendMsg(void * arg);
unsigned WINAPI RecvMsg(void * arg);
void ErrorHandling(char * msg);
char name[NAME_SIZE]="[DEFAULT]";
char msg[BUF_SIZE];
int main(int argc, char *argv[])
{
WSADATA wsaData;
SOCKET hSock;
SOCKADDR_IN servAdr;
HANDLE hSndThread, hRcvThread;
if(argc!=4) {
printf("Usage : %s <IP> <port> <name>\n", argv[0]);
exit(1);
}
if(WSAStartup(MAKEWORD(2, 2), &wsaData)!=0)
ErrorHandling("WSAStartup() error!");
sprintf(name, "[%s]", argv[3]);
hSock=socket(PF_INET, SOCK_STREAM, 0);
memset(&servAdr, 0, sizeof(servAdr));
servAdr.sin_family=AF_INET;
servAdr.sin_addr.s_addr=inet_addr(argv[1]);
servAdr.sin_port=htons(atoi(argv[2]));
if(connect(hSock, (SOCKADDR*)&servAdr, sizeof(servAdr))==SOCKET_ERROR)
ErrorHandling("connect() error");
hSndThread=
(HANDLE)_beginthreadex(NULL, 0, SendMsg, (void*)&hSock, 0, NULL);
hRcvThread=
(HANDLE)_beginthreadex(NULL, 0, RecvMsg, (void*)&hSock, 0, NULL);
WaitForSingleObject(hSndThread, INFINITE);
WaitForSingleObject(hRcvThread, INFINITE);
closesocket(hSock);
WSACleanup();
return 0;
}
unsigned WINAPI SendMsg(void * arg) // send thread main
{
SOCKET hSock=*((SOCKET*)arg);
char nameMsg[NAME_SIZE+BUF_SIZE];
while(1)
{
fgets(msg, BUF_SIZE, stdin);
if(!strcmp(msg,"q\n")||!strcmp(msg,"Q\n"))
{
closesocket(hSock);
exit(0);
}
sprintf(nameMsg,"%s %s", name, msg);
send(hSock, nameMsg, strlen(nameMsg), 0);
}
return 0;
}
unsigned WINAPI RecvMsg(void * arg) // read thread main
{
int hSock=*((SOCKET*)arg);
char nameMsg[NAME_SIZE+BUF_SIZE];
int strLen;
while(1)
{
strLen=recv(hSock, nameMsg, NAME_SIZE+BUF_SIZE-1, 0);
if(strLen==-1)
return -1;
nameMsg[strLen]=0;
fputs(nameMsg, stdout);
}
return 0;
}
void ErrorHandling(char *msg)
{
fputs(msg, stderr);
fputc('\n', stderr);
exit(1);
}