0%

Windows核心编程 - 线程与内核对象的同步

最近在学习 Win32编程,所以顺便将每日所学记录下来,一方面为了巩固学习的知识,另一方面也为同样在学习Win32开发的童鞋们提供一份参考。

本系列博文均根据学习《Windows核心编程》一书总结而来;

运行环境:

  • 操作系统: Windows 10家庭版
  • 编译器:Visual Studio 2019

线程与内核对象同步

内核对象机制的适应性远远优于用户方法机制。实际上,内核对象机制唯一不足之处是它的速度比较慢;

当进程正在运行的时候,进程内核对象保持这种状态,当进程终止运行的时候,它就变为已通知状态。进程内核对象是个布尔值,当对象创建时,它默认是FALSE(未通知),当进程终止时,操作系统自动将对应的对象改为TRUE,表示该对象已通知;

线程可以使自己进入等待状态,直到一个对象变为已通知状态。

等待函数

等待函数可使得线程自愿进入等待状态,直到一个特定的内核对象变为已通知状态为止。

1
2
DWORD WaitForSingleObject( HANDLE hHandle, 		//句柄
DWORD dwMilliseconds //等待时间); //INFINITE为一直等待,直到该进程终止

通常情况下,INFINITE传递是有风险的,如果对象永远不变为通知状态,那么调用线程永远不会唤醒,它将永远是死锁状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <Windows.h>
#include <tchar.h>
#include <stdio.h>

DWORD WINAPI ThreadProc(LPVOID lpParameter)
{
for(int i = 0; i < 10; i++)
{
Sleep(500);
printf("------\n");
}
return 0;
}

int main()
{
HANDLE hThread;
hThread = CreateThread(NULL, 0, ThreadProc, NULL, 0, NULL);
DWORD dw = WaitForSingleObject(hThread, 5000);
switch (dw)
{
case WAIT_OBJECT_0:
break;
case WAIT_TIMEOUT:
break;
case WAIT_FAILED:
break;

default:
break;
}
getchar();
return 0;
}

我们可以调用WaitFotMultipleObject查看若干个内核对象的已通知状态;

1
2
3
4
DWORD WaitForMultipleObjects(DWORD nCount, 		//等待几个线程
CONST HANDLE *lpHandles, //线程的句柄
BOOL fWaitAll, //等待状态,如果为TRUE,在所有对象变为已通知前,该函数不允许调用线程运行
DWORD dwMilliseconds );//等待时间
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include <Windows.h>
#include <tchar.h>
#include <stdio.h>

DWORD WINAPI ThreadProc(LPVOID lpParameter)
{
for(int i = 0; i < 10; i++)
{
Sleep(500);
printf("------\n");
}
return 0;
}

DWORD WINAPI ThreadProc1(LPVOID lpParameter)
{
for (int i = 0; i < 10; i++)
{
Sleep(500);
printf("++++++\n");
}
return 0;
}
int main()
{
HANDLE hThread[2];

hThread[0] = CreateThread(NULL, 0, ThreadProc, NULL, 0, NULL);
hThread[1] = CreateThread(NULL, 0, ThreadProc1, NULL, 0, NULL);
DWORD dw = WaitForMultipleObjects(2, hThread,TRUE,1000);
switch (dw)
{
case WAIT_OBJECT_0:
break;
case WAIT_TIMEOUT:
break;
case WAIT_FAILED:
break;

default:
break;
}
getchar();
return 0;
}

成功等待副作用

对于有些内核对象来说,成功调用等待函数,实际上会改变对象的状态;成功地调用是指函数发现对象已经得到通知并且返回一个相对于WAIT_OBJECT_0的值。如果函数返回WAIT_TIMEOUT或WAIT_FAILED,那么调用就没有成功。如果函数失败了,对象状态不可能改变;

1
2
3
4
5
HANDLE hThread[2];

hThread[0] = CreateThread(NULL, 0, ThreadProc, NULL, 0, NULL);
hThread[1] = CreateThread(NULL, 0, ThreadProc1, NULL, 0, NULL);
DWORD dw = WaitForMultipleObjects(2, hThread,TRUE,INFINITE);

事件内核对象

在所有内核对象中,事件内核是个最基本的对象。它们包含一个使用计数,一个用于指向该事件是个自动重置的事件还是一个人工重置事件的布尔值,另一个用于指明该事件处于已通知状态还是未通知状态。

当一个线程执行初始化操作,然后通知另一个线程执行剩余的操作时,事件使用的最多。事件初始化为未通知状态,然后当线程完成它的初始化操作后,它就将事件设置为已通知状态。

1
2
3
4
HANDLE CreateEvent( LPSECURITY_ATTRIBUTES lpEventAttributes, 	//安全描述符
BOOL bManualReset, //用于描述人工重置事件还是自动重置事件
BOOL bInitialState, //用于描述已通知或未通知
LPTSTR lpName); //事件名称

当系统创建事件后,CreateEvent就将与进程相关的句柄返回给事件对象。其他进程中的线程可以获得该对象的访问权,方法是在lpName中传入相同的值

1
2
3
HANDLE OpenEvent(  DWORD dwDesiredAccess,  // 事件对象访问权限
BOOL bInheritHandle, // 是否通知
LPCTSTR lpName // object name);

一旦事件创建完成,就可以控制状态,可以调用SetEvent将事件修改为已通知

1
BOOL SetEvent(HANDLE hEvent ); 

当调用ResetEvent函数,可以将事件修改为未通知:

1
BOOL ResetEvent( HANDLE hEvent ); 

当这个进程启动的时候,它会创建一个人工重置的未通知状态的事件,并且将句柄保存在一个全局变量中。这样使得该进程中的其他线程可以访问到同一个事件对象。

当线程完成它对数据的专门传递时,它就调用 S e t E v e n t函数,该函数允许系统使得两个正在等待的线程中的一个成为可调度线程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#include <Windows.h>
#include <tchar.h>
#include <stdio.h>
HANDLE hMutex;
int g_Max = 10; //生产产品最大容量
int g_Number = 0; //容器
HANDLE g_hSet;
HANDLE g_hClear;
DWORD WINAPI ThreadProc1(LPVOID pM)
{

for (size_t i = 0; i < g_Max; i++)
{
WaitForSingleObject(g_hSet, INFINITE);
g_Number = 1;
DWORD id = GetCurrentThreadId();
printf("生产者%d将数据%d放入缓冲区中\n", id, g_Number);
SetEvent(g_hClear);
}
return 0;
}
DWORD WINAPI ThreadProc2(LPVOID pM)
{
for (size_t i = 0; i < g_Max; i++)
{
WaitForSingleObject(g_hClear, INFINITE);
g_Number = 0;
DWORD id = GetCurrentThreadId();
printf("消费者%d将数据%d放入缓冲区中\n", id, g_Number);
SetEvent(g_hSet);
}
return 0;
}
int main()
{
HANDLE hThread[2];
g_hSet = ::CreateEvent(NULL, FALSE, TRUE, NULL);
g_hClear = ::CreateEvent(NULL, FALSE, FALSE, NULL);
hThread[0] = ::CreateThread(NULL, 0, ThreadProc1, NULL, 0, NULL);
hThread[1] = ::CreateThread(NULL, 0, ThreadProc2, NULL, 0, NULL);
WaitForMultipleObjects(2, hThread, TRUE, INFINITE);
CloseHandle(hThread[0]);
CloseHandle(hThread[1]);
getchar();
return 0;

}

等待定时器内核对象

等待定时器是在某个时间或按规定的间隔时间发出自己的信号通知的内核对象。

若要创建等待定时器,只需要调用CreateWaitableTimer

1
2
3
HANDLE CreateWaitableTimer(  LPSECURITY_ATTRIBUTES lpTimerAttributes, // SD
BOOL bManualReset, // reset type
LPCTSTR lpTimerName // object name);

bManualReset参数用于指明人工重置的定时器或自动重置的定时器;

进程可以获得它自身与进程相关的现有等待定时器的句柄,方法是调用WaitableTimer函数

1
2
3
HANDLE OpenWaitableTimer(  DWORD dwDesiredAccess,  // access
BOOL bInheritHandle, // inheritance option
LPCTSTR lpTimerName // object name);

等待定时器对象总是子啊未通知状态中创建。必须调用SetWaitableTimer函数来告诉定时器;

1
2
3
4
5
6
7
BOOL SetWaitableTimer(
HANDLE hTimer, // handle to timer
const LARGE_INTEGER *pDueTime, // timer due time
LONG lPeriod, // timer interval
PTIMERAPCROUTINE pfnCompletionRoutine, // completion routine
LPVOID lpArgToCompletionRoutine, // completion routine parameter
BOOL fResume // resume state);

信标内核对象

信标内核对象用于对资源进行计数。它们与所有的内核对象一样,包含一个使用计数,但是它们也包含另外两个带符号的32位值,一个是最大资源数,一个是当前资源数。

信标的使用规则如下:

  • 如果当前资源的数量大于0,则发出信标信号
  • 如果当前资源的数目是0,则不发出信标信号
  • 系统绝不允许当前资源的数量为负值
  • 当前资源数量绝不允许大于最大资源数量

使用以下函数创建信标内核对象:

1
2
3
4
5
HANDLE CreateSemaphore(
LPSECURITY_ATTRIBUTES lpSemaphoreAttributes,
LONG lInitialCount, //当前资源数
LONG lMaximumCount, //最大资源数
LPCTSTR lpName );

通过以下函数打开信标内核对象

1
2
3
HANDLE OpenSemaphore(  DWORD dwDesiredAccess,  // access
BOOL bInheritHandle, // inheritance option
LPCTSTR lpName // object name);

通过以下函数对信标内核对象进行递增

1
2
3
BOOL ReleaseSemaphore(HANDLE hSemaphore, 
LONG lReleaseCount, //递增资源数
LPLONG lpPreviousCount );

互斥内核对象

互斥对象内核对象能确保线程拥有对单个资源的互斥访问权。实际上互斥对象是因此而得名的。互斥对象包含一个使用计数,一个线程ID和一个递归计数器

ID用于标识系统中哪个线程当前拥有互斥对象,递归计数器用于指明该线程拥有互斥对象的次数。

互斥对象使用规则如下:

  • 如果线程ID是0,互斥对象不被任何线程拥有,并且发出该互斥对象的通知信号
  • 如果ID非0,那么一个线程就拥有互斥对象,并且不发出该互斥对象的通知信号
  • 与其他内核对象不同,互斥对象在操作系统中拥有特殊代码。

若要使用互斥对象,必须有一个进程首先调用CreateMutex,以便创建互斥对象:

1
2
3
HANDLE CreateMutex( LPSECURITY_ATTRIBUTES lpMutexAttributes, 		//安全描述符
BOOL bInitialOwner, //是否初始有信号
LPCTSTR lpName ); //

如果要打开互斥对象,必须使用OpenMutex函数

1
2
3
HANDLE OpenMutex(  DWORD dwDesiredAccess,  // access
BOOL bInheritHandle, // inheritance option
LPCTSTR lpName // object name);

创建的时候填写FALSE证明有信号

一旦线程成功地等待到一个互斥对象,该线程就知道它已经拥有对受保护资源的独占访问权。试图访问该资源的任何其他线程(通过等待相同的互斥对象)均被置于等待状态中。当目前拥有对资源的访问权的线程不再需要它的访问权时,它必须调用 R e l e a s e M u t e x函数来释放该互斥对象:

1
BOOL ReleaseMutex( HANDLE hMutex );

该函数将对象的递减计数器减1;

释放

互斥对象不同于其他的内核对象,因为互斥对象有一个”线程所有权”的概念。

如果在释放互斥对象之前,拥有互斥对象的线程终止运行,那么互斥对象和等待互斥对象的线程视为放弃;

互斥对象

demo01:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
//打开创建互斥体
#include <Windows.h>
#include <tchar.h>
#include <stdio.h>

int main()
{
HANDLE g_hMutex = CreateMutex(NULL, TRUE, TEXT("sYstemk1t"));//获取令牌
DWORD dwMutexResults = GetLastError();
//WaitForSingleObject(g_hMutex, INFINITE);
if (g_hMutex)
{
if (ERROR_ALREADY_EXISTS == dwMutexResults)
{
printf("程序已经打开了\n");
Sleep(2000);
return 0;
}
}
else
{
printf("创建失败\n");
return 0;
}
while (1)
{
Sleep(2000);
printf("程序正在运行\n");
}
//ReleaseMutex(g_hMutex); //释放令牌
return 0;
}

demo02:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//创建互斥体
#include <windows.h>
#include <stdio.h>
int main()
{
//创建一个互斥体
HANDLE g_hMutex = CreateMutex(0, FALSE, TEXT("sYstemk1t")); //TRUE 创建出来无信号 FALSE 创建出来有信号

//获取互斥体,等待令牌
WaitForSingleObject(g_hMutex, INFINITE);

for (int i = 0; i < 10; i++)
{
Sleep(1000);
printf("B进程的Y线程:%d \n", i);
}
//释放令牌
ReleaseMutex(g_hMutex);
return 0;
}