NJUOS-3-多处理器编程

本文最后更新于:2 年前

JYY就是我的男神!这一堂课开始逐渐接触多进程,多线程的概念了。

多线程视角

全局变量 + Heap -> Global变量

Stack -> 私有变量

  • Stack是每一个线程私有的,Heap和全局变量是各个线程共享的。 -> 每一个状态,相当于选定 全局状态 + 执行的线程的私有状态(局部状态),进行执行,并得到结果。

image-20221209112327114

由于并发程序是并发执行的,具有不确定性。导致状态机从一个链表,变成了一棵多叉树,复杂度飙升!!!

image-20221209112624574

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
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <stdatomic.h>
#include <assert.h>
#include <unistd.h>
#include <pthread.h>

#define NTHREAD 64
enum { T_FREE = 0, T_LIVE, T_DEAD, };
struct thread {
int id, status;
pthread_t thread;
void (*entry)(int);
};

struct thread tpool[NTHREAD], *tptr = tpool;

void *wrapper(void *arg) {
struct thread *thread = (struct thread *)arg;
thread->entry(thread->id);
return NULL;
}

void create(void *fn) {
assert(tptr - tpool < NTHREAD);
*tptr = (struct thread) {
.id = tptr - tpool + 1,
.status = T_LIVE,
.entry = fn,
};
pthread_create(&(tptr->thread), NULL, wrapper, tptr);
++tptr;
}

void join() {
for (int i = 0; i < NTHREAD; i++) {
struct thread *t = &tpool[i];
if (t->status == T_LIVE) {
pthread_join(t->thread, NULL);
t->status = T_DEAD;
}
}
}

__attribute__((destructor)) void cleanup() {
join();
}

image-20221209155608030

she-test.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include "thread.h"

int x = 0;

// 事实可以看到,这里共享了这个x昂!!!
void Thello(int id) {
usleep(id * 100000);
printf("Hello from thread #%c\n", "123456789ABCDEF"[x++]);
}

int main() {
for (int i = 0; i < 10; i++) {
create(Thello);
}
}

stack-probe.c

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
#include "thread.h"

// __thread就是类似于Java中的ThreadLocal类,为每个线程创建属于自己的私有堆栈
__thread char *base, *cur; // thread-local variables
__thread int id;

// objdump to see how thread-local variables are implemented
__attribute__((noinline)) void set_cur(void *ptr) { cur = ptr; }
__attribute__((noinline)) char *get_cur() { return cur; }

// stackoverflow手动栈溢出,然后看看OS给线程的私有栈,分配了多少内存。这里看出来其实是最多分配8192 KB的大小(2^13)
void stackoverflow(int n) {
set_cur(&n);
if (n % 1024 == 0) {
int sz = base - get_cur();
printf("Stack size of T%d >= %d KB\n", id, sz / 1024);
}
stackoverflow(n + 1);
}

void Tprobe(int tid) {
id = tid;
base = (void *)&tid;
stackoverflow(0);
}

int main() {
setbuf(stdout, NULL);
for (int i = 0; i < 4; i++) {
create(Tprobe);
}
}

提出疑问:这个大小为啥是这样?能否手动设置,使用呢?

image-20221209160719167

多线程特性

原子性

常见假设:当前程序独占处理器执行(根本不成立啊…)

image-20221209161313683

  • 思考:

为什么经典的i++类似的并行会出问题,但是,printf,不会打印到一半,突然暴毙?

1
2
3
4
5
6
7
void Ta(){while(1){printf("aaaaa")}};
void Tb(){while(1){printf("bbb")}};

int main() {
create(Ta);
create(Tb);
}

man 3 printf -> / thread(搜索和thread相关的)

发现系统库早就考虑了昂!!!

image-20221209162247545

字符串没有相互交集,可以自己测的。去查手册验证,确实printf是线程安全的。

  • 原子性和其实现:

image-20221209162439244

顺序

编译器会去优化代码昂!!!

  • 顺序的丧失:

image-20221209164431257

如果想让编译器不去做这样的优化:

image-20221209164729270

可见性

image-20221209164859406

Mem-ordering.c

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
#include "thread.h"

int x = 0, y = 0;

atomic_int flag;
#define FLAG atomic_load(&flag)
#define FLAG_XOR(val) atomic_fetch_xor(&flag, val)
#define WAIT_FOR(cond) while (!(cond)) ;

__attribute__((noinline))
void write_x_read_y() {
int y_val;
asm volatile(
"movl $1, %0;" // x = 1
"movl %2, %1;" // y_val = y
: "=m"(x), "=r"(y_val) : "m"(y)
);
printf("%d ", y_val);
}

__attribute__((noinline))
void write_y_read_x() {
int x_val;
asm volatile(
"movl $1, %0;" // y = 1
"movl %2, %1;" // x_val = x
: "=m"(y), "=r"(x_val) : "m"(x)
);
printf("%d ", x_val);
}

void T1(int id) {
while (1) {
WAIT_FOR((FLAG & 1));
write_x_read_y();
FLAG_XOR(1);
}
}

void T2() {
while (1) {
WAIT_FOR((FLAG & 2));
write_y_read_x();
FLAG_XOR(2);
}
}

// 这里看下视频,两只手,两个开关,并行处理一些东西
void Tsync() {
while (1) {
// full barrier guarantees that x and y are definitely written to the memory
x = y = 0;
__sync_synchronize(); // full barrier
usleep(1); // + delay
assert(FLAG == 0);
FLAG_XOR(3);
// T1 and T2 clear 0/1-bit, respectively
WAIT_FOR(FLAG == 0);
printf("\n"); fflush(stdout);
}
}

int main() {
create(T1);
create(T2);
create(Tsync);
}

  • 本质上,这里的代码就是上面那幅图的level up 版本,但是,高级一点点,我们发现了很炸裂的事情。。。

理论上,这里的x, y的取值,是看不到同时为0的。。。但是,他确实是出现了同时为0的情况。。。

image-20221209165824265

处理器还会把我们的汇编再经过一次编译,得到一个处理器看的懂的,更小的操作(uOps)

image-20221209165943416

image-20221209170207071

上面绿色的这里就是,处理器又回自己把能并行的给并行了。处理器内部维护了一个这样的(DAG),并且在一个时钟周期内,取出多条可以同时执行的指令,突然让我想到了https://alexanderliu-creator.github.io/2022/12/05/cpu-cache-xue-xi/

image-20221209170556122

  • 处理器也是(动态)编译器:

image-20221209170725768

C语言 —(C编译器)—> 汇编指令(内存屏障生效)

汇编指令 —(CPU)—> 机器指令(uOps) CPU根据数据依赖性等关系,可以同时将多条不冲突的指令,issue到多核上去执行昂!

image-20221209171632194

CPU执行指令的顺序,不一定和你的汇编版本的是一致的!!!CPU也会自己维护关系,并且尽可能实现并发!!!

  • 我的理解:
    • C -> 汇编(串行 -> 并行)
    • 汇编 -> uOps(串行 -> 并行)

image-20221209172003607

每一次FLAG_XOR(3) -> 来自于上面的代码的时候,Cache总是会被Invalidate,总是miss的,CPU会去取后面的指令执行。Cache Line还是被共享的状态,下面这条指令就被扔到了上面去执行。

image-20221209172326777

https://jyywiki.cn/OS/2022/slides/3.slides#/4/4

  • 从编译 -> 机器指令的编译,其实也是可以保持一致的:

image-20221209173520600

mfence -> 可以在多处理器之间,保证一致性!!!

image-20221209173627283

  • mfence加完了之后:

image-20221209174035236

  • summary:

image-20221209174129979

References

  1. mac中获取su权限:https://blog.csdn.net/fgx_123456/article/details/109550283

  2. Dtruss: https://blog.csdn.net/kfy2011/article/details/48102843 -> Dtruss需要命令行拥有su的权限,才能够执行昂!!

  3. 查看pthread的手册:

1
man 7 pthreads

思路:能不能手动设置分配的堆栈内存大小。

  1. man的使用:

https://blog.csdn.net/u012349696/article/details/50314215

  1. gcc可以对代码进行优化:

https://www.zhihu.com/question/27090458

  1. objdump的使用:https://blog.csdn.net/zoomdy/article/details/50563680

  2. head指令(-n参数),sort指令,uniq指令

  3. 内存一致性模型:https://research.swtch.com/hwmm

  4. MESI: https://xiaolincoding.com/os/1_hardware/cpu_mesi.html#%E5%86%99%E5%9B%9E

  5. 内存屏障: https://zhuanlan.zhihu.com/p/125737864


NJUOS-3-多处理器编程
https://alexanderliu-creator.github.io/2022/12/09/njuos-3-duo-chu-li-qi-bian-cheng/
作者
Alexander Liu
发布于
2022年12月9日
许可协议