经典线程同步互斥问题在windows下的各种解法

时间:2021-07-19 17:42:13

一、引言

这篇文章记录是来源于Windows多线程系列的学习笔记,表为原创,实为转载文章,只是为了让自己学的明白,稍微整理了下而已,在此向“MoreWindows”大神致敬!

二、问题所在

1.题目要求

主线程启动10个子线程并将表示子线程序号的变量地址作为参数传递给子线程。子线程接收参数 -> sleep(50) -> 全局变量++ -> sleep(0) -> 输出参数和全局变量。

要求:
1.子线程输出的线程序号不能重复。

2.全局变量的输出必须递增。

2.题目解析

分析下这个问题的考察点,主要考察点有二个:

1.主线程创建子线程并传入一个指向变量地址的指针作参数,由于线程启动须要花费一定的时间,所以在子线程根据这个指针访问并保存数据前,主线程应等待子线程保存完毕后才能改动该参数并启动下一个线程。这涉及到主线程与子线程之间的同步

2.子线程之间会互斥的改动和输出全局变量。要求全局变量的输出必须递增。这涉及到各子线程间的互斥

3.程序基本框架

//经典线程同步互斥问题 
#include <stdio.h>
#include <process.h>
#include <windows.h>

long g_nNum; //全局资源
unsigned int __stdcall Fun(void *pPM); //线程函数
const int THREAD_NUM = 10; //子线程个数

int main()
{
g_nNum = 0;
HANDLE handle[THREAD_NUM];

int i = 0;
while (i < THREAD_NUM)
{
handle[i] = (HANDLE)_beginthreadex(NULL, 0, Fun, &i, 0, NULL);
i++;//等子线程接收到参数时主线程可能改变了这个i的值
}
//保证子线程已全部运行结束
WaitForMultipleObjects(THREAD_NUM, handle, TRUE, INFINITE);
return 0;
}

unsigned int __stdcall Fun(void *pPM)
{
//由于创建线程是要一定的开销的,所以新线程并不能第一时间执行到这来
int nThreadNum = *(int *)pPM; //子线程获取参数
Sleep(50);//some work should to do
g_nNum++; //处理全局资源
Sleep(0);//some work should to do
printf("线程编号为%d 全局资源值为%d\n", nThreadNum, g_nNum);
return 0;
}

三、经典同步问题–关键段CS

1.关键段CRITICAL_SECTION介绍

关键段CRITICAL_SECTION一共就四个函数,使用很是方便。下面是这四个函数的原型和使用说明。

函数功能:初始化

函数原型:

void InitializeCriticalSection(LPCRITICAL_SECTIONlpCriticalSection);

函数说明:定义关键段变量后必须先初始化。

函数功能:销毁

函数原型:

void DeleteCriticalSection(LPCRITICAL_SECTIONlpCriticalSection);

函数说明:用完之后记得销毁。

函数功能:进入关键区域

函数原型:

void EnterCriticalSection(LPCRITICAL_SECTIONlpCriticalSection);

函数说明:系统保证各线程互斥的进入关键区域。

函数功能:离开关关键区域

函数原型:

void LeaveCriticalSection(LPCRITICAL_SECTIONlpCriticalSection);

2.关键段CS解决代码

#include <stdio.h>
#include <process.h>
#include <windows.h>

long g_nNum;
unsigned int __stdcall Fun(void *pPM);
const int THREAD_NUM = 10;
// 关键段变量声明
CRITICAL_SECTION g_csThreadParameter, g_csThreadCode;

int main()
{
printf(" 经典线程同步 关键段\n");
printf("-- by MoreWindows( http://blog.csdn.net/MoreWindows ) --\n\n");

// 关键段初始化
InitializeCriticalSection(&g_csThreadParameter);
InitializeCriticalSection(&g_csThreadCode);

HANDLE handle[THREAD_NUM];
g_nNum = 0;
int i = 0;
while (i < THREAD_NUM) {
EnterCriticalSection(&g_csThreadParameter); // 进入子线程序号关键区域
handle[i] = (HANDLE)_beginthreadex(NULL, 0, Fun, &i, 0, NULL);
++i;
}

WaitForMultipleObjects(THREAD_NUM, handle, TRUE, INFINITE);

DeleteCriticalSection(&g_csThreadCode);
DeleteCriticalSection(&g_csThreadParameter);
Sleep(10000);
return 0;
}

unsigned int __stdcall Fun(void *pPM)
{
int nThreadNum = *(int *)pPM;
LeaveCriticalSection(&g_csThreadParameter); // 离开字线程序号关键区域

Sleep(50);

EnterCriticalSection(&g_csThreadCode); // 进入各子线程互斥区域
g_nNum++;
Sleep(0);
printf("线程编号为%d 全局资源值为%d\n", nThreadNum, g_nNum);
LeaveCriticalSection(&g_csThreadCode);
return 0;
}

3.关键段总结

可以将关键段比作旅馆的房卡,调用EnterCriticalSection()即申请房卡,得到房卡后自己当然是可以多次进出房间的,在你调用LeaveCriticalSection()交出房卡之前,别人自然是无法进入该房间。

关键段可以用于线程间的互斥,但不可以用于同步。

最终总结下关键段的特性:

1.关键段共初始化化、销毁、进入和离开关键区域四个函数。

2.关键段可以解决线程的互斥问题,但因为具有“线程所有权”,所以无法解决同步问题。

3.推荐关键段与旋转锁配合使用。

四、经典同步问题–事件Event

1.事件Event介绍

事件Event实际上是个内核对象,它的使用非常方便。下面列出一些常用的函数。

第一个 CreateEvent

函数功能:创建事件

函数原型:

HANDLECreateEvent(

LPSECURITY_ATTRIBUTESlpEventAttributes,

BOOLbManualReset,

BOOLbInitialState,

LPCTSTRlpName

)
;

函数说明:

第一个参数表示安全控制,一般直接传入NULL。

第二个参数确定事件是手动置位还是自动置位,传入TRUE表示手动置位,传入FALSE表示自动置位。如果为自动置位,则对该事件调用WaitForSingleObject()后会自动调用ResetEvent()使事件变成未触发状态。打个小小比方,手动置位事件相当于教室门,教室门一旦打开(被触发),所以有人都可以进入直到老师去关上教室门(事件变成未触发)。自动置位事件就相当于医院里拍X光的房间门,门打开后只能进入一个人,这个人进去后会将门关上,其它人不能进入除非门重新被打开(事件重新被触发)。

第三个参数表示事件的初始状态,传入TRUR表示已触发。

第四个参数表示事件的名称,传入NULL表示匿名事件。

第二个 OpenEvent

函数功能:根据名称获得一个事件句柄。

函数原型:

HANDLEOpenEvent(

DWORDdwDesiredAccess,

BOOLbInheritHandle,

LPCTSTRlpName //名称

);

函数说明:

第一个参数表示访问权限,对事件一般传入EVENT_ALL_ACCESS。详细解释可以查看MSDN文档。

第二个参数表示事件句柄继承性,一般传入TRUE即可。

第三个参数表示名称,不同进程中的各线程可以通过名称来确保它们访问同一个事件。

第三个SetEvent

函数功能:触发事件

函数原型:BOOLSetEvent(HANDLEhEvent);

函数说明:每次触发后,必有一个或多个处于等待状态下的线程变成可调度状态。

第四个ResetEvent

函数功能:将事件设为末触发

函数原型:BOOLResetEvent(HANDLEhEvent);

最后一个事件的清理与销毁

由于事件是内核对象,因此使用CloseHandle()就可以完成清理与销毁了。

2.事件Event代码

#include <stdio.h> 
#include <process.h>
#include <windows.h>
long g_nNum;
unsigned int __stdcall Fun(void *pPM);
const int THREAD_NUM = 10;
//事件与关键段
HANDLE g_hThreadEvent;
CRITICAL_SECTION g_csThreadCode;
int main()
{
printf(" 经典线程同步 事件Event\n");
printf(" -- by MoreWindows( http://blog.csdn.net/MoreWindows ) --\n\n");
//初始化事件和关键段 自动置位,初始无触发的匿名事件
g_hThreadEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
InitializeCriticalSection(&g_csThreadCode);

HANDLE handle[THREAD_NUM];
g_nNum = 0;
int i = 0;
while (i < THREAD_NUM)
{
handle[i] = (HANDLE)_beginthreadex(NULL, 0, Fun, &i, 0, NULL);
WaitForSingleObject(g_hThreadEvent, INFINITE); //等待事件被触发
i++;
}
WaitForMultipleObjects(THREAD_NUM, handle, TRUE, INFINITE);

//销毁事件和关键段
CloseHandle(g_hThreadEvent);
DeleteCriticalSection(&g_csThreadCode);
return 0;
}
unsigned int __stdcall Fun(void *pPM)
{
int nThreadNum = *(int *)pPM;
SetEvent(g_hThreadEvent); //触发事件

Sleep(50);//some work should to do

EnterCriticalSection(&g_csThreadCode);
g_nNum++;
Sleep(0);//some work should to do
printf("线程编号为%d 全局资源值为%d\n", nThreadNum, g_nNum);
LeaveCriticalSection(&g_csThreadCode);
return 0;
}

3.认识PulseEvent事件脉冲

先来看看这个函数的原形:

第五个PulseEvent

函数功能:将事件触发后立即将事件设置为未触发,相当于触发一个事件脉冲。

函数原型:BOOLPulseEvent(HANDLEhEvent);

函数说明:这是一个不常用的事件函数,此函数相当于SetEvent()后立即调用ResetEvent();此时情况可以分为两种:

1.对于手动置位事件,所有正处于等待状态下线程都变成可调度状态。

2.对于自动置位事件,所有正处于等待状态下线程只有一个变成可调度状态。

此后事件是末触发的。该函数不稳定,因为无法预知在调用PulseEvent ()时哪些线程正处于等待状态。

4.事件脉冲PulseEvent代码

#include <stdio.h>
#include <conio.h>
#include <process.h>
#include <windows.h>

HANDLE g_hThreadEvent;
// 快线程
unsigned int __stdcall FastThreadFun(void *pPM)
{
Sleep(10); // 用这个来保证各线程调用等待函数的次序有一定的随机性
printf("%s 启动\n", (PSTR)pPM);
WaitForSingleObject(g_hThreadEvent, INFINITE);
printf("%s 等到事件被触发 顺利结束\n", (PSTR)pPM);
return 0;
}
// 慢线程
unsigned int __stdcall SlowThreadFun(void *pPM)
{
Sleep(100);
printf("%s 启动\n", (PSTR)pPM);
WaitForSingleObject(g_hThreadEvent, INFINITE);
printf("%s 等到事件被触发 顺利结束\n", (PSTR)pPM);
return 0;
}

int main()
{
printf(" 使用PluseEvent()函数\n");
printf(" -- by MoreWindows( http://blog.csdn.net/MoreWindows ) --\n\n");

BOOL bManualReset = FALSE;
// 创建事件 第二个参数手动置位TRUE, 自动置位FALSE
g_hThreadEvent = CreateEvent(NULL, bManualReset, FALSE, NULL);
if (bManualReset == TRUE) {
printf("当前使用手动置位事件\n");
} else {
printf("当前使用自动置位事件\n");
}

char szFastThreadName[5][30] = {"快线程1000", "快线程1001", "快线程1002", "快线程1003", "快线程1004"};
char szSlowThreadName[2][30] = {"慢线程196", "慢线程197"};

int i;
for (i = 0; i < 5; ++i) {
_beginthreadex(NULL, 0, FastThreadFun, szFastThreadName[i], 0, NULL);
}
for (i = 0; i < 2; ++i) {
_beginthreadex(NULL, 0, SlowThreadFun, szSlowThreadName[i], 0, NULL);
}

Sleep(50); // 保证快线程已经全部启动
printf("现在主线程触发一个事件脉冲 - PulseEvent()\n");
PulseEvent(g_hThreadEvent); // 调用PulseEvent()就相当于同时调用下面二句
// SetEvent(g_hThreadEvent);
// ResetEvent(g_hThreadEvent);

Sleep(3000);
printf("时间到,主线程结束运行\n");
CloseHandle(g_hThreadEvent);
return 0;
}

5.事件Event总结

1.事件是内核对象,事件分为手动置位事件和自动置位事件。事件Event内部它包含一个使用计数(所有内核对象都有),一个布尔值表示是手动置位事件还是自动置位事件,另一个布尔值用来表示事件有无触发。

2.事件可以由SetEvent()来触发,由ResetEvent()来设成未触发。还可以由PulseEvent()来发出一个事件脉冲。

3.事件可以解决线程间同步问题,因此也能解决互斥问题。

五、经典同步问题–互斥量Mutex

1.互斥量Mutex简介

互斥量也是一个内核对象,它用来确保一个线程独占一个资源的访问。互斥量与关键段的行为非常相似,并且互斥量可以用于不同进程中的线程互斥访问资源。使用互斥量Mutex主要将用到四个函数:

第一个 CreateMutex

函数功能:创建互斥量(注意与事件Event的创建函数对比)

函数原型:

HANDLECreateMutex(

LPSECURITY_ATTRIBUTESlpMutexAttributes,

BOOLbInitialOwner,

LPCTSTRlpName

)
;

函数说明:

第一个参数表示安全控制,一般直接传入NULL。

第二个参数用来确定互斥量的初始拥有者。如果传入TRUE表示互斥量对象内部会记录创建它的线程的线程ID号并将递归计数设置为1,由于该线程ID非零,所以互斥量处于未触发状态。如果传入FALSE,那么互斥量对象内部的线程ID号将设置为NULL,递归计数设置为0,这意味互斥量不为任何线程占用,处于触发状态。

第三个参数用来设置互斥量的名称,在多个进程中的线程就是通过名称来确保它们访问的是同一个互斥量。

函数访问值:

成功返回一个表示互斥量的句柄,失败返回NULL。

第二个打开互斥量

函数原型:

HANDLEOpenMutex(

DWORDdwDesiredAccess,

BOOLbInheritHandle,

LPCTSTRlpName //名称

);

函数说明:

第一个参数表示访问权限,对互斥量一般传入MUTEX_ALL_ACCESS。详细解释可以查看MSDN文档。

第二个参数表示互斥量句柄继承性,一般传入TRUE即可。

第三个参数表示名称。某一个进程中的线程创建互斥量后,其它进程中的线程就可以通过这个函数来找到这个互斥量。

函数访问值:

成功返回一个表示互斥量的句柄,失败返回NULL。

第三个触发互斥量

函数原型:

BOOLReleaseMutex (HANDLEhMutex)

函数说明:

访问互斥资源前应该要调用等待函数,结束访问时就要调用ReleaseMutex()来表示自己已经结束访问,其它线程可以开始访问了。

最后一个清理互斥量

由于互斥量是内核对象,因此使用CloseHandle()就可以(这一点所有内核对象都一样)。

2.互斥量解决代码

与关键段类似,互斥量也是不能解决线程间的同步问题。

#include <stdio.h>
#include <process.h>
#include <windows.h>

long g_nNum;
unsigned int __stdcall Fun(void *pPM);
const int THREAD_NUM = 10;
// 互斥量与关键段
HANDLE g_hThreadParameter;
CRITICAL_SECTION g_csThreadCode;

int main()
{
printf(" 经典线程同步 互斥量Mutext\n");
printf(" -- by MoreWindows( http://blog.csdn.net/MoreWindows ) --\n\n");

// 初始化互斥量与关键段 第二个参数为TRUE表示互斥量为创建进程所有
g_hThreadParameter = CreateMutex(NULL, FALSE, NULL);
InitializeCriticalSection(&g_csThreadCode);

HANDLE handle[THREAD_NUM];
g_nNum = 0;
int i = 0;
while (i < THREAD_NUM) {
handle[i] = (HANDLE)_beginthreadex(NULL, 0, Fun, &i, 0, NULL);
WaitForSingleObject(g_hThreadParameter, INFINITE); // 等待互斥量被触发
i++;
}
WaitForMultipleObjects(THREAD_NUM, handle, TRUE, INFINITE);

// 销毁互斥量和关键段
CloseHandle(g_hThreadParameter);
DeleteCriticalSection(&g_csThreadCode);
for (i = 0; i < THREAD_NUM; i++) {
CloseHandle(handle[i]);
}
Sleep(10000);
return 0;
}

unsigned int __stdcall Fun(void *pPM)
{
int nThreadNum = *(int *)pPM;
ReleaseMutex(g_hThreadParameter); // 触发互斥量

Sleep(50);

EnterCriticalSection(&g_csThreadCode);
g_nNum++;
Sleep(0);
printf("线程编号为%d 全局资源值为%d\n", nThreadNum, g_nNum);
LeaveCriticalSection(&g_csThreadCode);
return 0;
}

3.互斥量实现两个进程间的资源互斥访问

进程1

#include <stdio.h>
#include <conio.h>
#include <windows.h>

const TCHAR MUTEXT_NAME[] = TEXT("Mutex_MoreWindows");

int main()
{
HANDLE hMutex = CreateMutex(NULL, TRUE, MUTEXT_NAME); // 创建互斥量
printf("互斥量已经创建,现在按任意键触发互斥量\n");
getch();
// exit(0);
ReleaseMutex(hMutex);
printf("互斥量已经触发\n");
CloseHandle(hMutex);
Sleep(10000);
return 0;
}

进程2

#include <stdio.h>
#include <windows.h>

const TCHAR MUTEX_NAME[] = TEXT("Mutex_MoreWindows");

int main()
{
HANDLE hMutex = OpenMutex(MUTEX_ALL_ACCESS, TRUE, MUTEX_NAME); // 打开互斥量
if (NULL == hMutex) {
printf("打开互斥量失败\n");
Sleep(10000);
return 0;
}
printf("等待中...\n");
DWORD dwResult = WaitForSingleObject(hMutex, 20 * 1000); // 等待互斥量被触发
switch (dwResult)
{
case WAIT_ABANDONED:
printf("拥有互斥量的进程意外终止\n");
break;

case WAIT_OBJECT_0:
printf("已经收到信号\n");
break;

case WAIT_TIMEOUT:
printf("信号未在规定的时间内送到\n");
break;
}
CloseHandle(hMutex);
Sleep(10000);
return 0;
}

4.互斥量Mutex总结

最后总结下互斥量Mutex:

1.互斥量是内核对象,它与关键段都有“线程所有权”所以不能用于线程的同步。

2.互斥量能够用于多个进程之间线程互斥问题,并且能完美的解决某进程意外终止所造成的“遗弃”问题。

六、经典同步问题–信号量Semaphore

1.信号量Semaphore简介

信号量Semaphore常用有三个函数,使用很方便。下面是这几个函数的原型和使用说明:
第一个 CreateSemaphore

函数功能:创建信号量

函数原型:

HANDLE CreateSemaphore(

LPSECURITY_ATTRIBUTES lpSemaphoreAttributes,

LONG lInitialCount,

LONG lMaximumCount,

LPCTSTR lpName

);

函数说明:

第一个参数表示安全控制,一般直接传入NULL。

第二个参数表示初始资源数量。

第三个参数表示最大并发数量。

第四个参数表示信号量的名称,传入NULL表示匿名信号量。

第二个 OpenSemaphore

函数功能:打开信号量

函数原型:

HANDLE OpenSemaphore(

DWORD dwDesiredAccess,

BOOL bInheritHandle,

LPCTSTR lpName

);

函数说明:

第一个参数表示访问权限,对一般传入SEMAPHORE_ALL_ACCESS。详细解释可以查看MSDN文档。

第二个参数表示信号量句柄继承性,一般传入TRUE即可。

第三个参数表示名称,不同进程中的各线程可以通过名称来确保它们访问同一个信号量。

第三个 ReleaseSemaphore

函数功能:递增信号量的当前资源计数

函数原型:

BOOL ReleaseSemaphore(

HANDLE hSemaphore,

LONG lReleaseCount,

LPLONG lpPreviousCount

);

函数说明:

第一个参数是信号量的句柄。

第二个参数表示增加个数,必须大于0且不超过最大资源数量。

第三个参数可以用来传出先前的资源计数,设为NULL表示不需要传出。

注意:当前资源数量大于0,表示信号量处于触发,等于0表示资源已经耗尽故信号量处于末触发。在对信号量调用等待函数时,等待函数会检查信号量的当前资源计数,如果大于0(即信号量处于触发状态),减1后返回让调用线程继续执行。一个线程可以多次调用等待函数来减小信号量。

最后一个 信号量的清理与销毁

由于信号量是内核对象,因此使用CloseHandle()就可以完成清理与销毁了。

2.信号量解决代码

信号量可以解决线程之间的同步问题。

#include <stdio.h>
#include <process.h>
#include <windows.h>

long g_nNum;
unsigned int __stdcall Fun(void *pPM);
const int THREAD_NUM = 10;
// 信号量与关键段
HANDLE g_hThreadParameter;
CRITICAL_SECTION g_csThreadCode;

int main()
{
printf(" 经典线程同步 信号量Semaphore\n");
printf(" -- by MoreWindows( http://blog.csdn.net/MoreWindows ) --\n\n");

// 初始化信号量和关键段
g_hThreadParameter = CreateSemaphore(NULL, 0, 1, NULL); // 当前0个资源,最大允许1个同时访问
InitializeCriticalSection(&g_csThreadCode);

HANDLE handle[THREAD_NUM];
g_nNum = 0;
int i = 0;
while (i < THREAD_NUM) {
handle[i] = (HANDLE)_beginthreadex(NULL, 0, Fun, &i, 0, NULL);
WaitForSingleObject(g_hThreadParameter, INFINITE); // 等待信号量 > 0
++i;
}
WaitForMultipleObjects(THREAD_NUM, handle, TRUE, INFINITE);

// 销毁信号量和关键段
DeleteCriticalSection(&g_csThreadCode);
CloseHandle(g_hThreadParameter);
for (i = 0; i < THREAD_NUM; i++) {
CloseHandle(handle[i]);
}
Sleep(10000);
return 0;
}

unsigned int __stdcall Fun(void *pPM)
{
int nThreadNum = *(int *)pPM;
ReleaseSemaphore(g_hThreadParameter, 1, NULL); // 信号量++

Sleep(50);

EnterCriticalSection(&g_csThreadCode);
++g_nNum;
Sleep(0);
printf("线程编号为%d 全局资源值为%d\n", nThreadNum, g_nNum);
LeaveCriticalSection(&g_csThreadCode);
return 0;
}

3.信号量Semaphore总结

由于信号量可以计算资源当前剩余量并根据当前剩余量与零比较来决定信号量是处于触发状态或是未触发状态,因此信号量的应用范围相当广泛。