天天看点

多线程--临界区

前一段时间写Qt,在多线程的问题上卡住了,需要学习一下多线程的东西。

看了一个多线程的专题,在临界区的部分,学习了一些东西。

临界区部分主要是4个函数:

临界区初始化函数,void InitializeCriticalSection(LPCRITICAL_SECTION lpCriticalSection);

临界区销毁函数,void DeleteCriticalSection(LPCRITICAL_SECTION  lpCriticalSection);

进入临界区函数(获得所有权),void EnterCriticalSection(LPCRITICAL_SECTION  lpCriticalSection);

离开临界区函数(释放所有权),void LeaveCriticalSection(LPCRITICAL_SECTION  lpCriticalSection);

函数功能和用法还是很简单明了。

CRITICAL_SECTION所定义的结构体如下:

typedef struct _RTL_CRITICAL_SECTION {

    PRTL_CRITICAL_SECTION_DEBUG DebugInfo;

    LONG LockCount;

    LONG RecursionCount;

    HANDLE OwningThread; // from the thread's ClientId->UniqueThread

    HANDLE LockSemaphore;

    DWORD SpinCount;

} RTL_CRITICAL_SECTION, *PRTL_CRITICAL_SECTION;

其中

第一个参数DebugInfo用于调试。

第二个参数LockCount初始值为-1,用于记录等待此临界区的线程个数。

第三个参数RecursionCount初始值为0,用于记录临界区当前所有者进入临界区的次数。

第四个参数OwningThread为该临界区所有者的线程句柄。

第五个参数LockSemaphore为自复位事件,不太了解。

最后一个参数SpinCount是旋转锁设置,只在多核的计算机下起作用,这个还不怎么了解。

按照读到的东西,感觉临界区是这样的,微软设计它的时候就是单纯为了用来解决互斥问题的,所以提出了一个临界区的“线程所有”这么一个概念,希望进入临界区和离开临界区的两个函数EnterCriticalSection、LeaveCriticalSection可以在一个线程中成对出现。即使用EnterCriticalSection获得临界区的使用权,使用LeaveCriticalSection放弃临界区的使用权,同一个线程在获得临界区的使用权之后,依旧可以再次进入临界区,这时候临界区结构体中的RecursionCount会+1,在线程执行了LeaveCriticalSection后RecursionCount会-1,而不是清0。

同时微软也给出了提醒:如果试图离开一个并不是当前线程拥有的临界区,会是一个非常严重的错误。(虽然程序不会崩掉也不会抛出异常)按照微软的说法,当进程B中调用了LeaveCriticalSection(&critical_section_A),而此时critical_section_A的拥有者是A线程,那么此时A线程仍可以对临界区做正常操作。但是如果线程A退出了临界区,那么这个临界区被永久破坏;如果线程B调用了EnterCriticalSection(&critical_section_A),那么从此之后临界区被永久堵塞。就是说如果出现以上两种情况,临界区就废掉了。

但是网友们就是机智,有人轻而易举的就绕过了这两种情况,将临界区变成了既能实现互斥,也能实现同步的结构体。

我们通过一个题目来解析一下,题目是互斥同步可能最常见的一个了:

主线程启动10个子线程并将表示子线程序号的变量地址作为参数传给子线程,子线程收到参数,Sleep(50),将一个全局变量+1,Sleep(0),将自己的编号和全局变量都输出出来。

要求:全局变量的输出必须递增

分析:题目中主要目的就是输出两个参数,为了保证全局变量是递增的,就要实现子线程间的互斥,保证全局变量+1和输出的“原子性”,即两步必须同时进行。另外,由于线程获得的线程号是通过地址传递的,地址中的变量在每次传递后肯定是会改变的,为了保证每个子线程获得的是自己的线程号,就要在每个线程保存好自己的线程号后再来启新的线程,这就是题目中的同步的部分。

最正常思维,用mutex互斥,用semphore同步的代码就不上了,直接上,利用Critical_Section进行互斥和同步的代码:

#include <Windows.h>
#include <stdio.h>
#include <stdlib.h>
#include <process.h>
#include <iostream>

//子线程数
#define NUM 10

using namespace std;
unsigned _stdcall ThreadFun(LPVOID lpParameter);

//全局变量
int g_count;
//用于同步和互斥的临界区
CRITICAL_SECTION g_h_syncronous_section,g_h_mutex_section;

//主线程
void main()
{
	//子线程句柄
	HANDLE thread_handle[NUM];
	//初始化临界区
	InitializeCriticalSection(&g_h_syncronous_section);
	InitializeCriticalSection(&g_h_mutex_section);
    //子线程号变量
	int i=10;
	while(i)
	{
		//获得同步临界区所有权
		EnterCriticalSection(&g_h_syncronous_section);
		//重点的地方while循环
		while(g_h_syncronous_section.RecursionCount>1)
		{
			Sleep(0);
		}
		i--;
		//开始新的子线程
		thread_handle[i]=(HANDLE)_beginthreadex(NULL,0,ThreadFun,(LPVOID)&i,0,NULL);
	}
	//等待子线程执行完毕
	WaitForMultipleObjects(NUM,thread_handle,true,INFINITE);
	//释放子线程句柄
	for (int j=0;j<NUM;++j)
	{
		CloseHandle(thread_handle[j]); 
	}
	//销毁临界区
	DeleteCriticalSection(&g_h_syncronous_section);
	DeleteCriticalSection(&g_h_mutex_section);
}
//子线程
unsigned _stdcall ThreadFun(LPVOID lpParameter)
{
	//子线程保存自己的线程号
	int _index=*(int*)(lpParameter);
	//释放同步临界区
	LeaveCriticalSection(&g_h_syncronous_section);
	Sleep(50);
	//获得互斥临界区所有权
	EnterCriticalSection(&g_h_mutex_section);
	//全局变量自增
	InterlockedIncrement((LPLONG)&g_count);
	//输出变量
	printf("This is the %d th thread.The global variable is %d.\n",_index,g_count);
	//释放互斥临界区
	LeaveCriticalSection(&g_h_mutex_section);
	Sleep(0);
	//g_count++;
	return 0;
}
           

大部分的解释都写在注释里了,在子线程中的互斥部分没有什么问题,就是临界区的基本用法,用于互斥,在一个线程里成对存在。亮点就在于其中把临界区用于同步的部分,关键点在于大while循环内的while循环(注释中“重点的地方”)。由于占有临界区的线程本身还可以再次进入临界区,所以临界区本身是不能用来进行同步的,因为在程序中,主线程可以不等子线程进行释放操作而再次进入临界区,创造新的线程。因此,我们在创建新线程之前增加了一个while循环,检查主线程进入临界区的次数,如果大于1,说明释放操作还没有进行,所以就不创建新的线程,只有等到子线程的释放操作完成了,这时候线程号也已经保存完毕,再创建新的线程,从而达到同步的效果。

需要的效果我们已经到达了,现在我们来看一下安全性。之前提到的不安全因素需要有两个前提(两种情况),1、非占有临界区的线程调用LeaveCriticalSection函数,之后对此临界区进行EnterCriticalSection操作。2、占有临界区的线程被其他函数调用了LeaveCriticalSection之后,释放了临界区的所有权。然后我们发现,这两点都被我们巧妙的绕过了。1、子线程不会对同步临界区进行EnterCriticalSection操作。2、主线程不会对同步临界区进行LeaveCriticalSection操作。

以上都是自己的理解,也不知道对不对。当然,如果按照设计的初衷来说,临界区就是用来实现互斥的,还是不要用来实现同步了,这里也就是多一种选择吧。

继续阅读