windows进程线程

内核对象

windows操作系统是以C语言和汇编语言编写的,但是他是一个面向对象的操作系统,在系统中充满了对象的概念。

什么是内核对象?

内核对象的本质是一个个内核层的结构体变量。但是windows系统不希望程序员能够直接定义,访问,修改这些结构体变量,他们都被保护了起来,所以在使用这些对象的时候,都有一个共同点,就是都要先得到他们的句柄,再通过调用相应的API去操作这些对象。

内核对象的公共特点

所有的内核对象都遵循统一的使用模式

第一步:创建对象:

创建一个内核对象,一般都是使用CreateXXX的方式,比如:

CreateProcess 创建进程 CreateThread 创建线程
CreateFile 创建文件 CreateFileMapping 创建文件映射
CreateEvent 创建事件对象 CreateSemaphore 创建信号量

第二步:打开对象,得到句柄 (可与第一步合并在一起,表示创建的时候就打开)

第三步:通过API访问对象

第四步:关闭句柄

第五部:句柄全部关完,对象自动销毁

所有的内核对象都属于操作系统内核,可以在不同的进程间访问到,俗称:内核对象是跨进程的。很多时候,我们都需要再不同的进程中访问同一个内核对象,比如进程间的同步,进程间共享数据等。

每一个内核对象都有一个引用计数,当有一个进程创建或者打开了此内核对象,那么内核对象的引用计数自增。进程终止,或者关闭了句柄,引用计数自减1(CloseHandle),当引用计数自减为0,那么内核对象会自动销毁(CloseHandle)。

对象的安全属性(SECURITY_ATTRIBUTE),每一个内核对象在创建的时候,都是有一个安全属性,这个安全属性标识了,怎么区使用这个对象,比如对象是可读可写,对象所具有的权限等等。一旦内核对象以某种属性创建,之后便只能再规定的权限内工作,假如传NULL的话,就会指定一个默认的属性。

进程

进程是windows操作系统中一个重要的概念,很多时候,我们喜欢把一个exe文件想象成一个进程的全部,认为进程不过是.exe在内存中的样子,其实进程所涵盖的东西远远多于这个exe文件。一个进程至少包含了:

  • 一个虚拟的内存空间(4GB)
  • 在内存空间中,有映射进来的.exe文件,所有和程序运行相关的.dll文件,映射在内存中的exe与dll我们称之为模块
  • 进程内核对象,操作系统使用此对象来管理进程。内核对象中至少包含了,进程的内核对象句柄表,进程的权限,进程的全局唯一ID值
  • 至少有一个运行着的线程。

线程

线程是执行代码的一个单元,从CPU的角度来说,所有的线程一视同仁,不会因为你是某一个进程的线程而另眼相看,根据线程的优先级选择执行哪一个线程,分配其时间片,当此线程的时间片用完之后,再选择下一个线程执行

一般情况下,线程所执行的代码是属于某一进程的,同一个进程的所有线程共享进程内的所有资源:包括虚拟内存空间,内核对象句柄表等

还有一种线程叫做内核线程,其执行的代码全部是再内核空间。

我们通常把第一个执行的线程叫做主线程,主线程一旦退出整个程序就会退出,线程最少由一个线程内核对象,和一个线程的栈帧组成。

如何创建一个线程

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
// Windows.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include "pch.h"
#include <Windows.h>
#include <stdio.h>

DWORD WINAPI funThread(LPVOID lpParam)
{
for (int i = 0; i < 100; i++)
{
printf("%d\n", lpParam);

//让出线程的时间片
Sleep(100);
}
return 0;
}

int main()
{
HANDLE hThread = CreateThread(
NULL, //安全属性,NULL表示默认
NULL, //栈帧的大小,默认是4M
funThread, //回调函数,指明线程的其实位置
(LPVOID)123, //传递给线程函数的参数
NULL, //线程的创建标志,使用较多的是挂起
NULL //线程的ID,不需要可以传NULL
);
Sleep(100);

//挂起和恢复线程
//每一个线程都有一个挂起计数,每挂起一次,计数+1
//当挂起计数为0线程继续运行
SuspendThread(hThread);
system("pause");
ResumeThread(hThread);
system("pause");
//终止线程
TerminateThread(hThread, 0);

//为了确保主线程执行完毕前其他线程都正常退出
//需要等待其他线程执行完毕
if (hThread != NULL)
WaitForSingleObject(hThread, 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
30
31
32
33
34
35
36
37
38
// Windows.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include "pch.h"
#include <Windows.h>
#include <stdio.h>

int g_Number = 0;//可以被所有线程访问到的全局变量

//给g_Number自增100000次
DWORD WINAPI threadPorc(LPVOID lpParam)
{
for (int i = 0; i < 100000; i++)
{
g_Number++;
}
return 0;
}

int main()
{
HANDLE hThread1, hThread2;

hThread1 = CreateThread(NULL, NULL,
threadPorc, NULL, NULL, NULL);
hThread2 = CreateThread(NULL, NULL,
threadPorc, NULL, NULL, NULL);

if (hThread1 != NULL && hThread2 != NULL)
{
WaitForSingleObject(hThread1, INFINITE);
WaitForSingleObject(hThread2, INFINITE);
}

printf("%d", g_Number);

return 0;
}

image-20191118143045373

image-20191118143057613

可以看出,结果的值是不确定的。但是永远达不到200000(循环必须要足够大)

反汇编窗口可以看到

1
2
3
4
g_Number++;
mov eax,dword ptr [g_Number (0B69148h)]
add eax,1
mov dword ptr [g_Number (0B69148h)],eax

由于g_Number++;不是院子操作,所以可能出现如下问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[1]: mov eax,dword ptr [g_Number]	// g_Number(0)
[1]: add eax,1 // g_Number(0) eax(1)
[1]: mov dword ptr [g_Number],eax // g_Number(1) eax(1)

[2]: mov eax,dword ptr [g_Number] // g_Number(1)
[2]: add eax,1 // g_Number(1) eax(2)
[2]: mov dword ptr [g_Number],eax // g_Number(2) eax(2)

[1]: mov eax,dword ptr [g_Number] // g_Number(2)
[1]: add eax,1 // g_Number(2) eax(3)

[2]: mov eax,dword ptr [g_Number] // g_Number(2)
[2]: add eax,1 // g_Number(2) eax(3)
[2]: mov dword ptr [g_Number],eax // g_Number(3) eax(3)

[1]: mov dword ptr [g_Number],eax // g_Number(3) eax(3)

对于两个线程同时做自增而言,有可能线程1刚执行完add eax,1 还未来得及将自增后的结果写入,线程2开始执行,并完成了很多自增操作,当线程1再此执行的时候,将eax的值写回到全局变量中,然后发现,刚才线程2所做的都是无用功。因为线程的调度是不可控的。

原子操作

所谓的原子操作就是指一个线程对于某一个资源所做操作的时候能够确保没有其他的线程对此资源进行访问

本质就是将C语言的代码解释成了单条的汇编指令

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

应用场景:在进程inlinehook的时候可以解决线程安全

函数:Interlockxxx

InterlockedIncrement 给一个整形变量自增1
InterlockedExchangeAdd 为一个整形变量以原子方式加上一个数
InterlockedExchange 将一个32位数以原子方式赋值给另外一个数
InterlockedExchange64 将一个64位数以原子方式赋值给另外一个数
InterlockedCompareExchange 如果两个数相等,就将另外一个数赋值,不相等则无效果
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
// Windows.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include "pch.h"
#include <Windows.h>
#include <stdio.h>

long g_Number = 0;//形参类型为long

//给g_Number自增100000次
DWORD WINAPI threadPorc(LPVOID lpParam)
{
for (int i = 0; i < 100000; i++)
{
InterlockedIncrement(&g_Number);
}
return 0;
}

int main()
{
HANDLE hThread1, hThread2;

hThread1 = CreateThread(NULL, NULL,
threadPorc, NULL, NULL, NULL);
hThread2 = CreateThread(NULL, NULL,
threadPorc, NULL, NULL, NULL);

if (hThread1 != NULL && hThread2 != NULL)
{
WaitForSingleObject(hThread1, INFINITE);
WaitForSingleObject(hThread2, INFINITE);
}

printf("%d", g_Number);

return 0;
}

运行结果

image-20191118144057516

临界区(关键段)

原子操作的弱点:只能使得一个整形数据做简单算数运算的时候是原子的

但是大部分时候我们其实是希望保护一段代码,使得这一段代码是原子操作,而并非是某一个变量的操作,使用临界区恰好能够解决这个问题

临界区的概念:临界区使用EnterCriticalSectionLeaveCriticalSection形成一个保护区来保护代码,是一个结构体,通过结构体内的一些字段判断执行当前代码的线程是否是对应的线程,如果不是,就阻塞。拥有(线程拥有者)的概念。

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

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

在使用临界区前,需要调用InitializeCriticalSection初始化一个临界区,使用完后需要调用DeleteCriticalSection销毁临界区

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
// Windows.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include "pch.h"
#include <Windows.h>
#include <stdio.h>

long g_Number = 0;

//临界区结构体,是一个不确定的结构体,使用前必须初始化
//LONG LockCount; 如果被使用了就是-2,否则是-1
//CRLONG RecursionCount; 表示当前进入了多少次
//HANDLE OwningThread; 当前被哪一个线程引用了
CRITICAL_SECTION CriticalSection = { 0 };

//给g_Number自增100000次
DWORD WINAPI threadPorc(LPVOID lpParam)
{
for (int i = 0; i < 100000; i++)
{
//进入临界区,写在需要被保护的代码段的上方
EnterCriticalSection(&CriticalSection);
//需要被保护的代码
g_Number++;
//离开临界区
//进入了一个临界区多少次,就应该相应的离开多少次
LeaveCriticalSection(&CriticalSection);
}
return 0;
}

int main()
{
HANDLE hThread1, hThread2;

//初始化临界区
InitializeCriticalSection(&CriticalSection);

hThread1 = CreateThread(NULL, NULL,
threadPorc, NULL, NULL, NULL);
hThread2 = CreateThread(NULL, NULL,
threadPorc, NULL, NULL, NULL);

if (hThread1 != NULL && hThread2 != NULL)
{
WaitForSingleObject(hThread1, INFINITE);
WaitForSingleObject(hThread2, INFINITE);
}

//使用后需要释放
DeleteCriticalSection(&CriticalSection);

printf("%d", g_Number);

return 0;
}

运行结果:

image-20191118145004025

等待函数

等待函数可以等待一切可等待的内核对象,可等待的内核对象有两个状态,一个叫做激发态,另一个叫做非激发态

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

使用的函数:

  • 单个:WaitForSingleObject(内核对象句柄,等待时长)

  • 一组:WaitForMultipleObjects()

等待函数的返回值

WAIT_ABANDONED 互斥体的情况下有用
WAIT_OBJECT_0 等到了内核对象被设置为激发态
WAIT_TIMEOUT 超时了
WAIT_FAILED 失败了

等待函数的副作用:

  • 改变被等待内核对象的信号状态(有信号->无信号)
  • 基于这个原理才会有后面的内核对象同步
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
// Windows.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include "pch.h"
#include <Windows.h>
#include <stdio.h>

DWORD WINAPI threadPorc1(LPVOID lpParam)
{
for (int i = 0; i < 3; i++)
{
printf("%d - %d\n", (DWORD)lpParam, i + 1);
Sleep(500);
}
return 0;
}

DWORD WINAPI threadPorc2(LPVOID lpParam)
{
for (int i = 0; i < 3; i++)
{
printf("%d - %d\n", (DWORD)lpParam, i + 1);
Sleep(100);
}
return 0;
}

int main()
{
HANDLE hThread[10] = { 0 };

for (int i = 0; i < 10; i++)
{
if (i % 2)
hThread[i] = CreateThread(
NULL, NULL,
threadPorc1,
(LPVOID)(i + 1), NULL, NULL
);
else
hThread[i] = CreateThread(
NULL, NULL,
threadPorc2,
(LPVOID)(i + 1), NULL, NULL
);
}

//需要等待的内核对象的数量(数组元素的个数)
//保存所有需要等待的内核对象的数组
//是否等待所有的内核对象成为有信号状态
//等待时长
WaitForMultipleObjects(10, hThread, true, INFINITE);

//什么时候函数会返回
// 目标对象变成有信号(激发)状态
// 达到了等待时长,函数返回等待失败

return 0;
}

运行结果:

image-20191118150336270

互斥体

很多时候,在一个进程中,我们使用临界区能够解决很多的问题了,但是临界区也有它的缺点:

  • 临界区是在一个进程内有效的,无法在多线程的环境下进行同步
  • 一旦进入临进区的线程崩溃了,导致临界区无法释放,那么其他线程也就无法再进入临界区,全部会被卡住

所以使用互斥体来解决这些问题

互斥体是一个内核对象

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

缺点:慢

应用场景:用于预防应用双开

相关函数:

  • 保护:WaitforSingleObject
  • 离开:RealseMutex
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
// Windows.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include "pch.h"
#include <Windows.h>
#include <stdio.h>

long g_Number = 0;

//创建互斥体的函数
HANDLE hMutex = CreateMutex(
NULL, //安全属性
FALSE, //是否设置为当前线程拥有者(设置了就是无信号)
L"my_mutex" //互斥体的名称,用于打开
);

//初始(有信号)->等待函数(有信号->无信号)->Release(无信号->有信号)

DWORD WINAPI threadProc1(LPVOID lpParam)
{
for (int i = 0; i < 100000; i++)
{
//进入受保护的代码(有信号->无信号)
WaitForSingleObject(hMutex, INFINITE);

//需要被保护的代码
g_Number++;

//将互斥体设置为有信号
ReleaseMutex(hMutex);

}
return 0;
}

//对线程拥有者的测试
DWORD WINAPI threadProc2(LPVOID lpParam)
{
for (int i = 0; i < 100000; i++)
{
//拥有指定互斥体的线程,可以无限次的等待互斥体
WaitForSingleObject(hMutex, INFINITE);
g_Number++;
}
return 0;
}

int main()
{
HANDLE hThread1, hThread2;

//测试线程拥有者
HANDLE hThread3 = CreateThread(
NULL, NULL,
threadProc2,
NULL, NULL, NULL
);
WaitForSingleObject(hThread3, INFINITE);

hThread1 = CreateThread(
NULL, NULL,
threadProc1,
NULL, NULL, NULL
);
hThread2 = CreateThread(
NULL, NULL,
threadProc1,
NULL, NULL, NULL
);

if (hThread1 != NULL && hThread2 != NULL)
{
WaitForSingleObject(hThread1, INFINITE);
WaitForSingleObject(hThread2, INFINITE);
}

printf("%d", g_Number);
return 0;
}

运行结果:

image-20191118162619724

对于互斥体,有三个内容非常重要:

  • 他有两个状态,激发态和非激发态
  • 它有一个概念,叫做线程拥有权,与临界区类似
  • 等待函数等待互斥体的副作用,将互斥体的拥有者设置为本线程,将互斥体的状态设置为非激发态

于是一般情况下出现了下面的情况:

  • 当互斥体没有被任何一个线程拥有时,它处于激发态,也可以说锁是打开的
  • 当一个线程A调用了WaitforSingleObject时,WaitforSingleObject函数会立刻返回,并将互斥体设置为非激发态,互斥体被锁住,此线程获得拥有权
  • 后面的时刻,任何其他调用WaitforSingleObject函数的线程无法获得拥有权,必须一直等待互斥体,他们全都被阻塞
  • 当线程A调用RealseMutex函数,将互斥体释放,即为互斥体解锁,此时互斥体不被任何一个线程拥有,并被设置为激发态,会在等待它的线程中随机选择一个重复最开始的情况

互斥体同一时刻只能被一个线程拥有,在WaitForxxx与ReleaseMutex函数之间的代码被保护了起来,就像在临界区内一样。所不同的是,互斥体是一个内核对象,使得其能够在多进程间进行同步

相比于临界区的另外一个优势是,当锁住互斥体的线程意外崩溃,没有调用ReleaseMutex,互斥体会自动被设置为不被任何线程拥有,并处于激发态

事件

事件的使用给予了成员很大的权限,可以设置等待函数对于此事件对象有没有副作用。也可以手动设置事件对象为激发态还是非激发态

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

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

事件对象也是一个内核对象

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

缺点:慢

需要用到的函数:

CreateEventW 创建一个事件对象
OpenEventA 打开一个事件对象
SetEvent 将事件对象设置为激发态
ResetEvent 将事件对象设置为非激发态,注意:不是重新设置为激发态的意思
PulseEvent 将事件对象设置为激发态
CloseHandle 关闭事件对象,将事件对象引用计数减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
// Windows.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include "pch.h"
#include <Windows.h>
#include <stdio.h>

long g_Number = 0;

HANDLE hEvent = CreateEvent(
NULL, //安全属性
FALSE, //是否是手动状态,一旦设置成手动状态,等待函数就没有副作用了
TRUE, //事件的初始信号
L"my_event"//互斥体的名称,用于打开
);
//当设置为手动状态之后,通常就不能用于互斥了,会被用于同步
// 原因:ResetEvent函数并不是原子操作

//自动:初始(有信号)->等待函数(有信号->无信号)->Release(无信号->有信号)
//手动:初始(有信号)->等待函数(有信号)->Release(有信号)

DWORD WINAPI threadProc(LPVOID lpParam)
{
for (int i = 0; i < 100000; i++)
{
//进入受保护的代码(有信号->无信号)
WaitForSingleObject(hEvent, INFINITE);

//需要被保护的代码
g_Number++;

//设置为有信号
SetEvent(hEvent);

}
return 0;
}

int main()
{
HANDLE hThread1, hThread2;

hThread1 = CreateThread(
NULL, NULL,
threadProc,
NULL, NULL, NULL
);
hThread2 = CreateThread(
NULL, NULL,
threadProc,
NULL, NULL, NULL
);

if (hThread1 != NULL && hThread2 != NULL)
{
WaitForSingleObject(hThread1, INFINITE);
WaitForSingleObject(hThread2, INFINITE);
}

printf("%d", g_Number);
return 0;
}
1
当CreateEvent的第二个参数为TRUE(手动),并且没有SetEvent(hEvent)的结果

image-20191118164505336

1
当CreateEvent的第二个参数为FALSE(自动),并且设置SetEvent(hEvent)的结果

image-20191118164609290

信号量

信号量是一个内核对象

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

缺点:慢

应用场景:控制同时执行的线程的最大个数 , 信号量通常不会单独使用,一般要结合互斥体或事件

CreateSemaphore 创建信号量
OpenSemaphore 打开信号量
ReleaseSemaphore 释放信号量
CloseHandle 关闭信号量
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
// Windows.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include "pch.h"
#include <Windows.h>
#include <stdio.h>

long g_Number = 0;

HANDLE hSemaphore = CreateSemaphore(
NULL, //安全属性
20, //初始化的信号个数
20, //总的信号个数(数量没有限制)
L"my_semaphore" //名称
);
//初始(有n个信号)->等待函数(信号n->无信号n-1)->Release(信号n->信号n-1)

DWORD WINAPI threadProc(LPVOID lpParam)
{
for (int i = 0; i < 100000; i++)
{
//进入受保护的代码(将信号量的信号数量-1)
WaitForSingleObject(hSemaphore, INFINITE);

//需要被保护的代码
g_Number++;

//将信号量的信号数量+1
ReleaseSemaphore(
hSemaphore,
1, //将信号量加上一个值
NULL //增加信号量之前一共多少个信号,NULL表示不需要知道
);

}
return 0;
}

//输出参数
DWORD WINAPI threadProc2(LPVOID lpParam)
{
while (true)
{
WaitForSingleObject(hSemaphore, INFINITE);
printf("%d\n", lpParam);
Sleep(100);
ReleaseSemaphore(hSemaphore, 1, NULL);
}
return 0;
}

int main()
{
HANDLE thread[1000] = { 0 };
for (int i = 0; i < 1000; i++)
thread[i] = CreateThread(
NULL, NULL,
threadProc2,
(LPVOID)(i + 1),
NULL, NULL
);
WaitForMultipleObjects(1000, thread, true, INFINITE);

HANDLE hThread1, hThread2;

hThread1 = CreateThread(
NULL, NULL,
threadProc,
NULL, NULL, NULL
);
hThread2 = CreateThread(
NULL, NULL,
threadProc,
NULL, NULL, NULL
);

if (hThread1 != NULL && hThread2 != NULL)
{
WaitForSingleObject(hThread1, INFINITE);
WaitForSingleObject(hThread2, INFINITE);
}

printf("%d", g_Number);
return 0;
}

对于信号量,有这么几方面是需要掌控的

信号量有一个当前信号数,只要当前信号书部位0,信号量就处于激发态

当有线程调用了WaitForSingleObject函数,其副作用是将当前信号数减1,即为信号量上了一把锁

此时加入信号数量不为0,那么再有线程调用WaitForSingleObject函数,又会将当前信号数减1,从而再讲信号量上一把锁

调用ReleaseSemaphore会将当前信号数加1,即打开一把锁

再当前信号数为0的时候,即信号量被完全锁住,再有线程调用WaitForSingleObject时,就会被阻塞,直到有线程释放信号量,打开了一把锁

信号量有一个最大信号数,释放信号量不会使得当前信号数超过最大信号数,即在信号量上运行的线程的个数有一个数量限制

信号量不同于互斥体的是,互斥体的等待与释放应该在用一个线程内成对出现,信号量的释放动作,可以在任何一个线程中,从而使得其他线程得以执行

总结

原子操作:简单的同步机制,只能队4个字节的数据进行算数运算

临界区:队一段代码实现保护操作,只能在一个进程中的不同线程使用。无法检测由于线程崩溃造成的临界区无法释放的问题。有拥有者线程的概念,对于拥有者,可以多次进入临界区,相应的也需要多次离开临界区,否则容易造成死锁(其他线程永远等待)

互斥体:是一个内核对象,可以在不同的进程的线程中实现对于一段代码的保护。能够检测由于线程崩溃造成的互斥体释放问题。有拥有者线程的概念,对于拥有者,可以多次给互斥体上锁(即多次等待),相应的也需要多次解锁(即释放互斥体),否则容易造成死锁(其他线程也会永远等待),并且,它只能被拥有者线程释放,故而多线程间的不同回调函数的同步使用互斥体可能会出现问题

信号量:是一个内核对象,没有拥有者的概念,可以控制多个线程同时访问被保护的代码。并且给线程数量设置一个上限(即被锁定的次数)

事件:是一个内核对象,没有拥有者的概念,自主性非常高,特性完全可以由程序员设定,可以封装自己的同步机制。

原子操作:

InterlockedIncrement 自增 InterlockedIncrement(&g_Number)
InterlockedDecrement 自减 InterlockedDecrement(&g_Number)
InterlockedExchangeAdd 加法/减法 InterlockedExchangeAdd(&g_Number,256L)
InterlockedExchange 赋值 InterlockedExchange(&g_Number,256L)

临界区:

InitializeCriticalSection 初始化
DeleteCriticalSection 销毁
EnterCriticalSection 进入临界区
LeaveCriticalSection 离开临界区

互斥体:

CreateMutex 创建互斥体 可以给互斥体起名字
OpenMutex 打开互斥体,得到句柄 根据名字才能打开互斥体
ReleaseMutex 释放互斥体 会使得互斥对象处于激发态
CloseHandle 关闭句柄 使用完后关闭
WaitForSingleObject 等待互斥体处于激发态 等到激发态后,会使得互斥体再此处于非激发态

事件:

CreateEvent 创建事件 可以给事件起名字;可以设置两种模式:手工 自动
OpenEvent 打开事件,得到句柄 根据名字才能打开事件
SetEvent 释放事件 会使得事件处于激发态
ResetEvent 重置事件 会使得事件处于非激发态,对手工模式的事件有效
WaitForSingleObject 等待事件处于激发态 等到激发态后,对于自动模式的事件会使其再此处于非激发态

信号量:

CreateSemaphore 创建信号量 可以给信号量起名字;可以指定最大信号数和当前信号数
OpenSemaphore 打开信号量 根据名字才能打开信号量
ReleaseSemaphore 释放信号量 会增加信号量的信号数,但是不会超过最大信号数
WaitForSingleObject 等待信号量处于激发态 若处于激发态,则会减少一个信号数,信号数为零,则将其置为非激发态