复习

  1. 内核对象

    内核对象本质是一个内核层的结构体,只能使用 Windows 提供的API操作结构体内容

  2. 内核对象的特性

    操作内核对象需要使用句柄,每个进程都有一张句柄表保存自己的句柄

    大多数内核对象在操作时都需要提供指定的安全描述符(安全属性)

    内核对象的全局性,不同的进程可以通过 id 或名称打开同一个内核对象

    内核对象的引用计数,每个内核对象都有引用计数,当引用计数为0,内核对象会被销毁,CloseHandle 的作用是将引用计数减1

  3. 进程:是内核对象,通常由一个可执行文件产生,最少由一块4GB的虚拟空间,一个进程内核对象,一个线程内核对象和需要用到的模块组成

    线程:是内核对象,用于执行代码,线程间没有从属关系,但把一个进程的第一个线程称为主线程,主线程一旦退出,整个程序就会退出,线程最少由一个线程内核对象和一个线程的栈帧组成

线程的基本操作:

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
#include<windows.h>
#include<stdio.h>
//线程回调函数
DWORD WINAPI WorkThread(LPVOID lpThreadParameter) {
for (int i = 0; i < 100; i++) {
printf("%X\n", lpThreadParameter);
Sleep(1000);
}
return 0;
}
int main()
{
//创建一个线程
HANDLE ThreadHandle = CreateThread(
NULL, //安全属性,NULL表示默认
NULL, //栈帧大小,默认通常是4M
WorkThread, //回调函数,指明线程的起始位置
(LPVOID)0x10, //传递给线程函数的参数
NULL, //线程创建标志,使用比较多的是挂起
NULL //线程ID,不需要传递,通常是NULL
);
//让出时间片给ThreadHandle
Sleep(2000);
//挂起和恢复线程,每个线程都有一个挂起计数,每挂起一次,计数加1
//当挂起计数为0,线程继续运行
SuspendThread(ThreadHandle);
system("pause");
ResumeThread(ThreadHandle);//恢复线程
system("pause");
TerminateThread(ThreadHandle, 0);//结束线程
//为保证主线程执行完毕前所有的其他线程都正常退出,需要等待其他线程执行完毕
if (ThreadHandle != NULL)
WaitForSingleObject(ThreadHandle, INFINITE);

return 0;
}

线程同步

线程同步问题

在多线程编程中,极易产生错误,原因是:

  • 多个线程同时访问了共有的资源(如全局变量、句柄、堆空间等),造成资源在不同线程中修改时出现不一致,出现访问错误
  • 多个线程对于资源的访问需要按照一定的先后顺序,但未按照预想的顺序来,导致程序出错
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
#include<windows.h>
#include<stdio.h>
int g_Number = 0;//会被多个线程访问到的全局变量

//线程1
DWORD WINAPI ThreadPro1(LPVOID lpThreadParameter) {
for (int i = 0; i < 100000; i++)
g_Number++;
return 0;
}
//线程2
DWORD WINAPI ThreadPro2(LPVOID lpThreadParameter) {
for (int i = 0; i < 100000; i++)
g_Number++;
return 0;
}
int main()
{
HANDLE hThread1 = NULL, hThread2 = NULL;
hThread1 = CreateThread(NULL, NULL, ThreadPro1, NULL, NULL, NULL);
hThread2 = CreateThread(NULL, NULL, ThreadPro2, NULL, NULL, NULL);
if (hThread1 != NULL && hThread2 != NULL) {
WaitForSingleObject(hThread1, INFINITE);
WaitForSingleObject(hThread2, INFINITE);
}
printf("%d\n", g_Number);
system("pause");
return 0;
}

image-20191118134032529

结果不是200000

由于单条 g_Number++ 被翻译成了三条汇编指令,若不能保证三条指令连续执行,就会由于线程的切换产生问题,最终结果出错

1
2
3
mov  eax,dword ptr [g_Number (07AA138h)]
add eax,1
mov dword ptr [g_Number (07AA138h)],eax

原子操作

原子操作本质就是将C语言代码解释成单条汇编指令,一个线程对某个资源操作时保证没有其他线程能够对此资源进行访问

缺陷:只支持对最长8字节的整数类型执行算数运行

应用场景:在进行内联 hook 时可以解决线程安全问题

常见操作:

InterlockedIncrement 给一个整型变量自增1
InterlockedExchangeAdd 为一个整型变量以原子方式加上一个数
InterlockedExchange 将一个32位数以原子方式赋值给另一个数
InterlockedExchange64 将一个64位数以原子方式赋值给另一个数
InterlockedCompareExchange 若两数相等,就将另一个数赋值,不相等则无效

以上操作全都是作为一个执行单元来做的,基本上都是对于变量的算数运算

1
2
3
4
5
6
7
8
9
10
11
12
13
long g_Number = 0;
//线程1
DWORD WINAPI ThreadPro1(LPVOID lpThreadParameter) {
for (int i = 0; i < 100000; i++)
InterlockedIncrement(&g_Number);
return 0;
}
//线程2
DWORD WINAPI ThreadPro2(LPVOID lpThreadParameter) {
for (int i = 0; i < 100000; i++)
InterlockedIncrement(&g_Number);
return 0;
}

临界区

临界区(关键段)是一个结构体,通过结构体内的一些字段判断执行当前代码的线程是否是对应的线程,若不是就阻塞

优点:可以保护一段代码,执行速度快

缺点:拥有该临界区的线程一旦崩塌,就会产生死锁

临界区具有线程所有权这个概念,必须进入临界区的线程,调用离开临界区,临界区才会被打开。假如加锁的线程崩溃了,其他线程就锁死了。

临界区结构体是一个不确定的结构体,使用前必须调用 InitializeCriticalSection 初始化,使用完后需调用 DeleteCriticalSection 销毁临界区

void InitializeCriticalSection(LPCRITICAL_SECTION lpCriticalSection)
void DeleteCriticalSection(LPCRITICAL_SECTION lpCriticalSection)
void EnterCriticalSection(LPCRITICAL_SECTION lpCriticalSection)
void LeaveCriticalSection(LPCRITICAL_SECTION lpCriticalSection)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int g_Number = 0;
//LONG LockCount: 如果被使用了就是 -2 否则是 -1
//LONG RecursionCount: 表示当前被进入的多少次
//HANDLE OwningThread: 当前被哪一个线程使用了
CRITICAL_SECTION CriticalSection = { 0 };
//线程1
DWORD WINAPI ThreadPro1(LPVOID lpThreadParameter) {
for (int i = 0; i < 100000; i++) {
//进入临界区,写在需要被保护的代码块上方
EnterCriticalSection(&CriticalSection);
g_Number++;
//离开临界区,写在需要被保护的代码块的下方
//进入一个临界区多少次,相应的就要离开多少次
LeaveCriticalSection(&CriticalSection);
}
return 0;
}
//线程2
......
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//主函数中还要初始化和销毁临界区
int main()
{
HANDLE hThread1 = NULL, hThread2 = NULL;
//初始化临界区
InitializeCriticalSection(&CriticalSection);
hThread1 = CreateThread(NULL, NULL, ThreadPro1, NULL, NULL, NULL);
hThread2 = CreateThread(NULL, NULL, ThreadPro2, NULL, NULL, NULL);
if (hThread1 != NULL && hThread2 != NULL) {
WaitForSingleObject(hThread1, INFINITE);
WaitForSingleObject(hThread2, INFINITE);
}
//使用后释放
DeleteCriticalSection(&CriticalSection);
printf("%d\n", g_Number);
system("pause");
return 0;
}

等待函数

等待函数可以等待一切可等待的内核对象,可等待的内核对象有2个状态,激发态和非激发态

等待函数的作用是使一个线程进入到等待状态,直到指定的内核对象被触发为止

等待函数的副作用:改变被等待内核对象的信号状态(有信号 -> 无信号),基于此原理,才能实现后面的内核对象同步

函数原型:

1
2
3
4
5
6
7
8
9
10
11
12
DWORD WINAPI WaitForSingleObject(
_In_ HANDLE hHandle, //内核对象句柄
_In_ DWORD dwMilliseconds //等待超时时间
);
DWORD WINAPI WaitForMultipleObject(
_In_ DWORD nCount, //等待数量
_In_reads_(nCount) CONST HANDLE* lpHandles, //等待的句柄数组
_In_ BOOL bWaitAll, //是否等待全部
_In_ 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
#include<windows.h>
#include<stdio.h>
int g_Number = 0;
//LONG LockCount: 如果被使用了就是 -2 否则是 -1
//LONG RecursionCount: 表示当前被进入的多少次
//HANDLE OwningThread: 当前被哪一个线程使用了
CRITICAL_SECTION CriticalSection = { 0 };
//线程1,不断输出传入的参数
DWORD WINAPI WorkerThread1(LPVOID lParam) {
for (int i = 0; i < 5; i++) {
printf("%d - %d\n", (DWORD)lParam, i);
Sleep(500);
}
return 0;
}
//线程2
DWORD WINAPI WorkerThread2(LPVOID lParam) {
for (int i = 0; i < 5; i++) {
printf("%d - %d\n", (DWORD)lParam, i);
Sleep(100);
}
return 0;
}
int main()
{
HANDLE Threads[10] = { 0 };
for (int i = 0; i < 10; i++) {
if(i % 2)
Threads[i] = CreateThread(NULL, NULL, WorkerThread1,
(LPVOID)(i + 1), NULL, NULL);
else
Threads[i] = CreateThread(NULL, NULL, WorkerThread2,
(LPVOID)(i + 1), NULL, NULL);
}
WaitForMultipleObjects(10, Threads, true, INFINITE);
system("pause");
return 0;
}

互斥体

互斥体是一个内核对象

互斥体也具有线程所有权的概念,得到互斥体的线程,需要自己去释放互斥体。谁加锁,谁开锁。如果得到互斥体的线程崩溃了,互斥体会立即变为激发态。所有等待互斥体的线程中会立即有线程得到互斥体。不会造成死锁的问题

优点:拥有临界区的特性(线程拥有者),但不会产生死锁,且跨进程

缺点:慢

应用场景:用于防双开

互斥体的内容:

  • 两个状态,激发态(有信号)和非激发态(无信号)
  • 一个概念,线程拥有权,与临界区类似
  • 等待函数等待互斥体的副作用,将互斥体的拥有者设为本线程,将互斥体的状态设为非激发态
函数 作用 备注
CreateMutex 创建互斥体 可以给互斥体起名字
OpenMutex 打开互斥体,得到句柄 根据名字才能打开互斥体
ReleaseMutex 释放互斥体 会使得互斥体处于激发态
CloseHandle 关闭句柄 使用完后关闭
WaitForSignalObject 等待互斥体处于激发态 等到激发态后,会使得互斥体再次处于非激发态
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
54
55
56
57
58
59
60
#include<windows.h>
#include<stdio.h>
//初始(有信号)-> 等待函数(有信号 -> 无信号)-> Release(无信号->有信号)
int g_Number = 0;
//创建互斥体函数
HANDLE Mutex = CreateMutex(
NULL, //安全属性
FALSE, //是否设置当前线程为拥有者(设置了就是无信号),是否一开始就锁住
"my_mutex" //互斥体名称,用于打开
);
//线程1
DWORD WINAPI ThreadPro1(LPVOID lpThreadParameter) {
for (int i = 0; i < 100000; i++)
{
//进入受保护的代码(有信号->无信号)
WaitForSingleObject(Mutex, INFINITE);
//需要被保护的代码
g_Number++;
//将互斥体设置为无信号 -> 有信号
ReleaseMutex(Mutex);//为互斥体解锁
}
return 0;
}
//线程2
DWORD WINAPI ThreadPro2(LPVOID lPlpThreadParameteraram) {
for (int i = 0; i < 100000; i++)
{
WaitForSingleObject(Mutex, INFINITE);
g_Number++;
ReleaseMutex(Mutex);
}
return 0;
}
// 对线程拥有者的测试
DWORD WINAPI ThreadPro3(LPVOID lPlpThreadParameteraram) {
for (int i = 0; i < 100000; i++) {
//拥有指定互斥体的线程,可以无限次的等待互斥体
WaitForSingleObject(Mutex, INFINITE);
g_Number++;
}
return 0;
}
int main()
{
HANDLE hThread1 = NULL, hThread2 = NULL;
// 测试线程拥有者的
HANDLE hThread = CreateThread(NULL, NULL, ThreadPro3, NULL, NULL, NULL);
WaitForSingleObject(hThread, INFINITE);

hThread1 = CreateThread(NULL, NULL, ThreadPro1, NULL, NULL, NULL);
hThread2 = CreateThread(NULL, NULL, ThreadPro2, NULL, NULL, NULL);
if (hThread1 != NULL && hThread2 != NULL)
{
WaitForSingleObject(hThread1, INFINITE);
WaitForSingleObject(hThread2, INFINITE);
}
printf("%d", g_Number);
system("pause");
return 0;
}

信号量

互斥:通常是多个进程访问同一个资源

同步:通常是多个线程按照指定顺序执行

信号量是一个内核对象

特点:可以进行多次上锁操作

缺点:慢

应用场景:控制同时执行的线程的最大个数

信号量通常不会单独使用,一般要结合互斥体或事件

只要信号数不为0,那么就处于激发态

函数 作用 备注
CreateSemaphore 创建信号量 可以给信号量起名字 可以指定最大信号数和当前信号数
OpenSemaphore 打开信号量 根据名字才能打开信号量
ReleaseSemaphore 释放信号量 会增加信号量的信号数,但是不会超过最大信号数
WaitForSignalObject 等待信号量处于激发态 若处于激发态,则会减少1个信号数,信号数位0,将其置为非激发态
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
54
55
56
57
58
59
60
61
62
63
64
65
66
#include<windows.h>
#include<stdio.h>
int g_Number = 0;
//创建信号量函数
HANDLE Semaphore = CreateSemaphore(
NULL, //安全属性
10, //初始信号个数
10, //总信号个数(数量没有限制)
"my_semaphore" //互斥体名称,用于打开
);
//线程1
DWORD WINAPI ThreadPro1(LPVOID lpThreadParameter) {
for (int i = 0; i < 100000; i++)
{
//进入受保护的代码(将信号量的信号数量 -1)
WaitForSingleObject(Semaphore, INFINITE);
//需要被保护的代码
g_Number++;
//将信号量的信号数量 +1
//参数2: 将信号数量 +1
//参数3: 增加信号之前,一共有多少个信号,NULL表示不需要知道
ReleaseSemaphore(Semaphore, 1, NULL);
}
return 0;
}
//线程2
DWORD WINAPI ThreadPro2(LPVOID lPlpThreadParameteraram) {
for (int i = 0; i < 100000; i++)
{
WaitForSingleObject(Semaphore, INFINITE);
g_Number++;
ReleaseSemaphore(Semaphore, 1, NULL);
}
return 0;
}
//输出参数
DWORD WINAPI ThreadPro3(LPVOID lPlpThreadParameteraram) {
while (true) {
WaitForSingleObject(Semaphore, INFINITE);
printf("%d\n", lPlpThreadParameteraram);
Sleep(1000);
ReleaseSemaphore(Semaphore, 1, NULL);
}
return 0;
}
int main()
{
//控制线程的数量
HANDLE Threads[1000] = { 0 };
for (int i = 0; i < 1000; i++)
Threads[i] = CreateThread(NULL, NULL, ThreadPro3,
(LPVOID)(i + 1), NULL, NULL);
WaitForMultipleObjects(1000, Threads, true, INFINITE);

HANDLE hThread1 = NULL, hThread2 = NULL;
hThread1 = CreateThread(NULL, NULL, ThreadPro1, NULL, NULL, NULL);
hThread2 = CreateThread(NULL, NULL, ThreadPro2, NULL, NULL, NULL);
if (hThread1 != NULL && hThread2 != NULL)
{
WaitForSingleObject(hThread1, INFINITE);
WaitForSingleObject(hThread2, INFINITE);
}
printf("%d", g_Number);
system("pause");
return 0;
}

事件

事件是一个内核对象

事件,没有线程所有权的概念,任何线程都可以释放事件

特点:可手动操作,也可设置为自动操作

缺点:慢

函数 作用 备注
CreateEvent 创建事件 可以给事件起名字 可以设置两种模式:手工 自动
OpenEvent 打开事件,得到句柄 根据名字才能打开事件
SetEvent 释放事件 会使得事件处于激发态
ResetEvent 重置事件 会使得事件处于非激发态,对手工模式的事件有效
WaitForSignalObject 等待事件处于激发态 等到激发态后,对于自动模式的事件会使其再次处于非激发态
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
#include<windows.h>
#include<stdio.h>
int g_Number = 0;
//创建事件函数
HANDLE Event = CreateEvent(
NULL, //安全属性
FALSE, //是否是手动状态,一旦设置成手动状态,等待函数就没有副作用了
TRUE, //事件的初始信号,处于激发态
"my_event" //互斥体名称,用于打开
);
//当设为手动状态后,通常就不能用于进行互斥,会被用于同步
//因为SetEvent函数本身不是原子操作
// 自动:初始(有信号) -> 等待函数(有信号->无信号) -> Release(无信号->有信号)
// 手动:初始(有信号) -> 等待函数(有信号) -> Release(有信号)

//线程1
DWORD WINAPI ThreadPro1(LPVOID lpThreadParameter) {
for (int i = 0; i < 100000; i++)
{
//进入受保护的代码(有信号->无信号)
WaitForSingleObject(Event, INFINITE);
//需要被保护的代码
g_Number++;
//将事件设置为无信号->有信号
SetEvent(Event);
}
return 0;
}
//线程2
DWORD WINAPI ThreadPro2(LPVOID lPlpThreadParameteraram) {
for (int i = 0; i < 100000; i++)
{
WaitForSingleObject(Event, INFINITE);
g_Number++;
SetEvent(Event);
}
return 0;
}
int main()
{
HANDLE hThread1 = NULL, hThread2 = NULL;
hThread1 = CreateThread(NULL, NULL, ThreadPro1, NULL, NULL, NULL);
hThread2 = CreateThread(NULL, NULL, ThreadPro2, NULL, NULL, NULL);
if (hThread1 != NULL && hThread2 != NULL)
{
WaitForSingleObject(hThread1, INFINITE);
WaitForSingleObject(hThread2, INFINITE);
}
printf("%d", g_Number);
system("pause");
return 0;
}

总结:

原子操作,只能保证对于基本算数操作是原子性的

临界区和互斥体从词语的含义上看,他们主要就是为了解决互斥问题

临界区的优点是快,互斥体的优点是能够跨进程访问,崩溃不死锁

事件从词语的含义上看,更适合做通知(产生了一个事件)。比较适合解决有先后顺序的多线程问题

事件和互斥体的最大区别,就是线程所有权。互斥体谁上锁,谁开锁。事件没有这个要求

信号量,由于存在信号数的问题,比较适合解决多线程的协调问题。典型问题,就是生产者消费者问题

实例

事件和信号量更适合解决有序的问题。因为他们不要求谁上锁,谁开锁

用代码实现一个读文件线程,一个写文件线程,实现先写后读,两个线程都结束之后,主线程才结束,这种没有过多线程同时访问的有顺序的问题,比较适合用事件来解决

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
#include<windows.h>
#include<stdio.h>
#include<tchar.h>
HANDLE WriteFinish = 0;
HANDLE ReadFinish = 0;
DWORD WINAPI ReadProc(LPVOID lpThreadParameter) {
WaitForSingleObject(WriteFinish, -1);//等待写文件为激发态
printf("Read File\n");
SetEvent(ReadFinish);//将读文件变为激发态
return 0;
}
DWORD WINAPI WriteProc(LPVOID lpThreadParameter) {
printf("Write File\n");
SetEvent(WriteFinish);//将写文件变为激发态
return 0;
}
int _tmain(int argc, _TCHAR* argv[]) {
HANDLE hThread1 = 0, hThread2;
//两事件都是非激发态
WriteFinish = CreateEvent(NULL, FALSE, FALSE, NULL);
ReadFinish = CreateEvent(NULL, FALSE, FALSE, NULL);
hThread1 = CreateThread(NULL, NULL, ReadProc, NULL, NULL, NULL);
hThread2 = CreateThread(NULL, NULL, WriteProc, NULL, NULL, NULL);
//等待读文件为激发态才执行主线程函数
WaitForSingleObject(ReadFinish, -1);
printf("Hello World\n");
system("pause");
return 0;
}

多个信号数的信号量比较适合解决多个线程间有顺序需要协调的问题, 最为经典的就是生产者消费者问题

关键点有2个:1. 必须有一个队列,可以有数量限制,也可以没有数量限制。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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
//生产者先生产食物,并将消费者变为非激发态,使自己为激发态
//然后进入消费者线程,将生产者变为非激发态,消费者开始消费,然后将自己变为激发态
//又转去生产者线程
#include<windows.h>
#include<stdio.h>
#include <vector>
#include<time.h>
HANDLE hSemaphoreFull = 0;//生产
HANDLE hSemaphoreEmpty = 0;//消费
HANDLE hMutex = 0;
HANDLE hMutexNum = 0;
int g_nNum = 0;
class 盖饭
{
public:
盖饭(const char *szName, int nNum) :m_Id(nNum)
{
int nSize = strlen(szName) + 1;
m_szName = new char[nSize];
memset(m_szName, 0, nSize);
strcpy_s(m_szName, nSize, szName);
}
int GetId() {
return m_Id;
}
private:
char * m_szName;
int m_Id;
};
std::vector<盖饭*> g_盖饭表;
int Continue = 1;
//生产者回调函数,
DWORD WINAPI Creater(LPVOID lpThreadParameter){
srand(time(NULL));
while (Continue)
{
int nTime = rand() % 100;
printf("%d号生产者开始做饭,预计%dms\n", (int)lpThreadParameter, nTime);
WaitForSingleObject(hMutexNum, -1);//互斥体保护变量的自增,与生产消费无关
g_nNum++;
盖饭* p = new 盖饭("鱼香肉丝", g_nNum);
ReleaseMutex(hMutexNum);
Sleep(nTime * 10);
printf("%d号生产者传菜,菜号为%d\n",
(int)lpThreadParameter, p->GetId());
WaitForSingleObject(hSemaphoreEmpty, -1);//消费者在消耗,空位在消耗
WaitForSingleObject(hMutex, -1);
g_盖饭表.push_back(p);
ReleaseMutex(hMutex);
ReleaseSemaphore(hSemaphoreFull, 1, NULL);
}
return 0;
}
//消费者回调函数
DWORD WINAPI User(LPVOID lpThreadParameter)
{
srand(time(NULL));
while (Continue)
{
WaitForSingleObject(hSemaphoreFull, -1);//等待生产者
int nTime = rand() % 100;
Sleep(nTime * 10);
WaitForSingleObject(hMutex, -1);
printf(" %d号消费者取走了%d号饭,耗时%dms\n",
(int)lpThreadParameter, g_盖饭表[0]->GetId(), nTime);
g_盖饭表.erase(g_盖饭表.begin());
ReleaseMutex(hMutex);
ReleaseSemaphore(hSemaphoreEmpty, 1, NULL);//空位在增长
}
return 0;
}
int main()
{
hSemaphoreFull = CreateSemaphore(NULL, 0, 3, NULL);//信号量处于非激发态
hSemaphoreEmpty = CreateSemaphore(NULL, 3, 3, NULL);//信号量处于激发态
hMutex = CreateMutex(NULL, FALSE, NULL);//互斥体激发态
hMutexNum = CreateMutex(NULL, FALSE, NULL);
HANDLE hThread[20] = {};
//10个生产者
for (int i = 0; i < 10; i++) {
hThread[i] = CreateThread(NULL, 0, Creater, (LPVOID)(i+1), 0, 0);
}
//4个消费者
for (int i = 10; i < 14; i++) {
hThread[i] = CreateThread(NULL, 0, User, (LPVOID)(i-9), 0, 0);
}
WaitForMultipleObjects(14, hThread, TRUE, -1);
return 0;
}