Windows

同步

多线程共享资源同步原因

以一个双线程计数为例

建立一个控制台应用:

img

线程回调:

img

创建两个线程

img

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

#include<Windows.h>
#include <iostream>
using namespace std;

int g_nCounter = 0;

DWORD WINAPI ThreadFunc(LPVOID lpParam)
{
for (int i = 0; i < 0x10000; i++) {
++g_nCounter;
}
printf("tid:%08x counter:%08x \r\n", GetCurrentThreadId(), g_nCounter);

return 0;
}

int main()
{
DWORD dwThreadId, dwThrdParam = 1;
HANDLE hThread[2];
hThread[0] = CreateThread(
NULL, // no security attributes
0, // use default stack size
ThreadFunc, // thread function
&dwThrdParam, // argument to thread function
0, // use default creation flags
&dwThreadId); // returns the thread identifier

hThread[1] = CreateThread(
NULL, // no security attributes
0, // use default stack size
ThreadFunc, // thread function
&dwThrdParam, // argument to thread function
0, // use default creation flags
&dwThreadId); // returns the thread identifier


system("pause");
cout << "Hello World!\n";

}

两个线程运行结果应该为00020000 但是实际上无法达到 由于多线程的同步问题

img

++g_nCounter:

img

多线程同步可能发生的情况

当线程1执行到加1时 线程被切到线程2 线程2执行完 再切回线程1 而线程1在被切走的时候内存为10 接着执行就会变成11(环境保存和恢复) 相当于两次加1 实际上就只有一次

img

三环同步

原子操作

原子操作(atomic operation)指的是由多步操作组成的一个操作。如果该操作不能原子地执行,则要么执行完所有步骤,要么一步也不执行,不可能只执行所有步骤的一个子集。

例如刚才的++g_nCounter 为三步操作 这三步使用原子操作时 必须全部执行 不能中途切换

windows原子操作API

img

image-20240714104816563

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
#include<Windows.h>
#include <iostream>
using namespace std;

LONG g_nCounter = 0;

DWORD WINAPI ThreadFunc(LPVOID lpParam)
{
for (int i = 0; i < 0x10000; ++i) {
InterlockedIncrement( &g_nCounter);
}
printf("tid:%08X counter:%08X \r\n", GetCurrentThreadId(), g_nCounter);

return 0;
}

int main()
{
DWORD dwThreadId, dwThrdParam = 1;
HANDLE hThread[2];
hThread[0] = CreateThread(
NULL, // no security attributes
0, // use default stack size
ThreadFunc, // thread function
&dwThrdParam, // argument to thread function
0, // use default creation flags
&dwThreadId); // returns the thread identifier

hThread[1] = CreateThread(
NULL,
0,
ThreadFunc,
&dwThrdParam,
0,
&dwThreadId);


system("pause");
cout << "Hello World!\n";
}

img

由于两个线程是同时进行的(不是先做完线程1 再做线程2 ,二者切换)所以线程1最后的结果并不是00010000

临界区(关键段)

当遇到更加复杂的情况多个线程同时修改了 vector 或者其它需要保护的共享资源时原本上述的API就不适用了 这时候就得使用临界区(同进程)

进入临界区:

img

离开临界区:

img

初始化(一次即可):

img

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

#include<Windows.h>
#include <iostream>
#include<vector>
using namespace std;
#pragma pack(1)

vector<int> g_vct;
CRITICAL_SECTION g_csForVct; // 定义临界区对象
DWORD WINAPI ThreadFunc(LPVOID lpParam)
{
for (int i = 0; i < 0x1000; ++i) {
EnterCriticalSection(&g_csForVct); // 进入临界区
g_vct.push_back(i);
LeaveCriticalSection(&g_csForVct); // 离开临界区
}
printf("tid:%08X counter:%08X \r\n", GetCurrentThreadId(), g_vct.size());
return 0;
}

int main()
{
DWORD dwThreadId, dwThrdParam = 1;
HANDLE hThread[2];
InitializeCriticalSection(&g_csForVct);//初始化
hThread[0] = CreateThread(
NULL, // no security attributes
0, // use default stack size
ThreadFunc, // thread function
&dwThrdParam, // argument to thread function
0, // use default creation flags
&dwThreadId); // returns the thread identifier

hThread[1] = CreateThread(
NULL,
0,
ThreadFunc,
&dwThrdParam,
0,
&dwThreadId);


system("pause");
cout << "Hello World!\n";
DeleteCriticalSection(&g_csForVct);//删除

}


运行结果:

img

如果在main函数中添加EnterCriticalSection(&g_csForVct);

而没有LeaveCriticalSection(&g_csForVct);

会使得两个进程都挂起 二者都在等待LeaveCriticalSection(&g_csForVct);的调用

调用几次EnterCriticalSection(&g_csForVct);

相应的就调用几次LeaveCriticalSection(&g_csForVct);

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
int main()
{
DWORD dwThreadId, dwThrdParam = 1;
HANDLE hThread[2];
InitializeCriticalSection(&g_csForVct);
//EnterCriticalSection(&g_csForVct); //进入临界区
hThread[0] = CreateThread(
NULL, // no security attributes
0, // use default stack size
ThreadFunc, // thread function
&dwThrdParam, // argument to thread function
0, // use default creation flags
&dwThreadId); // returns the thread identifier

hThread[1] = CreateThread(
NULL,
0,
ThreadFunc,
&dwThrdParam,
0,
&dwThreadId);

system("pause");
cout << "Hello World!\n";
DeleteCriticalSection(&g_csForVct);

}

内核同步对象

实现内核和三环之间的同步 由内核提供 可以做到跨进程同步

事件(Event)

1
2
3
4
5
CreateEvent
SetEvent
ResetEvent
WaitForSingleObject//检测函数
WaitForMultipleObjects//检测多个

WaitForSingleObject :

signaled(有信号)-开关, 开

nonsignaled(无信号) = 开关, 关

线程和进程内核对象:

 活着的时候,无信号

​ 死了的时候,有信号

创建事件:

img

img

参数2:如果此参数为 TRUE,则函数将创建手动重置事件对象,该对象需要使用 ResetEvent函数将事件状态设置为无信号

如果此参数为 FALSE,则函数将创建一个自动重置事件对象,在释放单个等待线程后,系统会自动将事件状态重置为无信号

将事件设置为有信号状态

image-20240714115719358

将事件设置为无信号状态

img

等待内核同步对象 若内核同步对象为无信号就会阻塞 直到对象变为有信号 或者等到参数2的时间到

img

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<Windows.h>
#include <iostream>
#include<vector>
using namespace std;
#pragma pack(1)

vector<int> g_vct;
HANDLE g_hEvent;
DWORD WINAPI ThreadFunc(LPVOID lpParam)
{
for (int i = 0; i < 0x1000; ++i) {
WaitForSingleObject(g_hEvent, INFINITE);
g_vct.push_back(i);
SetEvent(g_hEvent);
}
printf("tid:%08X counter:%08X \r\n", GetCurrentThreadId(), g_vct.size());

return 0;
}

int main()
{
DWORD dwThreadId, dwThrdParam = 1;
HANDLE hThread[2];

g_hEvent = CreateEvent(NULL,
FALSE,//WaitForSingleObject自动把事件从有信号置为无信号状态 这样就会使一个进程等待一个进程执行
//若为TRUE 则在调用WaitForSingleObject时两个线程都不会处于等待状态 大部分时候使用FALSE

TRUE,//事件初始状态为有信号的
NULL);

hThread[0] = CreateThread(
NULL, // no security attributes
0, // use default stack size
ThreadFunc, // thread function
&dwThrdParam, // argument to thread function
0, // use default creation flags
&dwThreadId); // returns the thread identifier

hThread[1] = CreateThread(
NULL,
0,
ThreadFunc,
&dwThrdParam,
0,
&dwThreadId);


system("pause");
cout << "Hello World" << endl;
}


区分WaitForSingleObject 是有信号还是超时:

img

等待多个信号(最多为64):

img

互斥体

1
2
3
CreateMutex;//创建互斥体
OpenMutex;//打开一个存在的互斥体对象
ReleaseMutex;//释放互斥体使用权

线程拥有权:任何线程在进入临界区之前,必须获取(acquire)与此区域相关联的互斥体的所有权。如果已有另一线程拥有了临界区的互斥体,其他线程就不能再进入其中。这些线程必须等待,直到当前的属主线程释放(release)该互斥体。

线程遗弃 :当拥有临界区的互斥体退出前,没有释放互斥体,那么系统会自动的将互斥体对象的线程ID设置为0,递归计数设置为0.等待函数返回WAIT_ABANDONED 而不是 WAIT_OBJECT_0 ,但是与互斥体不一样,事件的等待函数便会一直等待

img

参数2:当前创建互斥体的线程是否拥有互斥体的使用权

img

img

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
#include<Windows.h>
#include <iostream>
#include<vector>
using namespace std;
#pragma pack(1)


vector<int> g_vct;
HANDLE g_hMutex;
DWORD WINAPI ThreadFunc(LPVOID lpParam)
{
for (int i = 0; i < 0x1000; ++i) {
WaitForSingleObject(g_hMutex, INFINITE);
g_vct.push_back(i);
ReleaseMutex(g_hMutex);
}
printf("tid:%08X counter:%08X \r\n", GetCurrentThreadId(), g_vct.size());

return 0;
}

int main()
{
DWORD dwThreadId, dwThrdParam = 1;
HANDLE hThread[2];

g_hMutex = CreateMutex(NULL,
FALSE,//TRUE,当前线程获取此互斥体的使用权
//FALSE,当前线程不拥有此互斥体的使用权
NULL
);
//引用计数 调用几次waitfor 就要调用几次release
WaitForSingleObject(g_hMutex, INFINITE);
WaitForSingleObject(g_hMutex, INFINITE);
ReleaseMutex(g_hMutex);
ReleaseMutex(g_hMutex);
hThread[0] = CreateThread(
NULL, // no security attributes
0, // use default stack size
ThreadFunc, // thread function
&dwThrdParam, // argument to thread function
0, // use default creation flags
&dwThreadId); // returns the thread identifier

hThread[1] = CreateThread(
NULL,
0,
ThreadFunc,
&dwThrdParam,
0,
&dwThreadId);


system("pause");
cout << "Hello World" << endl;
}


img