前言

加上代码片段,markdown上一篇写的有点长了,自己都不好理了,还是分开写写吧。


正文

内核对象

  1. 内核对象
    windows中的每个内核对象都是一个内存块,它由操作系统内和分配,并且只能由操作系统内核进程访问,应用程序不能在内存中定位这些数据结构并直接更改其内容。这个内存卡本质上是一个数据结构,其成员维护着与对象相关的信息。
    CreateFile
    如file文件对象,event事件对象,process进程,thread线程,iocompletatinport完成端口,mailslot邮槽,mutex互斥量和registry注册表等
  2. 内核对象的使用计数和生命周期
    因为所有者是操作系统内核,而非进程,所以说当进程退出,内核对象不一定就被销毁。
    初次创建内核对象,使用计数为1,当另一个进程获得访问权之后,使用计数+1,当使用计数为0,操作系统内核会主动销毁内核对象。
  1. 操作内核对象
    通过Create之类的函数构建,成功构建后返回句柄,否则返回NULL。
    在32位进程中,句柄是一个32位值;在64位进程中,句柄则是一个64位值。
  2. 内核对象和其他类型的对象
    windows除了内核对象还有,窗口、菜单、字体等对象,但这些属于用户对象和GDI对象。要区分内核对象和非内核对象,最简单的方式就是查看创建这个对象的函数,几乎所有创建内核对象的函数都有一个允许我们指定安全属性的参数。

注:一个对象是不是内核对象,通常可以看创建次对象API的参数中是否需要PSECURITY_ATTRIBUTES类型的参数

只有内核对象的引用计数为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
#include <stdio.h>
#include <windows.h>

DWORD WINAPI ThreadProc(LPVOID lpParameter){
printf("I am comming....");
while (true){

}

return 0;
}

int main(){

HANDLE hThread;
HANDLE headle2;
DWORD threadId;

hThread = CreateThread(NULL, 0, ThreadProc, NULL, 0, &threadId); //创建 引用,内核计数位2

CloseHandle(hThread); //关闭线程句柄,内核计数-1

headle2 = OpenThread(THREAD_QUERY_INFORMATION, FALSE, threadId);
headle2 = OpenThread(THREAD_QUERY_INFORMATION, FALSE, threadId);
headle2 = OpenThread(THREAD_QUERY_INFORMATION, FALSE, threadId);

return 0;
}

信号量

内核对象的状态:
触发状态:有信号状态,表示有可用资源
未触发状态:无信号状态,表示没有可用资源

  1. 计数器:该内核对象被使用的次数
  2. 最大资源数量:标识信号量可以控制的最大资源数量(带符号的32位)
  3. 当前资源数量:标识当前可用的资源的数量(带符号的32位)。即表示当前开放资源的个数,注意不是剩下的资源个数。只有开放的资源才能被线程所申请,但开放的资源不一定被线程占用完。
    比如当前开放5个资源,目前只有3个线程申请,还剩2个可以用。
    但如果瞬发7个线程就要使用信号量,因为5个开放的资源显然不够用。

信号量的规则

  1. 如果当前资源计数大于0,那么信号量处于触发状态即有信号状态,表示有可用资源
  2. 如果当前资源计数等于0,那么信号量属于未触发状态即无信号状态,表示没有可用资源
  3. 系统本身绝对不会让当前资源计数变为负数
  4. 当前资源技术也绝对不会大于最大资源计数

CreateSemaphore

1
2
3
4
5
6
7
8
9
10
11
12
13
WINBASEAPI
HANDLE
WINAPI
CreateSemaphoreW(
_In_opt_ LPSECURITY_ATTRIBUTES lpSemaphoreAttributes,
_In_ LONG lInitialCount,
_In_ LONG lMaximumCount,
_In_opt_ LPCWSTR lpName
);

#ifdef UNICODE
#define CreateSemaphore CreateSemaphoreW
#endif

文档详解:

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
#include <stdio.h>
#include <windows.h>
#include <process.h>

static HANDLE semOne;
static HANDLE semTwo;
static int num;

unsigned WINAPI Read(void *arg){
for (int i = 0; i < 5; i++){
fputs("Input num:", stdout);
printf("begin read\n");

//等待内核对象信号,有信号继续执行没有就等待
WaitForSingleObject(semTwo, INFINITE);
printf("begining read\n");
scanf("%d", &num);
ReleaseSemaphore(semOne, 1, NULL);
}
return 0;
}

unsigned WINAPI Accu(void *arg){
int sum = 0;
for (int i = 0; i < 5; i++){
printf("begin Accu\n");

//等待内核对象semOne的信号,如果有信号继续执行,反之等待
WaitForSingleObject(semOne, INFINITE);
printf("beginning Accu\n");
sum += num;
printf("sum = %d\n", sum);
ReleaseSemaphore(semTwo, 1, NULL);
}
printf("Result:%d\n", sum);
return 0;
}

int main(){

HANDLE hThread1, hThread2;
semOne = CreateSemaphore(NULL, 0, 1, NULL);
//semOne 没有可用资源 只能表示0或1的二进制信号量 无信号
semTwo = CreateSemaphore(NULL, 1, 1, NULL);
//semTwo 有可用资源 有信号状态 有信号

hThread1 = (HANDLE)_beginthreadex(NULL, 0, Read, NULL, 0, NULL);
hThread2 = (HANDLE)_beginthreadex(NULL, 0, Read, NULL, 0, NULL);

WaitForSingleObject(hThread1, INFINITE);
WaitForSingleObject(hThread2, INFINITE);

CloseHandle(semOne);
CloseHandle(semTwo);

return 0;
}

因为semTwo初始有信号,所以线程开始的时候如果先调用Accu就会被卡在Wait那里,如上图2
而当线程先跑Read的时候虽然也执行到了scanf,但是由于我们还没有向内存输入,另一个线程Accu也已经先跑了就会看到图1的情况。

至于后续的结果,也是可以大致猜到的。
由于线程运行完之后都有一个重置信号的过程,所以另一个线程等到这个信号就立马执行了,导致控制台看上去有点乱。


关键代码段

前面学的互斥对象、事件对象、信号量这些对象都是属于内核态的线程同步。

critical_section关键代码段,也成为临界区,工作在用户方式下。它是指一个小代码段,在代码能够执行钱,他必须独占对某些资源的访问其。通常把多线程中的访问同一种资源的那部分代码当作关键代码段。

InitializeCriticalSection用于初始化一个关键代码段。
其原型为:

1
2
3
4
5
6
7
8
WINBASEAPI
VOID
WINAPI
InitializeCriticalSection(
_Out_ LPCRITICAL_SECTION lpCriticalSection
);

#endif // (_WIN32_WINNT < 0x0600)

EnterCriticalSection表示进入关键代码段
原型:

1
2
3
4
5
6
WINBASEAPI
VOID
WINAPI
EnterCriticalSection(
_Inout_ LPCRITICAL_SECTION lpCriticalSection
);

LeaveCriticalSection用于退出关键代码段

1
2
3
4
5
6
WINBASEAPI
VOID
WINAPI
LeaveCriticalSection(
_Inout_ LPCRITICAL_SECTION lpCriticalSection
);

DeleteCriticalSection用于删除临界区

1
2
3
4
5
6
WINBASEAPI
VOID
WINAPI
DeleteCriticalSection(
_Inout_ LPCRITICAL_SECTION lpCriticalSection
);

总共是初始化、进入、离开、删除四个模块

看起来是真的忒长了多看一眼都没有想法

魔改一下卖票功能用线程实现

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
#include <stdio.h>
#include <windows.h>
#include <process.h>

unsigned int piao = 100;
CRITICAL_SECTION g_cs;

DWORD WINAPI SellTicketA(void *arg){
while (true){
EnterCriticalSection(&g_cs); //进入临界区
if (piao > 0){
Sleep(1);
piao--;
printf("售票口1卖出:%d张票\n", piao);
LeaveCriticalSection(&g_cs); //离开临界区
} else{
LeaveCriticalSection(&g_cs); //离开临界区
break;
}
}
return 0;
}

DWORD WINAPI SellTicketB(void *arg){
while (true){
EnterCriticalSection(&g_cs); //进入临界区
if (piao > 0){
Sleep(1);
piao--;
printf("售票口2卖出:%d张票\n", piao);
LeaveCriticalSection(&g_cs); //离开临界区
} else{
LeaveCriticalSection(&g_cs); //离开临界区
break;
}
}
return 0;
}

int main(){
HANDLE hThreadA, hThreadB;
hThreadA = CreateThread(NULL, 0, SellTicketA, NULL, 0, NULL);
hThreadB = CreateThread(NULL, 0, SellTicketB, NULL, 0, NULL);

CloseHandle(hThreadA);
CloseHandle(hThreadB);

InitializeCriticalSection(&g_cs); //初始化关键代码

Sleep(20000);
DeleteCriticalSection(&g_cs); //删除临界区

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
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
#include <stdio.h>
#include <windows.h>
#include <process.h>

unsigned int piao = 100;
CRITICAL_SECTION g_cs;
CRITICAL_SECTION g_ct;

DWORD WINAPI SellTicketA(void *arg){
while (true){
EnterCriticalSection(&g_cs); //进入临界区cs
Sleep(1);
EnterCriticalSection(&g_ct); //进入临界区ct
if (piao > 0){
Sleep(1);
piao--;
printf("售票口1卖出:%d张票\n", piao);
LeaveCriticalSection(&g_ct); //离开临界区ct
LeaveCriticalSection(&g_cs); //离开临界区cs
} else{
LeaveCriticalSection(&g_ct); //离开临界区ct
LeaveCriticalSection(&g_cs); //离开临界区cs
break;
}
}
return 0;
}

DWORD WINAPI SellTicketB(void *arg){
while (true){
EnterCriticalSection(&g_ct); //进入临界区ct
Sleep(1);
EnterCriticalSection(&g_cs); //进入临界区cs
if (piao > 0){
Sleep(1);
piao--;
printf("售票口2卖出:%d张票\n", piao);
LeaveCriticalSection(&g_cs); //离开临界区cs
LeaveCriticalSection(&g_ct); //离开临界区ct
} else{
LeaveCriticalSection(&g_cs); //离开临界区cs
LeaveCriticalSection(&g_ct); //离开临界区ct
break;
}
}
return 0;
}

int main(){
HANDLE hThreadA, hThreadB;
hThreadA = CreateThread(NULL, 0, SellTicketA, NULL, 0, NULL);
hThreadB = CreateThread(NULL, 0, SellTicketB, NULL, 0, NULL);

CloseHandle(hThreadA);
CloseHandle(hThreadB);

InitializeCriticalSection(&g_cs); //初始化关键代码cs
InitializeCriticalSection(&g_ct); //初始化关键代码ct

Sleep(20000);
DeleteCriticalSection(&g_cs); //删除临界区cs
DeleteCriticalSection(&g_ct); //删除临界区ct

return 0;
}

这里创建了两个CRITICAL_SECTION对象,然后在两个线程里,关键代码段都在互相等待。人为的去造成这种死锁的情况。

因为两个线程都卡住了,所以就不会有显示。

解决方法:避免这种叼毛写法。


线程同步的比较小结

线程同步的方式主要有四种:

  1. 互斥对象Mutex
  2. 事件对象Event
  3. 关键代码段criticalSection
  4. 信号量——偏冷
  • 互斥对象和事件对象以及信号量都属于内核对象,利用内核对象进行线程同步,速度会比较慢,比较要wait之后重置。但是这样的内核对象可以在多个进程中的各个线程进行同步
  • 关键代码段说到是在用户方式下,它的同步速度肯定快于前面几个。但使用关键代码段容易不留神造成死锁状态。最后就是关键代码段只适用于本进程中

随便扯个表格,markdown格式写的,不确定会不会溢出。瞎看看吧。

差异/对象 互斥对象Mutex 事件对象Event 信号量Semaphore 关键代码段criticalSection
是否为内核对象
速度 较慢 较慢 较慢 快!
多个进程中的线程同步 支持 支持 支持 不支持
发生死锁现象
组成 一个线程ID;
用来标识哪个线程拥有该互斥量;
一个计数器:用来统计该线程用于互斥对象的次数
一个使用计数;
一个布尔值:用来标识该事件是自动重置还是人工重置;
一个布尔值:标识该事件处于有信号状态还是无信号状态
一个使用计数,
最大资源数,
标志当前可用资源数
一个小代码段。
在代码能执行钱,必须占用对某些资源的访问权
相关函数 CreateMutex;
WaitForsingleObjects;
ReleaseMutex
CreateEvent;
ResetEvent;
WaitforSingleobject;
SetEvent
CreateSemaphore;
WaitForsingleobject
ReleaseSemaPhore
InitializeCriticalSection
EnterCriticalSection
LeaveCriticalSection
DeleteCriticalSection
注意事项 谁拥有互斥对象谁来释放。
如果多次在同一个线程中请求同一个互斥对象,需要多次调用release函数
为了实现线程间的同步,不应该使用人工重置,应该把第二个参数设置为false;也就是自动重置 它允许多个线程在同一时间访问同一个资源,但是需要限制访问资源的最大数目。 防止死锁,使用多个关键代码段变量的时候
类比 一把钥匙 一个钥匙,自动和人工 停车场和保安 电话亭

线程安全

假如你写的代码在多线程中执行和单线程中执行的结果永远完全一致,那么可以说你的代码是线程安全的。
就是纯纯概念

视频推荐书籍陈硕《muduo》


结语

溜溜球,保持学了跟没学的状态。