内核对象



1、什么是内核对象


avater

内核对象:

  • 进程
  • 线程
  • 文件
  • 文件映射
  • 事件
  • 互斥体等待

2、事件内核创建


1
2
3
4
5
6
7
8
9
10
11
HANDLE CreateEventA(
[in, optional] LPSECURITY_ATTRIBUTES lpEventAttributes,
[in] BOOL bManualReset,
[in] BOOL bInitialState,
[in, optional] LPCSTR lpName
);

// 创建事件内核对象
HANDLE g_hEvent = CreateEvent(NULL,TRUE,FALSE,"XYZ");
// 创建互斥体对象
HANDLE g_hMutex = CreateMutex(NULL,FALSE,"XYZ")

CreateEvent 函数参数解释:

  • lpEventAttributes : 安全属性
  • bManualReset : 如果值为TRUE,在获取事件对象后要手动设置未通知;如果为FALSE,则自动变成未通知
  • bInitialState : 设置事件初始发送信号的状态,TRUE为初始已通知,FALSE为初始未通知
  • lpName : 事件对象的名称。(只用在进程间才需要用到)

3、事件内核对象的获取


1
2
3
4
5
6
7
8
9
10
HANDLE OpenEvent(
DWORD dwDesiredAccess, // access
BOOL bInheritHandle, // ingeritance option
LPCTSTR lpName // object name
);

// 事件内核对象的获取
HANDLE g_hEvent = OpenEvent(EVENT_ALL_ACCESS , FALSE,"XYZ");
// 互斥体内核对象的获取
HANDLE g_hMutex = OpenMutex(MUTEX_ALL_ACCESS,FALSE,"XYZ");

4、内核对象的销毁


BOOL CloseHandle(HANDLE hobj);

  1. 当没有其他程序引用时,系统会销毁内核对象(使用数量)
  2. 内核对象的生命周期,可能比创建它的对象要长

实验验证:

进程一创建内核对象,进程二获取内核对象,进程一销毁内核对象,进程三依旧可以获取内核对象,通俗个人理解:因为有两个人在使用,一个人销毁了,还有另外一个人可以找

  • 首先要明白计数器的概念,在高2g内存(内核)有一个结构体存储着这些参数,进程一创建对象,计数器+1,进程二获取对象,计数器+1,进程一销毁对象,计数器-1,所以当进程三去获取内核对象时,计数器里面还剩1个可以获取

事件对象



1、事件对象的创建


1
2
3
4
5
6
HANDLE CreateEvent(
LPSECURITY_ATTRIBUTES lpEventAttributes, // 安全属性 NULL时为系统默认
BOOL bManualReset, // TRUE 通过调用ResetEvent将事件对象标记为未通知
BOOL bInitialState, // TRUE 已通知状态 FALSE未通知状态(初始)
LPCTSTR lpName // 对象名称,以NULL结尾的字符串
);

2、事件对象的控制


BOOL SetEvent(HANDLE hEvent); // 将对象设置为已通知

对已通知、未通知的理解:

简单来说就是未通知时,不会发出信号,其他线程会在WaitForSingleObject的作用下阻塞,当对象变成已通知时,获取到对象的线程才能通过WaitForSingleObject进行执行下去

3、线程控制实验:只读形式的线程控制


实验一:

CreateEvent 的第二个参数设置成TRUE,即要手动设置成未通知

ThreadMain线程函数:

image-20240915190756532

其他三个线程,开始时用WaitForSingleObject阻塞住:

image-20240915190825087

当Thread1程序跑到SetEvent后,三个文本框同时出现1000,主线程里修改对象状态为已通知时,第一个线程wait到了,执行完后状态依旧是已通知(因为我们第二个参数为TRUE,需要手动使用设置成未通知),所以三个编辑框都可以读取到

image-20240915190845413

实验二:

CreateEvent 的第二个参数设置成FALSE,即Wait后自动设置成未通知

此时其他线程在函数执行完的位置要加上SetEvent(hEvent);来将事件对象设置成已通知

image-20240915190855897

执行代码,下面三个文本框是一个一个变成1000的,因为设置成FALSE后当线程wait到事件对象时就会自动设置成未通知状态,其他线程就无法使用,得等到我这个线程用完并用SetEvent将其设置成已通知其他线程才能用,从而可以实现互斥

线程同步


1、什么是线程同步?


线程同步:

通俗来讲就是两个或以上的线程运行要严格按照规定的某种先后次序来运行,这种先后次序依赖于要完成的特定的任务。按预定的先后次序运行。比如 A 任务的运行依赖于 B 任务产生的数据。

线程互斥:

与线程同步不同,线程互斥只是当这个线程运行时,其他线程不能运行,得等到我这个线程运行完才能执行,没有顺序要求

2、代码实验


实现两个线程严格的交替输出。
首先先用互斥或者临界区来看看能不能实现:
代码1(临界区):

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
48
49
50
51
52
53
CRITICAL_SECTION g_cs;
int g_Max = 10;
int g_Number = 0;
//生产者线程函数
DWORD WINAPI ThreadProduct(LPVOID pM)
{
for (int i = 0; i < g_Max; i++)
{
//互斥的访问缓冲区
EnterCriticalSection(&g_cs);
g_Number = 1;
DWORD id = GetCurrentThreadId();
printf("生产者%d将数据%d放入缓冲区\n", id, g_Number);
LeaveCriticalSection(&g_cs);

}
return 0;
}
//消费者线程函数
DWORD WINAPI ThreadConsumer(LPVOID pM)
{
for (int i = 0; i < g_Max; i++)
{
//互斥的访问缓冲区
EnterCriticalSection(&g_cs);
g_Number = 0;
DWORD id = GetCurrentThreadId();
printf("----消费者%d将数据%d放入缓冲区\n", id, g_Number);
LeaveCriticalSection(&g_cs);
}
return 0;
}

int main(int argc, char* argv[])
{
InitializeCriticalSection(&g_cs);


HANDLE hThread[2];

hThread[0] = ::CreateThread(NULL, 0, ThreadProduct, NULL, 0, NULL);
hThread[1] = ::CreateThread(NULL, 0, ThreadConsumer, NULL, 0, NULL);

WaitForMultipleObjects(2, hThread, TRUE, INFINITE);
CloseHandle(hThread[0]);
CloseHandle(hThread[1]);

//销毁
DeleteCriticalSection(&g_cs);


return 0;
}

输出:

image-20240915190909087

代码2(互斥体):

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
48
49
50
51
HANDLE hMutex;
int g_Max = 10;
int g_Number = 0;
//生产者线程函数
DWORD WINAPI ThreadProduct(LPVOID pM)
{
for (int i = 0; i < g_Max; i++)
{
//互斥的访问缓冲区
WaitForSingleObject(hMutex, INFINITE);
g_Number = 1;
DWORD id = GetCurrentThreadId();
printf("生产者%d将数据%d放入缓冲区\n", id, g_Number);
ReleaseMutex(hMutex);
}
return 0;
}
//消费者线程函数
DWORD WINAPI ThreadConsumer(LPVOID pM)
{
for (int i = 0; i < g_Max; i++)
{
//互斥的访问缓冲区
WaitForSingleObject(hMutex, INFINITE);
g_Number = 0;
DWORD id = GetCurrentThreadId();
printf("----消费者%d将数据%d放入缓冲区\n", id, g_Number);
ReleaseMutex(hMutex);
}
return 0;
}

int main(int argc, char* argv[])
{
//创建一个互斥体
hMutex = CreateMutex(NULL, FALSE, NULL);

HANDLE hThread[2];

hThread[0] = ::CreateThread(NULL, 0, ThreadProduct, NULL, 0, NULL);
hThread[1] = ::CreateThread(NULL, 0, ThreadConsumer, NULL, 0, NULL);

WaitForMultipleObjects(2, hThread, TRUE, INFINITE);
CloseHandle(hThread[0]);
CloseHandle(hThread[1]);

//销毁
CloseHandle(hMutex);

return 0;
}

输出:

Pasted image 20240912215204

发现不管时临界区,还是互斥体都无法实现严格的交替输出,这时,我们就可以用事件来实现。

事件是可以自动设置成未通知,然后通过SetEvent来设置成已通知的,对于两个线程,我们就可以通过两个事件来实现线程同步,具体代码如下:

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
HANDLE hEvent1,hEvent2;


DWORD WINAPI ThreadProc1(LPVOID lpParameter)
{
for(int i = 0;i < 5;i++){
WaitForSingleObject(hEvent1, INFINITE);
DWORD id = GetCurrentThreadId();
printf("生产者 %d 将数据 1 放入缓冲区\n",id);
SetEvent(hEvent2);
}

return 0;
}

DWORD WINAPI ThreadProc2(LPVOID lpParameter)
{
for (int i = 0; i < 5; i++) {
WaitForSingleObject(hEvent2, INFINITE);
DWORD id = GetCurrentThreadId();
printf("----消费者 %d 将数据 1 取出缓冲区\n",id);
SetEvent(hEvent1);
}

return 0;
}

int main() {
HANDLE hThread[2];
hThread[0] = ::CreateThread(NULL, 0, ThreadProc1, NULL, 0, NULL);
hThread[1] = ::CreateThread(NULL, 0, ThreadProc2, NULL, 0, NULL);

hEvent1 = CreateEvent(NULL, FALSE, TRUE, NULL);
hEvent2 = CreateEvent(NULL, FALSE, FALSE, NULL);

WaitForMultipleObjects(2, hThread, TRUE, INFINITE);
CloseHandle(hThread[0]);
CloseHandle(hThread[1]);
CloseHandle(hEvent1);
CloseHandle(hEvent2);

return 0;
}

可以看到输出已经是严格交替了:

image-20240915190925802

总结:


  1. 线程同步的实现:可以用事件来实现,有几个线程就创建多少个事件对象,全部都设置成自动变成未通知状态,第一个启动的线程要在将事件对象的初始状态设置成已通知,然后通过一个线程控制另一个线程的启动。