《TCP/IP网络编程》学习笔记 | Chapter 20:Windows 中的线程同步
《TCP/IP网络编程》学习笔记 | Chapter 20:Windows 中的线程同步
- 《TCP/IP网络编程》学习笔记 | Chapter 20:Windows 中的线程同步
- 用户模式和内核模式
- 用户模式同步
- 内核模式同步
- 基于 CRITICAL_SECTION 的同步
- 内核模式的同步方法
- 基于互斥量对象的同步
- 基于信号量对象的同步
- 基于事件对象的同步
《TCP/IP网络编程》学习笔记 | Chapter 20:Windows 中的线程同步
用户模式和内核模式
Windows操作系统的运行方式是“双模式操作”(Dual-mode Operation):
- 用户模式(User mode):运行应用程序的基本模式,禁止访问物理设备,而且会限制访问的内存区域。
- 内核模式(Kernal mode):操作系统运行时的模式,不仅不会限制访问的内存区域,而且访问的硬件设备也不会受限。
实际上,在应用程序运行过程中,Windows操作系统不会一直停留在用户模式,而是在用户模式和内核模式之间切换。
例如,可以在Windows中创建线程。虽然创建线程的请求是由应用程序的函数调用完成,但实际创建线程的是操作系统。因此,创建线程的过程中无法避免向内核模式的转换。
定义这2种模式主要是为了提高安全性。应用程序的运行时错误会破坏操作系统及各种资源。特别是C/C++可以进行指针运算,很容易发生这类问题。例如,因为错误的指针运算覆盖了操作系统中存有重要数据的内存区域,这很可能引起操作系统崩溃。但实际上各位从未经历过这类事件,因为用户模式会保护与操作系统有关的内存区域。因此,即使遇到错误的指针运算也仅停止应用程序的运行,而不会影响操作系统。
总之,像线程这种伴随着内核对象创建的资源创建过程中,都要默认经历如下模式转换过程:用户模式→内核模式→用户模式。
从用户模式切换到内核模式是为了创建资源,从内核模式再次切换到用户模式是为了执行应用程序的剩余部分。不仅是资源的创建,与内核对象有关的所有事务都在内核模式下进行。
模式切换对系统而言其实也是一种负担,频繁的模式切换会影响性能。
用户模式同步
用户模式同步是用户模式下进行的同步,即无需操作系统的帮助而在应用程序级别进行的同步。
用户模式同步的最大优点是——速度快。无需切换到内核模式,仅考虑这一点也比经历内核模式切换的其他方法要快。而且使用方法相对简单,因此,适当运用用户模式同步并无坏处。
但因为这种同步方法不会借助操作系统的力量,其功能上存在一定局限性。稍后将介绍属于用户模式同步的、基于“CRITICAL_SECTION”的同步方法。
内核模式同步
下面给出内核模式同步的优点。
- 比用户模式同步提供的功能更多。
- 可以指定超时,防止产生死锁。
因为都是通过操作系统的帮助完成同步的,所以提供更多功能。特别是在内核模式同步中,可以跨越进程进行线程同步。因为内核对象并不属于某一进程,而是操作系统拥有并管理的。
与此同时,由于无法避免用户模式和内核模式之间的切换,所以性能上会受到一定影响。
基于 CRITICAL_SECTION 的同步
基于 CRITICAL_SECTION 的同步中将创建并运用“CRITICAL_SECTION对象”,但这并非内核对象。与其他同步对象相同,它是进入临界区的一把“钥匙”。因此,为了进入临界区,需要得到 CRITICAL_SECTION 对象这把“钥匙”。相反,离开时应上交 CRITICAL_SECTION 对象。
下面介绍 CRITICAL_SECTION 对象的初始化及销毁相关函数。
#include<windows.h>
void InitilizerCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
void DeleteCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
参数:
- IpCriticalSection:InitilizerCriticalSection 函数中传入需要初始化的 CRITICAL_SECTION 对象的地址值,DeleteCriticalSection 函数中传入需要解除的 CRITICAL_SECTION 对象的地址值。
上述函数的参数类型 LPCRITICAL_SECTION 是 CRITICAL_SECTION 指针类型。另外 DeleteCriticalSection 函数并不销毁CRITICAL_SECTION 对象。该函数的作用是销毁 CRITICAL_SECTION 对象相关的资源。
接下来介绍获取(拥有)及释放 CRITICAL_SECTION 对象的函数,可以简单理解为获取和释放“钥匙”的函数。
#include<windows.h>
void EnterCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
void LeaveCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
参数:
- IpCriticalSection:获取(拥有)及释放 CRITICAL_SECTION 对象的地址值。
与 Linux 部分中介绍过的互斥量类似,相信大部分人仅靠这些函数介绍也能写出示例程序。
示例程序:
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
#include <process.h>
#define NUM_THREAD 50
unsigned WINAPI threadInc(void *arg);
unsigned WINAPI threadDes(void *arg);
long long num = 0;
CRITICAL_SECTION cs;
int main(int argc, char *argv[])
{
HANDLE tHandles[NUM_THREAD];
int i;
InitializeCriticalSection(&cs);
for (i = 0; i < NUM_THREAD; i++)
{
if (i % 2)
tHandles[i] = (HANDLE)_beginthreadex(NULL, 0, threadInc, NULL, 0, NULL);
else
tHandles[i] = (HANDLE)_beginthreadex(NULL, 0, threadDes, NULL, 0, NULL);
}
WaitForMultipleObjects(NUM_THREAD, tHandles, TRUE, INFINITE);
DeleteCriticalSection(&cs);
printf("result: %lld \n", num);
system("pause");
return 0;
}
unsigned WINAPI threadInc(void *arg)
{
int i;
EnterCriticalSection(&cs);
for (i = 0; i < 50000000; i++)
num += 1;
LeaveCriticalSection(&cs);
return 0;
}
unsigned WINAPI threadDes(void *arg)
{
int i;
EnterCriticalSection(&cs);
for (i = 0; i < 50000000; i++)
num -= 1;
LeaveCriticalSection(&cs);
return 0;
}
运行结果:
程序将整个循环纳入临界区,可以减少运行时间。
内核模式的同步方法
基于互斥量对象的同步
基于互斥量(Mutual Exclusion)对象的同步方法与基于 CRITICAL_SECTION 对象的同步方法类似,因此,互斥量对象同样可以理解为“钥匙”。
首先介绍创建互斥量对象的函数:
#include<windows.h>
HANDLE CreateMutex(LPSECURITY_ATTRIBUTES lpMutexAttributes,
BOOL bInitialOwner,
LPCTSTR lpName
);
参数:
- lpMutexAttributes:传递安全相关的配置信息,使用默认安全设置时可以传递 NULL。
- blnitialOwner:如果为 TRUE,则创建出的互斥量对象属于调用该函数的线程,同时进入 non-signaled 状态;如果为 FALSE,则创建出的互斥量对象不属于任何线程,此时状态为 signaled。
- IpName:用于命名互斥量对象。传入 NULL 创建无名的互斥量对象。
成功时返回创建的互斥量对象句柄,失败时返回 NULL。
从上述参数说明中可以看到,如果互斥量对象不属于任何拥有者,则将进入 signaled 状态。利用该特点进行同步。
另外,互斥量属于内核对象,所以通过如下函数销毁:
#include<windows.h>
BOOL CloseHandle(HANDLE hObject);
参数:
- hObject:要销毁的内核对象的句柄。
成功时返回 TRUE,失败时返回 FALSE。
上述函数是销毁内核对象的函数,所以同样可以销毁即将介绍的信号量及事件。下面介绍获取和释放互斥量的函数,但我认为只需介绍释放的函数,因为获取是通过各位熟悉的 WaitForSingleObject 函数完成的。
#include<windows.h>
BOOL ReleaseMutex(HANDLE hMutex);
参数:
- hMutex:需要释放的互斥量对象句柄。
成功时返回 TRUE,失败时返回 FALSE。
接下来分析获取和释放互斥量的过程。互斥量被某一线程获取时(拥有时)为 non-signaled 状态,释放时(未拥有时)进入 signaled 状态。因此,可以使用 WaitForSingleObject 函数验证互斥量是否已分配。该函数的调用结果有如下 2 种。
- 调用后进入阻塞状态:互压量对象已被其他线程获取,现处于 non-signaled 状态。
- 调用后直接返回:其他线程未占用互斥量对象,现处于 signaled 状态。
互斥量在 WaitForSingleObject 函数返回时自动进入 non-signaled 状态,因为它是第 19 章介绍过的"auto-reset"模式的内核对象。结果,WaitForSingleObject 函数为申请互斥量时调用的函数。因此,基于互斥量的临界区保护代码如下:
WaitForsingleobject(hMutex, INFINITE);
// 临界区的开始
// ......
// 临界区的结束
ReleaseMutex(hMutex);
WaitForSingleObject 函数使互斥量进入 non-signaled 状态,限制访问临界区,所以相当于临界区的门禁系统。相反,ReleaseMutex 函数使互斥量重新进入 signaled 状态,所以相当于临界区的出口。
示例程序:
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
#include <process.h>
#define NUM_THREAD 50
unsigned WINAPI threadInc(void *arg);
unsigned WINAPI threadDes(void *arg);
long long num = 0;
HANDLE hMutex;
int main(int argc, char *argv[])
{
HANDLE tHandles[NUM_THREAD];
int i;
hMutex = CreateMutex(NULL, FALSE, NULL);
for (i = 0; i < NUM_THREAD; i++)
{
if (i % 2)
tHandles[i] = (HANDLE)_beginthreadex(NULL, 0, threadInc, NULL, 0, NULL);
else
tHandles[i] = (HANDLE)_beginthreadex(NULL, 0, threadDes, NULL, 0, NULL);
}
WaitForMultipleObjects(NUM_THREAD, tHandles, TRUE, INFINITE);
CloseHandle(hMutex);
printf("result: %lld \n", num);
system("pause");
return 0;
}
unsigned WINAPI threadInc(void *arg)
{
int i;
WaitForSingleObject(hMutex, INFINITE);
for (i = 0; i < 50000000; i++)
num += 1;
ReleaseMutex(hMutex);
return 0;
}
unsigned WINAPI threadDes(void *arg)
{
int i;
WaitForSingleObject(hMutex, INFINITE);
for (i = 0; i < 50000000; i++)
num -= 1;
ReleaseMutex(hMutex);
return 0;
}
运行结果:
基于信号量对象的同步
Windows 中基于信号量对象的同步也与 Linux 下的信号量类似,二者都是利用名为“信号量值”的整数值完成同步的,而且该值都不能小于 0。当然,Windows 的信号量值注册于内核对象。
下面介绍创建信号量对象的函数,其销毁同样是利用 CloseHandle 函数进行的。
#include <windows.h>
HANDLE Createsemaphore(LPSECURITY_ATTRIBUTES lpSemaphoreAttributes,
LONG lInitialCount,
LONG lMaximumCount,
LPCTSTR lpName
);
参数:
- IpSemaphoreAttributes:安全配置信息,采用默认安全设置时传递 NULL。
- lInitialCount:指定信号量的初始值,应大于 0 小于 lMaximumCount。
- IMaximumCount:信号量的最大值。该值为 1 时,信号量变为只能表示 0 和 1 的二进制信号量。
- lpName:用于命名信号量对象,传递 NULL 时创建无名的信号量对象。
成功时返回创建的信号量对象的句柄,失败时返回 NULL。
向 lInitialCount 参数传递 0 时,创建 non-signaled 状态的信号量对象。而向 IMaximumCount 传入 3 时,信号量最大值为 3,因此可以实现 3 个线程同时访问临界区时的同步。
可以利用“信量值为 0 时进入 non-signaled 状态,大于 0 时进入 signaled 状态”的特性进行同步。
下面介绍释放信号量对象的函数:
#include <windows.h>
BOOL ReleaseSemaphore(HANDLE hSemaphore,
LONG lReleaseCount,
LPLONG lpPreviouscount
);
参数:
- Semaphore:传递需要释放的信号量对象.
- IReleaseCount:释放意味着信号量值的增加,通过该参数可以指定增加的值。超过最大值则不增加,返回 FALSE。
- IpPreviousCount:用于保存修改之前值的变量地址,不需要时可传递 NULL。
成功时返回 TRUE,失败时返回 FALSE。
信号量对象的值大于 0 时成为 signaled 状态,为 0 时成为 non-signaled 状态。因此,调用 WaitForSingleObject 函数时,信号量大于 0 的情况才会返回,返回的同时将信量值减 1。可以通过如下程序结构保护临界区。
WaitForSingleObject(hSemaphore, INFINITE);
// 临界区的开始
// ......
// 临界区的结束
ReleaseSemaphore(hSemaphore, 1, NULL);
示例程序:
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
#include <process.h>
unsigned WINAPI read(void *arg);
unsigned WINAPI accu(void *arg);
static HANDLE sem_one;
static HANDLE sem_two;
static int num;
int main(int argc, char const *argv[])
{
HANDLE hThread1, hThread2;
sem_one = CreateSemaphore(NULL, 0, 1, NULL);
sem_two = CreateSemaphore(NULL, 1, 1, NULL);
hThread1 = (HANDLE)_beginthreadex(NULL, 0, read, NULL, 0, NULL);
hThread2 = (HANDLE)_beginthreadex(NULL, 0, accu, NULL, 0, NULL);
WaitForSingleObject(hThread1, INFINITE);
WaitForSingleObject(hThread2, INFINITE);
CloseHandle(sem_one);
CloseHandle(sem_two);
system("pause");
return 0;
}
unsigned WINAPI read(void *arg)
{
int i;
for (i = 0; i < 5; i++)
{
fputs("Input num: ", stdout);
WaitForSingleObject(sem_two, INFINITE);
scanf("%d", &num);
ReleaseSemaphore(sem_one, 1, NULL);
}
return 0;
}
unsigned WINAPI accu(void *arg)
{
int sum = 0, i;
for (i = 0; i < 5; i++)
{
WaitForSingleObject(sem_one, INFINITE);
sum += num;
ReleaseSemaphore(sem_two, 1, NULL);
}
printf("Result: %d \n", sum);
return 0;
}
运行结果:
在循环内部构件临界区,起到尽可能缩小临界区的作用,尽量提高程序性能。