Windows核心编程对于临界区的详细描述

 

临界区是指一个小代码段,在代码执行前能够独占某些资源的访问权;需要注意的是,系统仍然能够控制线程的运行,去安排其他线程。不过,在线程退出临界区之前,系统不会调度其他试图访问相同资源的线程。来看一段代码:

const int p = 1000;//对这个全局变量进行操作
int g_index = 0;
DWORD g_time[p];
DWORD WINAPI Thread1(DWORD pParam) {
	while (g_index

这俩个函数如果单独执行,会产生相同的结果;如果在一个只有一个处理器的机器上跑,系统可能会先调用Thread2,当线程执行完g_index++后,cpu时间片耗尽要切换到Thread1,当这个线程的GetTickCount()获取时间时会将g_thime[1]设置为系统时间,然后再讲cpu分配到Thread2,它会将g_time[0]设置为系统时间;这样时间长的反而会被设置为0,短的设置为1,和预期结果是不一样的,这种线程安全问题会给后期的检查工作带来极大的麻烦。我们使用临界区去解决这个问题:

const int p = 1000;//对这个全局变量进行操作
int g_index = 0;
DWORD g_time[p];
CRITICAL_SECTION g_cs;//创建临界区对象

DWORD WINAPI Thread1(DWORD pParam) {
	while (g_index

指定了一个CRITICAL_SECTION 结构用来保护所有的资源,用EnterCriticalSection和LeaveCriticalSection函数将可能会共享资源的代码包裹住,这两个函数都调用了结构体的地址。

如果有多个不是一道使用的资源,比如1和2访问一个资源,1和3访问另外一个,这样需要为每一个资源创建一个独立的CRITICAL_SECTION结构。这个结构用来标识需要进入的线程,而EnterCriticalSection用来标识这个线程是否有人在使用。要记住在离开临界区时一定要调用LeaveCriticalSection,不然其他线程还是无法访问资源。

在无法使用互锁函数解决同步问题时,需要用到临界区。临界区的优点是使用容易,在内部使用互锁函数,因此能够快速运行。它的缺点是无法使用它们对多个进程中各个线程进行同步。

 

在了解完临界区的第一阶段之后,进入临界区的第二阶段,进入底层了解原理:

先从第一个疑点,CRITIACAL_SECTION结构说起。当你用F1查看这个结构时,你只能看到结构成员,成员从哪来并不知道。因为微软认为你没有必要了解这个结构。CRITICAL_SECTION在WinNT.h中定义为RTL_CRITICAL_SECTION,这个结构也爱WinBase中做了定义、但是绝不应该编写引用这些成员的代码。

想要使用CRITIACAL_SECTION结构需要一个windows函数,那这个函数是如何对结构体成员进行操控的?

使用这个结构体有俩个要求:

  1. 需要访问这个资源的线程必须要知道负责保护线程的CRITICAL_SECTION结构我的地址,这个地址可以使用任何方法获取。
  2. CRITICAL_SECTION结构中的成员应该在被访问前对成员进行初始化。函数为VOID IniticlizeCriticalSection(PCRITICAL_SECTION pcs);

这个函数只是对结构体的某些成员做了初始化,所以运行并不会失败,如果一个线程进入了一个未初始化的CRITICAL_SECTION结构,后果是不可预测的。

当没有线程需要访问资源是,需要调用函数清楚CRITICAL_SECTION结构:VOID DeleteCriticalSection(PCRITION pcs);

前面说过EnterCriticalSecion怎么使用,现在说一下为什么这么使用:这个函数负责查看这个结构体中的成员变量,然后进行如下测试:后面为了方便这个函数使用ECS函数代替;

  • 如果没有线程访问资源,ECS就更新成员变量。告诉线程能够单独访问这个资源。
  • 如果成员变量指明线程已经被赋予对资源的访问权,ECS就更新成员变量,说明线程被赋予了多少次访问权并且立即返回,使现车个继续运行。这种情况很少见,只有当线程在一行中调用俩次ECS函数并且不影响LeaveCriticalSection函数的调用,才会出现这种情况。
  • 如果成员变量指明,这个资源在被调用之前就有别的线程获取了访问权,那ECS将调用线程置于等待状态。等待线程不会浪费cpu。当这个资源调用了LeaveCritiolSecton释放资源后,这个线程就会从等待状态恢复为可调度状态。

有一种极端情况,如果在多处理器上俩个线程在同一时刻调用ECS函数,那这个函数还有用吗?  答案是有用,还是会将一个线程赋予资源访问权,有一个线程进入等待。因为这个函数的所有测试操作都是以原子方式进行的。

如果ECS函数将一个线程置于等待状态,要是在编写不好的程序中这个线程永远不会被调用,这个线程被称为渴求线程。但是在实际操作中,永远也不会出现这种情况。在注册表中CriticalSectionTimeout数据值决定的。如果请求时间超过这个时间,就会产生一个异常条件。这个函数其实可以用更方便的一个函数来代替:BOOL TryEnterCriticalSection(PCRITICAL_SECTION pcs);

这个不允许进入等待状态,它的返回值能够指明调用线程是否能够获取资源的访问权。如果发现有别的线程在访问资源,句返回FALSE,其他条件都会返回TRUE。要注意的是这个函数在windows 98中并没有实现,调用总会返回FALSE。

再来认识一个函数:WaitForSingleObject。

当WaitForSingleObject函数的第一个参数从未通知状态别为已通知状态时,在这个函数之下的WaitForSingleObject就不会在等待;线程正在运行为未通知状态,反正为已通知;这个函数等待单个对象,WaitForMultipleObjects()函数等待多个对象;第三个参数如果是true,会等待所有线程都执行完才会往下跑,如果是false,只要有任何一个线程变为已通知就会往下跑;

再来看在结尾处需要调用的函数LeaveCriticalSection函数的使用:这个函数没调用一次计数就会减1,如果这个计数大于0,那么这个函数不做其他操作,只返回。如果为0,就会查看在EnterCriticalSection中是否有其他线程在等待,如果至少有一个线程在等待,它就会先更新成员变量,将其中一个线程变为可调度状态。如果没有线程在等待,这个函数也会更新成员变量说明情况。

LeaveCriticalSection函数和EnterCriticalSection函数一样都可以以原子操作执行所有这些测试和更新,不过LeaveCriticalSection从不会使线程进入等待状态。当线程进入等待状态时,意味着线程必须从用户模式转为内核状态,这种转换消耗巨大。

在来看临界区的另外一种情况:在内存不足的情况下,可能会争临界区,同时系统也无法创建必要的事件内核对象,这是EnterCriticalSection会产生一个EXCEPTION_INVALID_HANDLE异常,这种情况非常少见,有俩种方法可以对这种情况进行处理。

  • 可以使用结构化异常处理方式来跟踪错误。当初五发生时,可以不访问临界区保护的资源,可以等待某些内存变为可用状态时,再次调用EnterCriticalSection函数。
  • 可以使用InitializeCriticalSectionAndSpinCount函数创建代码段,函数解释可以在vs中选中按F1进行查看,该函数要确保设置了dwSpinCount参数的高位。如果设置了,就创建事件内核对象。并且在初始化时和临界区关联起来。如果事件无法创建,就返回FALSE。如果事件创建成功,那EnterCriticalSection函数始终都能够运行。

 

关于临界区的使用技巧:

1.每个共享资源使用一个CRITICAL_SECTION变量

DWORD g_time[100];
DWORD g_name[100];
CRITICAL_SECTION g_cs;//创建临界区对象
DWORD WINAPI Thread1(DWORD pParam) {
	EnterCriticalSection(&g_cs);
	for (int i = 0; i < 100; i++) {
		g_name[i] = 0;
	}
	for (int i = 0; i < 100; i++) {
		g_time[i] = 'x';
	}
	LeaveCriticalSection(&g_cs);
	return 0;
}

这段代码在理论上是讲,俩个数组初始化没有联系,在初始化数组g_name后,另一个只需要访问g_name数组而不是访问g_time数组的线程就可以执行了,同时Thread1可以继续对g_time数组进行初始化,但是这是不可能的,因为用一个临界区保护着这俩个数据结构。这种情况就需要创建俩个临界区分别初始化:

DWORD g_time[100];
CRITICAL_SECTION g_csTime;//创建临界区对象
DWORD g_name[100];
CRITICAL_SECTION g_csName;//创建临界区对象
DWORD WINAPI Thread1(DWORD pParam) {
	EnterCriticalSection(&g_csTime);
	for (int i = 0; i < 100; i++) {
		g_name[i] = 0;
	}
	LeaveCriticalSection(&g_csTime);

	EnterCriticalSection(&g_csName);
	for (int i = 0; i < 100; i++) {
		g_time[i] = 'x';
	}
	LeaveCriticalSection(&g_csName);
	return 0;
}

这个代码一旦完成了对g_name数组的初始化,另一个线程就可以开始使用g_name数组。

2.同时访问多个资源

DWORD WINAPI Thread1(DWORD pParam) {
	EnterCriticalSection(&g_csTime);
	EnterCriticalSection(&g_csName);
	for (int i = 0; i < 100; i++) {
		g_name[i] = g_time[i];
	}
	
	LeaveCriticalSection(&g_csName);
	LeaveCriticalSection(&g_csTime);

	return 0;
}

如果另外一个函数中的一个进程也要访问这俩个资源:

DWORD WINAPI Thread2(DWORD pParam) {
	EnterCriticalSection(&g_csName);
	EnterCriticalSection(&g_csTime);
	for (int i = 0; i < 100; i++) {
		g_name[i] = g_time[i];
	}
	
	LeaveCriticalSection(&g_csName);
	LeaveCriticalSection(&g_csTime);

	return 0;
}

这个函数切换了进入临界区的顺序,就有可能产生死锁。Thread1先获得g_csTime的所有权,当线程切换到Thread2时,先获得了g_csName的所有权,当cpu再次到thread1的时候,就会发生死锁,谁都无法获得另一个临界区的所有权。

解决这个问题,必须始终按照完全相同的顺序请求对资源的访问。

3.不要在临界区长时间运行同一个线程

如果无法确定在处理消息需要花费多长时间,可能几个毫秒,可能需要几年。这样的程序就是有问题的。

 

 

 

 

 

你可能感兴趣的:(winows内核,c++)