线程同步 | 使用互斥对象和临界区

文章目录

      • 1.线程同步
      • 2.利用互斥对象实现线程同步
      • 3.利用临界区实现线程同步

1.线程同步

火车站售票系统模拟

#include 
#include 

DWORD WINAPI ThreadProc1( LPVOID lpParameter); //售票窗口1
DWORD WINAPI ThreadProc2( LPVOID lpParameter); //售票窗口2

int tickets = 1; //票号,从第一张票票号为1

void main()
{
	HANDLE hThread1;
	HANDLE hThread2;
	hThread1 = CreateThread(NULL,0,ThreadProc1,NULL,0,NULL);
	hThread2 = CreateThread(NULL,0,ThreadProc2,NULL,0,NULL);
	CloseHandle(hThread1);
	CloseHandle(hThread2);

	Sleep(5000);
}
//线程1的入口函数(售票窗口1)
DWORD WINAPI ThreadProc1(LPVOID lpParameter)
{
	while (TRUE) 
	{
		if (tickets <= 100)//卖超过100张就结束
		{
   		         cout << "thread1 sell ticket : " << tickets ++ << endl;
		}
		else
		        break;
	}
	return 0;
}
//线程2的入口函数(售票窗口2)
DWORD WINAPI ThreadProc2(LPVOID lpParameter)
{
	while (TRUE) 
	{
		if (tickets <= 100)
		{
   		         cout << "thread2 sell ticket : " << tickets ++ << endl;
		}
		else
		        break;
	}
	return 0;
}

程序隐患:

事实上,上述程序存在隐患,比如以下情况:

当tickets为100时,线程1函数进入 if 语句块后,正好该线程的时间片到了,操作系统就会选择线程2让其进行,而这时变量tickets的值还没有加1,因此这时变量tickets的值仍是100,线程2进入它的if 语句块中,于是线程2执行卖票操作,打印票号100,然后tickets变量加1,其值变为101。如果当线程2执行完成上述操作之后,正好又轮到线程1开始运行了。而这时线程1将从原先的if 语句块开始执行,于是它输出当前的票号,而此时tickets变量的值已经是101了,也就是说,我们会看到线程1卖了号码为101的票。显然这种情况时不允许的。

上述问题的出现主要原因就是两个线程访问了同一个全局变量:ticket。为了避免这种问题的发生,就要求在多个线程之间进行一个同步处理,保证一个线程访问共享资源时,其他线程不能访问该资源。

线程需要在下面两种情况下互相通信,以实现同步:

  1. 当有多个线程访问共享资源,而不使资源被破坏时。
  2. 当一个线程需要将某个任务已经完成的情况通知另一个或多个线程时。

线程同步原理

线程同步 | 使用互斥对象和临界区_第1张图片

2.利用互斥对象实现线程同步

互斥对象(Mutex)属于内核对象,它能够确保线程拥有对单个资源的互斥访问权。 创建互斥对象需要调用函数:CreateMutex

互斥对象就相当于上图的那把锁。

HANDLE CreateMutex(
     LPSECURITY_ATTRIBUTES lpMutexAttributes, //通常设为NULL,该线程使用默认的安全性
     BOOL bInitialOwner,//可设为FALSE,创建这个互斥对象的线程不获得其所有权。
     LPCTSTR lpName//指定互斥对象的名称。如果此参数为NULL,则创建一个匿名的互斥对象。

); 

获得互斥对象的所有权

线程必须主动请求共享对象的使用权才能获得该所有权,这可以通过调用WaitForSingleObject函数实现。

DWORD WatiForSingleObject(HANDLE hHandle, DWORD dwMillisecond); //(锁定+等待)

HANDLE hHandle
所请求对象的句柄。本例为互斥对象句柄:hMutex。一旦互斥对象处于有信号状态,则该函数返回,接着,操作系统会将这个互斥对象设为无信号状态。如果该互斥对象处于无信号状态,则该函数会一直等待,这样会暂停线程的执行。

DWORD dwMillisecond
指定等待的时间,如果指定的时间间隔已过,即使所请求的对象处于无信号状态,该函数也返回。如果该参数为0,该函数立即返回。如果该参数为INFINTE,则该函数永远等待,直到互斥对象处于有信号状态才返回。

释放互斥对象的所有权

当线程对共享资源访问结束后,应释放互斥对象的所有权,让该对象处于有信号状态。这时需要调用函数:ReleaseMutex

BOOL ReleaseMutex(HANDLE hMutex);//(解锁) 

完整的卖票程序代码:

#include 
#include 

DWORD WINAPI ThreadProc1( LPVOID lpParameter); 
DWORD WINAPI ThreadProc2( LPVOID lpParameter); 

int tickets = 1;
HANDLE hMutex;//锁定义为全局对象
void main()
{
	HANDLE hThread1;
	HANDLE hThread2;
	hThread1 = CreateThread(NULL,0,ThreadProc1,NULL,0,NULL);
	hThread2 = CreateThread(NULL,0,ThreadProc2,NULL,0,NULL);
	CloseHandle(hThread1);
	CloseHandle(hThread2);
	hMutex = CreateMutex(NULL, FALSE, NULL);//创建锁的对象不具有锁的所有权,注意第二个参数一定要设为False
	Sleep(5000);
}
//线程1的入口函数(售票窗口1)
DWORD WINAPI ThreadProc1(LPVOID lpParameter)
{
	while (TRUE) 
	{
		WaitForSingleObject(hMutex,INFINITE);//(锁上 + 等待)
		if (tickets <= 100)
		{
			Sleep(1);
			cout << "thread1 sell ticket : " << tickets ++ << endl;
		}
		else
			break;
		ReleaseMutex(hMutex);//(解锁)
	}
	return 0;
}
//线程2的入口函数(售票窗口2)
DWORD WINAPI ThreadProc2(LPVOID lpParameter)
{
	while (TRUE) 
	{
		WaitForSingleObject(hMutex,INFINITE);//(锁上 + 等待)
		if (tickets <= 100)
		{
			Sleep(1);
			cout << "thread2 sell ticket : " << tickets ++ << endl;
		}
		else
			break;
		ReleaseMutex(hMutex);//(解锁)
	}
	return 0;
}

3.利用临界区实现线程同步

临界区对象

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

通常把多线程中访问统一资源的那部分代码当作临界区,从而达到线程同步的目的。

相关API函数

初始化临界区对象:
void InitializeCriticalSection(LPCRITICAL_SECTION lpCriticalSetion);
 获得临界区对象所有权:
void EnterCriticalSection(LPCRITICAL_SECTION lpCriticalSetion);
 释放临界区对象所有权:
void LeaveCriticalSection(LPCRITICAL_SECTION lpCriticalSetion);
 释放临界区对象:
void DeleteCriticalSection(LPCRITICAL_SECTION lpCriticalSetion);

利用临界区完成上述售票系统

#include 
#include 

DWORD WINAPI ThreadProc1( LPVOID lpParameter); 
DWORD WINAPI ThreadProc2( LPVOID lpParameter); 

int tickets = 1;
CRITICAL_SECTION cs;//创建一把锁
void main()
{
	HANDLE hThread1;
	HANDLE hThread2;
	hThread1 = CreateThread(NULL,0,ThreadProc1,NULL,0,NULL);
	hThread2 = CreateThread(NULL,0,ThreadProc2,NULL,0,NULL);
	CloseHandle(hThread1);
	CloseHandle(hThread2);
	InitializeCriticalSection(&cs);//把这把锁初始化
	Sleep(5000);
}
//线程1的入口函数(售票窗口1)
DWORD WINAPI ThreadProc1(LPVOID lpParameter)
{
	while (TRUE) 
	{
		EnterCriticalSection(&cs);//获取临界区对象所有权
		if (tickets <= 100)
		{
			Sleep(1);
			cout << "thread1 sell ticket : " << tickets ++ << endl;
		}
		else
			break;
		LeaveCriticalSection(&cs);//释放临界区对象所有权
	}
	return 0;
}

//线程2的入口函数(售票窗口2)
DWORD WINAPI ThreadProc2(LPVOID lpParameter)
{
	while (TRUE) 
	{
		EnterCriticalSection(&cs);//获取临界区对象所有权
		if (tickets <= 100)
		{
			Sleep(1);
			cout << "thread2 sell ticket : " << tickets ++ << endl;
		}
		else
			break;
		LeaveCriticalSection(&cs);//释放临界区对象所有权
	}
	
	return 0;
}

互斥对象与临界区的比较

互斥对象属于内核对象,利用内核对象进行线程同步,速度较慢,但利用互斥对象这样的内核对象,可以在多个进程中的各个线程间进行同步。

临界区只能在同一进程内的线程间进行同步,但使用最简单,同步速度较快,因此是实现同步化的首选方法在使用临界区时,由于在等待进入关键代码段时无法设定超时值,容易进入死锁状态。

你可能感兴趣的:(#,Windows编程)