Windows中多线程的基础知识3——关键代码段(临界区)

 上面两节中,我们学习了利用互斥对象、事件对象实现线程同步的方法,这一节,我们继续学习利用关键代码段(临界区)实现线程同步的方法。
 如果对互斥对象和事件对象不熟悉,请移步到
 Windows中多线程的基础知识2——事件对象
 Windows中多线程的基础知识1——互斥对象

1 关键代码段(临界区)

1.1 关键代码段概念

 关键代码段,也称为临界区,工作在用户方式下。它是指一个小代码段,在代码能够执行前,它必须独占对某些资源的访问权。如果有多个线程试图同时访问临界区,那么在有一个线程进入后其他所有试图访问此临界区的线程将被挂起,并一直持续到进入临界区的线程离开。临界区在被释放后,其他线程可以继续抢占,并以此达到用原子方式操作共享资源的目的。
 临界资源是一次仅允许一个进程使用的共享资源。各进程采取互斥的方式,实现共享的资源称作临界资源。
 属于临界资源的硬件:打印机,磁带机等;
 软件:消息队列,变量,数组,缓冲区等。
 诸进程间采取互斥方式,实现对这种资源的共享。

1.2 关键代码段相关函数

一、定义全局的锁CRITICAL_SECTION

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

CRITICAL_SECTION cs;//可以理解为锁定一个资源

二、InitializeCriticalSection

 InitializeCriticalSection函数用来初始化一个临界资源对象。“临界区”CCriticalSection 是临界资源对象指针,该函数无返回值。单进程的各个线程可以使用临界资源对象来解决同步互斥问题,该对象不能保证哪个线程能够获得到临界资源对象,该系统能公平的对待每一个线程。
 其原型为:

VOID WINAPI InitializeCriticalSection(
LPCRITICAL_SECTION lpCriticalSection //临界资源对象指针。
);

三、EnterCriticalSection和LeaveCriticalSection

 EnterCriticalSection等待指定临界区对象的所有权,如果该所有权赋予了调用线程,该函数返回;否则,该函数一直等待,从而导致线程等待。LeaveCriticalSection释放指定的临界区对象所有权。

加锁————接下来的代码处理过程中不允许其他线程进行操作,除非遇到LeaveCriticalSection。
VOID WINAPI EnterCriticalSection(
    _Inout_ LPCRITICAL_SECTION lpCriticalSection //临界资源对象指针。
    );
解锁————EnterCriticalSection之间代码资源已经释放了,其他线程可以进行操作。
VOID WINAPI LeaveCriticalSection(
    _Inout_ LPCRITICAL_SECTION lpCriticalSection //临界资源对象指针。
    );

四、DeleteCriticalSection

 删除临界区,先前必须已在InitializeCriticalSection函数中将该对象初始化。

VOID WINAPI DeleteCriticalSection(
Inout LPCRITICAL_SECTION lpCriticalSection //临界资源对象指针。
);

2 实例代码

#include 
#include 

int iTickets = 1000;

CRITICAL_SECTION g_cs;

//线程1的入口函数
DWORD WINAPI Fun1Proc(__in  LPVOID lpParameter)
{
	while (true)
	{
		//等待获取关键段对象
		EnterCriticalSection(&g_cs);
		if (iTickets > 0)
		{
			std::cout << "thread1 sell tickes:" << iTickets-- << std::endl;
			LeaveCriticalSection(&g_cs);
		}
		else
		{
			LeaveCriticalSection(&g_cs);
			break;
		}
	}

	return 0;
}

//线程2的入口函数
DWORD WINAPI Fun2Proc(__in  LPVOID lpParameter)
{
	while (true)
	{
		//等待获取关键段对象
		EnterCriticalSection(&g_cs);
		if (iTickets > 0)
		{
			std::cout << "thread2 sell tickes:" << iTickets-- << std::endl;
			LeaveCriticalSection(&g_cs);
		}
		else
		{
			LeaveCriticalSection(&g_cs);
			break;
		}
	}

	return 0;
}

int main()
{
	//创建线程
	HANDLE hThread1;
	HANDLE hThread2;
	hThread1 = CreateThread(NULL, 0, Fun1Proc, NULL, 0, NULL);
	hThread2 = CreateThread(NULL, 0, Fun2Proc, NULL, 0, NULL);
	CloseHandle(hThread1);
	CloseHandle(hThread2);

	InitializeCriticalSection(&g_cs);

	//以下代码为了防止主线程提前退出,从而使进程退出,也就终止了进程下所有线程的运行
	while (true)
	{
		if (iTickets > 0)
			Sleep(200);
		else
		{
			//释放关键代码段对象
			DeleteCriticalSection(&g_cs);
			getchar();
			return 0;
		}
	}
}

 通过以上实例,我们可以看出,利用临界区实现线程同步的方法是:

  1. 定义全局临界区对象。
  2. 在main中调用InitializeCriticalSection初始化临界区对象,并在进程退出前,调用DeleteCriticalSection函数释放临界区使用的资源。
  3. 在每个线程受保护代码之前调用EnterCriticalSection函数,在受保护代码结束之后调用LeaveCriticalSection函数。如此,完成线程同步访问共享资源。

3 互斥对象、事件对象和临界区的异同

 首先,互斥对象、事件对象和临界区都可以实现线程同步,但是它们又具有显著的差别:

  1. 使用范围和效率不同
    互斥对象和事件对象属于内核对象,利用内核对象进行线程同步时,速度较慢,但利用互斥对象和事件对象这样的内核对象,可以在多个进程中的各个线程间进行同步;而关键代码段工作在用户方式下,同步速度较快,但是使用关键代码段时,很容易进入死锁状态,因为在等待进入关键代码段时无法设定超时值。
  2. 互斥对象可以反复执行受保护的代码
    某个线程通过WaitForSingleObject拥有互斥对象以后,就一直会拥有它的所有权,直到ReleaseMutex或线程退出。也就是说,如果某个线程已经拥有了某个互斥对象的所有权(此时,互斥对象处于无信号状态),又再次通过WaitForSingleObject获取这个互斥对象的所有权,那么WaitForSingleObject不会阻塞线程的执行。互斥对象的特点是一旦线程获得了其所有权,就可以反复执行受保护的代码。
  3. 自动重置事件对象只能单次执行受保护的代码
    某个线程通过WaitForSingleObject得到事件对象以后,如果这个事件对象是自动重置事件对象,事件对象随即变为无信号状态,那么其他线程就一直处在等待状态,直到这个线程将事件恢复为有信号状态。请注意,此时如果这个线程想再次执行WaitForSingleObject下的受保护的代码,则WaitForSingleObject会阻塞线程的执行。自动重置对象事件的特点是,线程只能执行一次受保护的代码,再次执行需要重置事件对象为有信号状态。
  4. 临界区可以反复执行受保护的代码
    临界区和互斥对象很相似,某个线程通过EnterCriticalSection获取临界区对象的所有权后,就会阻止其他线程获取这个所有权,直到这个线程通过LeaveCriticalSection释放临界区的所有权。也就是说,如果某个线程已经拥有了临界区的所有权,又再次通过EnterCriticalSection获取相同临界区所有权,那么EnterCriticalSection不会阻塞线程的执行。临界区对象的特点是一旦线程获得了其所有权,就可以反复执行受保护的代码。

 在我们编写多线程程序的时候,首选关键代码段,因为它的使用比较简单,不过一定要防止线程死锁的发生;另外,如果需要在多个进程间的各个线程间实现同步的话,可以使用互斥对象和事件对象。

你可能感兴趣的:(多线程,Win32编程,windows)