《C++》基础入门_23——线程同步

线程同步一般有三种实现方法:

  • 互斥对象(CreateMutex),
  • 事件对象(CreateEvent),
  • 关键代码段(CriticalSection)

互斥对象

  • 创建互斥
#ifdef UNICODE
#define CreateMutex  CreateMutexW
#else
#define CreateMutex  CreateMutexA
#endif // !UNICODE
HANDLE
WINAPI
CreateMutexA(
   LPSECURITY_ATTRIBUTES  lpMutexAttributes, // 指向安全属性的指针,NULL表示默认安全性
   BOOL  bInitialOwner, // 初始化互斥对象的所有者,TRUE表示创建这个互斥对象的线程获得该对象所有权,否则该线程将不获得
   LPCTSTR  lpName // 指向互斥对象名的指针,NULL表示匿名对象,如果存在命名则下次调用则GetLastError返回ERROR_ALREADY_EXISTS
);
//返回值:返回创建互斥对象的句柄
HANDLE
WINAPI
CreateMutexW(
    _In_opt_ LPSECURITY_ATTRIBUTES lpMutexAttributes,
    _In_ BOOL bInitialOwner,
    _In_opt_ LPCWSTR lpName
    );

  • 互斥等待
    调用该函数后,函数会一直等待,只有在以下两种情况才会返回
    1.指定对象变成有信号状态
    2.指定的等待时间间隔已过
DWORD
WINAPI
WaitForSingleObject(  
     HANDLE hHandle,
     DWORD dwMilliseconds
    );
     // 使内部计数器+1
  • 释放互斥对象
BOOL
WINAPI
ReleaseMutex(
    _In_ HANDLE hMutex
    );
//释放指定对象的所有权,使内部计数器-1
  • 实例:
#include 
#include 
using namespace std;

typedef struct  _STRUCT_DATA_
{
	int id; //用于标识出票id
	int tickets;
}_DATA, *_pDATA;

HANDLE g_hMutex;
DWORD WINAPI Fun1(LPVOID lpParam);
DWORD WINAPI Fun2(LPVOID lpParam);

void main()
{
	HANDLE hThread1;
	HANDLE hThread2;

	_DATA stru_data;
	stru_data.id = 0;
	stru_data.tickets = 20;

	g_hMutex = CreateMutex(NULL, FALSE, L"Ticket");
	if (g_hMutex)
	{
		if (ERROR_ALREADY_EXISTS == GetLastError())
		{
			cout << "the instance is exist!" << endl;
			return;
		}
	}

	hThread1 = CreateThread(NULL, 0, Fun1, &stru_data, 0, NULL);
	hThread2 = CreateThread(NULL, 0, Fun2, &stru_data, 0, NULL);

	CloseHandle(hThread1);
	CloseHandle(hThread2);

	Sleep(4000);
}



DWORD WINAPI Fun1(LPVOID lpParam)
{
	_pDATA data = (_pDATA)lpParam;
	while (TRUE)
	{
		WaitForSingleObject(g_hMutex, INFINITE);
		if (data->tickets > 0)
		{
			Sleep(1);
			cout << "fun1: " << data->id++ ;
			cout << "thread 1:sell ticket: " << data->tickets-- << endl;
		}
		else
			break;
		ReleaseMutex(g_hMutex);
	}
	return 0;
}



DWORD WINAPI Fun2(LPVOID lpParam)
{
	_pDATA data = (_pDATA)lpParam;
	while (TRUE)
	{
		WaitForSingleObject(g_hMutex, INFINITE);
		if (data->tickets > 0)
		{
			Sleep(1);
			cout << "fun2: " << data->id++ ;
			cout << "thread 2:sell  ticket: " << data->tickets-- << endl;
		}
		else
			break;
		ReleaseMutex(g_hMutex);
	}
	return 0;
}

《C++》基础入门_23——线程同步_第1张图片

事件对象

事件对象为立即访问,一旦事件对象被设置为有信号 ,立刻会被其余线程访问!能实现实时监听

  • 创建事件
#ifdef UNICODE
#define CreateEvent  CreateEventW
#else
#define CreateEvent  CreateEventA
#endif
HANDLE
WINAPI
CreateEventA(
     LPSECURITY_ATTRIBUTES lpEventAttributes,
     BOOL bManualReset,
     BOOL bInitialState,
     LPCSTR lpName
    );
HANDLE
WINAPI
CreateEventW(
     LPSECURITY_ATTRIBUTES lpEventAttributes, // 安全属性
     BOOL bManualReset, //复位方式,TRUE表示人工重置(需要调用ResetEvent将事件置为无信号状态),FALSE表示自动重置(线程得到所有权后立即置为无信号状态)
     BOOL bInitialState,// 初始状态,TRUE为有信号状态,FALSE为无信号状态
     LPCWSTR lpName     //对象名称
    );
  • 把指定事件对象设置为有信号状态
BOOL
WINAPI
SetEvent(
    _In_ HANDLE hEvent
    );

  • 把指定事件对象设置为无信号状态
BOOL
WINAPI
ResetEvent(
    _In_ HANDLE hEvent
    );
  • 实例
#include 
#include 
using namespace std;

typedef struct  _STRUCT_DATA_
{
	int id; //用于标识出票id
	int tickets;
}_DATA, *_pDATA;

HANDLE g_hEvent;
DWORD WINAPI Fun1(LPVOID lpParam);
DWORD WINAPI Fun2(LPVOID lpParam);

void main()
{
	HANDLE hThread1;
	HANDLE hThread2;

	_DATA stru_data;
	stru_data.id = 0;
	stru_data.tickets = 20;

	g_hEvent = CreateEvent(NULL, FALSE, FALSE, L"Ticket");
	if (g_hEvent) { 
		if (ERROR_ALREADY_EXISTS == GetLastError()) { 
			cout << "the instance is exist!" << endl;			
			return; 
		} 
	}
	
	hThread1 = CreateThread(NULL, 0, Fun1, &stru_data, 0, NULL);
	hThread2 = CreateThread(NULL, 0, Fun2, &stru_data, 0, NULL);

	CloseHandle(hThread1);
	CloseHandle(hThread2);

	SetEvent(g_hEvent);
	Sleep(4000);
	CloseHandle(g_hEvent);
}



DWORD WINAPI Fun1(LPVOID lpParam)
{
	_pDATA data = (_pDATA)lpParam;
	while (TRUE)
	{
		WaitForSingleObject(g_hEvent, INFINITE);
		if (data->tickets > 0)
		{
			Sleep(1);
			cout << "fun1: " << data->id++ ;
			cout << "thread 1:sell ticket: " << data->tickets-- << endl;
			SetEvent(g_hEvent);
		}
		else {
			SetEvent(g_hEvent);
			break;
		}
	}
	return 0;
}

DWORD WINAPI Fun2(LPVOID lpParam)
{
	_pDATA data = (_pDATA)lpParam;
	while (TRUE)
	{
		WaitForSingleObject(g_hEvent, INFINITE);
		if (data->tickets > 0)
		{
			Sleep(1);
			cout << "fun2: " << data->id++ ;
			cout << "thread 2:sell  ticket: " << data->tickets-- << endl;
			SetEvent(g_hEvent);
		}
		else {
			SetEvent(g_hEvent);
			break;
		}
	}
	return 0;
}

《C++》基础入门_23——线程同步_第2张图片
使用事件对象实现线程间同步需要注意区分人工重置事件对象和自动重置事件对象。

  • 当人工重置事件对象得到通知时,等待该事件对象的所有线程均变为可调度线程。在一个线程得到该事件对象之后,操作系统并不会将该事件设置为无信号状态,需要显示调用ResetEvent函数将其设置为无信号状态,否则该对象会一直是有信号状态。
  • 当一个自动重置的事件对象得到通知时,等待该事件对象的线程中只有一个线程变为可调度,同时操作系统会将该事件对象设置无信号状态。这样,当对所保护的代码执行完成后,需要调用SetEvent函数将该事件对象设置为有信号状态。

关键代码段

关键代码段,也称为临界区,工作在用户方式下。它是指一个小代码段,在代码能够执行前,它必须独占对某些资源的访问权。

多个线程操作相同的数据时,一般是需要按顺序访问的,否则会引导数据错乱,无法控制数据,变成随机变量。为解决这个问题,就需要引入互斥变量,让每个线程都按顺序地访问变量。这样就需要使用EnterCriticalSection和LeaveCriticalSection函数。

  • 定义全局的锁CRITICAL_SECTION

没有任何资源被“锁定”,CRITICAL_SECTION这个东东不是针对于资源的,而是针对于不同线程间的代码段的!我们能够用它来进 行所谓资源的“锁定”,其实是因为我们在任何访问共享资源的地方都加入了EnterCriticalSection和 LeaveCriticalSection语句,使得同一时间只能够有一个线程的代码段访问到该共享资源而已(其它想访问该资源的代码段不得不等待)。

CRITICAL_SECTION cs;//可以理解为锁定一个资源
  • 初始化临界区
VOID
WINAPI
InitializeCriticalSection(
   LPCRITICAL_SECTION lpCriticalSection
 );
  • 进入临界区
    加锁 接下来的代码处理过程中不允许其他线程进行操作,除非遇到LeaveCriticalSection
VOID
WINAPI
EnterCriticalSection(
    _Inout_ LPCRITICAL_SECTION lpCriticalSection
    );
  • 离开临界区
    解锁 到EnterCriticalSection之间代码资源已经释放了,其他线程可以进行操作
    https://baike.baidu.com/item/EnterCriticalSection/1945472
VOID
WINAPI
LeaveCriticalSection(
    _Inout_ LPCRITICAL_SECTION lpCriticalSection
    );
  • 删除临界区
VOID
WINAPI
DeleteCriticalSection(
    _Inout_ LPCRITICAL_SECTION lpCriticalSection
    );

  • 实例
#include 
#include 
using namespace std;

typedef struct  _STRUCT_DATA_
{
	int id; //用于标识出票id
	int tickets;
}_DATA, *_pDATA;

CRITICAL_SECTION g_cs; 

DWORD WINAPI Fun1(LPVOID lpParam);
DWORD WINAPI Fun2(LPVOID lpParam);

void main()
{
	HANDLE hThread1;
	HANDLE hThread2;

	_DATA stru_data;
	stru_data.id = 0;
	stru_data.tickets = 100;

	
	hThread1 = CreateThread(NULL, 0, Fun1, &stru_data, 0, NULL);
	hThread2 = CreateThread(NULL, 0, Fun2, &stru_data, 0, NULL);
	CloseHandle(hThread1);
	CloseHandle(hThread2);

	InitializeCriticalSection(&g_cs);
	Sleep(4000);
	LeaveCriticalSection(&g_cs);
}

DWORD WINAPI Fun1(LPVOID lpParam)
{
	_pDATA data = (_pDATA)lpParam;
	while (TRUE)
	{
		EnterCriticalSection(&g_cs);
		if (data->tickets > 0)
		{
			Sleep(1);
			cout << "fun1: " << data->id++ <<endl;
			cout << "thread 1:sell ticket: " << data->tickets-- << endl;
			LeaveCriticalSection(&g_cs);
		}
		else {
			LeaveCriticalSection(&g_cs);
			break;
		}
	}
	return 0;
}

DWORD WINAPI Fun2(LPVOID lpParam)
{
	_pDATA data = (_pDATA)lpParam;
	while (TRUE)
	{  
		EnterCriticalSection(&g_cs);
		if (data->tickets > 0)
		{
			Sleep(1);
			cout << "fun2: " << data->id++ <<endl;
			cout << "thread 2:sell  ticket: " << data->tickets-- << endl;
			LeaveCriticalSection(&g_cs);
		}
		else {
			LeaveCriticalSection(&g_cs);
			break;
		}
	}
	return 0;
}

三种方式之间的区别:

  • 互斥对象和事件对象都属于内核对象,利用内核对象进行线程同步时,速度较慢,但利用互斥对象和事件对象这样的内核对象,可以在多个进程中的各个线程间进行同步。
  • 关键代码段工作在用户方式下,同步速度较快,但在使用关键代码段时,很容易进入死锁状态,因为在等待进入关键代码段时无法确定超时值。

使用建议:
在MFC程序中,可以在类的构造函数中调用InitializeCriticalSection函数,在析构函数中调用DeleteCriticalSection函数,在所需保护的代码前面调用EnterCriticalSection函数,在访问完所需保护的资源后,调用LeaveCriticalSection函数。

原文:https://blog.csdn.net/xuanyin235/article/details/77684077
参考:
https://blog.csdn.net/zxxssdsd/article/details/17304429

你可能感兴趣的:(多线程,C++)