Linux 内核101:[译]并发导论

原文:Operating Systems: Three Easy Pieces:Concurrency: An Introduction

进程和线程

进程和线程在底层的区别

在单线程进程中,只有一个execution flow,进程只能从一个 PC(Program counter)里面获取指令。多线程的进程有多个 execution flow,能够从多个 PCs 获取指令。要简单的对比一下进程和线程的话,就是每个 thread 很像一个独立的进程,但是同一个进程里面的线程共享一部分数据,同时共享地址空间

操作系统如何调度线程

每个线程有自己独立的PC和寄存器。也就是说,运行在同一个核的的两个线程 T1、T2,当CPU 从 T1切换到 T2执行的时候,会像进程切换一样,发生一次context switch。 CPU 需要把 T1 的运行状态和寄存器的数据保存起来,然后 restore T2的状态和寄存器数据。对于进程,状态被保存在 PCB(process control block);对于线程,使用的是 TCBs(Thread control block)。

线程和进程切换还有一点不同是:如果操作系统调度切换的两个线程是属于同一个进程的,那么地址空间就不需要切换,因为线程间是共享同一个地址空间的。这也就意味着线程切换相对于进程切换更加轻量级。

进程和线程能实现并行

首先,一个核在同一时刻只能执行一个进程(或者线程,下同)。如下图左所示。

要在同一时刻运行多个进程,必须要有多个核。因为操作系统有一套调度系统,所以能把多个进程分配给多个核。

线程调度全看操作系统喜欢

我们假设下面这个例子中:只有一个核。

下面这个程序主线程先用Pthread_create创建两个线程,这两个线程的作用就是简单的打印A或者B,然后主线程调用Pthread_join等待两个线程结束,最后主线程退出。

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

#include "common.h"
#include "common_threads.h"

void *mythread(void *arg) {
    printf("%s\n", (char *) arg);
    return NULL;
}

int main(int argc, char *argv[]) {                    
    if (argc != 1) {
	fprintf(stderr, "usage: main\n");
	exit(1);
    }

    pthread_t p1, p2;
    printf("main: begin\n");
    Pthread_create(&p1, NULL, mythread, "A"); 
    Pthread_create(&p2, NULL, mythread, "B");
    // join waits for the threads to finish
    Pthread_join(p1, NULL); 
    Pthread_join(p2, NULL); 
    printf("main: end\n");
    return 0;
}
复制代码

有两点:

  1. 一个线程先被创建,但它不一定会先被执行。
  2. 一个线程被创建,但它不一定会立即被执行。

可能会出现下面三种情况:

第一种:A 在 B 之前被执行。

第二种:线程被创建之后立即被执行,Pthread_join将会立即返回。

第三种: B 在 A 之前被执行。

从这个例子我们可以看到,线程的创建和调度是由操作系统来调度地,你无法判断哪个线程会先被执行,什么时候被执行。

线程共享变量带来的问题

下面这个程序创建两个线程,每个线程将共享的全局变量counter做N次加一,所以我们预期最终的结果将会是2N。

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

#include "common.h"
#include "common_threads.h"

int max;
volatile int counter = 0; // shared global variable

void *mythread(void *arg) {
    char *letter = arg;
    int i; // stack (private per thread) 
    printf("%s: begin [addr of i: %p]\n", letter, &i);
    for (i = 0; i < max; i++) {
	counter = counter + 1; // shared: only one
    }
    printf("%s: done\n", letter);
    return NULL;
}
                                                                             
int main(int argc, char *argv[]) {                    
    if (argc != 2) {
	fprintf(stderr, "usage: main-first <loopcount>\n");
	exit(1);
    }
    max = atoi(argv[1]);

    pthread_t p1, p2;
    printf("main: begin [counter = %d] [%x]\n", counter, 
	   (unsigned int) &counter);
    Pthread_create(&p1, NULL, mythread, "A"); 
    Pthread_create(&p2, NULL, mythread, "B");
    // join waits for the threads to finish
    Pthread_join(p1, NULL); 
    Pthread_join(p2, NULL); 
    printf("main: done\n [counter: %d]\n [should: %d]\n", 
	   counter, max*2);
    return 0;
}
复制代码

有的时候,结果和我们预期的一致:

有时候又不一致:

N越大偏离地越离谱。

上述问题的根源:不可控的调度

counter加1的操作,生成的汇编代码如下:

mov 0x8049a1c, %eax
add $0x1, %eax
mov %eax, 0x8049a1c
复制代码
  • 假设counter变量在内存地址0x8049a1c处。
  • mov 0x8049a1c, %eax把内存0x8049a1c的值加载到寄存器%eax
  • add $0x1, %eax将寄存器%eax地值加一。
  • mov %eax, 0x8049a1c把寄存器%eax地值写入0x8049a1c

想象一下两个线程一起运行上面这段代码时会发生什么不可预期的情况:

假如现在counter的值为50,T1执行了前面两行,那么它寄存器的值将会是51。如果这时候 interrupt 发生,操作系统会把T1地当前状态保存到它的 TCB,当然这也就包括了它的寄存器%eax的值。所以,当前的情况是:T1寄存器的值为51,但是内存0x8049a1c处的值还是50,因为 T1还没来得及把值写到内存里面去。

这个时候一个 context switch 就会发生,操作系统有两种选择:运行 T1或者运行 T2。如果是继续运行 T1,一切都是正常的,T1会接着执行第三行代码,把值51写入内存相应位置。这里我们假设操作系统会运行 T2,那问题就来了。T1执行第一行的时候,内存中的值还是51,如果 T2成功执行了完整的三行代码,就会把值51写入内存。

又一次 context switch 发生,这次假设是 T1运行。T1接着运行第三行代码,把自己独立寄存器的值(这里是51)写入内存,内存的值将还是51。

发现了吗?两个线程做了两次相加操作,但是counter的值只增加了1。

假如上诉汇编代码在内存中的地址如下(第一条在地址100处):

100 mov 0x8049a1c, %eax
105 add $0x1, %eax
108 mov %eax, 0x8049a1c
复制代码

下面这个图展示了上述发生的过程:执行两次相加,但是结果只增加了1。

对原子化操作的渴望

解决上诉问题的思路很简单,那就是原子化执行。如果加一的操作能用一条指令完成,那就不存在interrupt 带来的问题了:如果这条指令没有"中间状态",事情就能够往我们预期的方向发展。

memory-add 0x8049a1c, $0x1
复制代码

但是现实是,没有这么多强大的原子化指令。所以就需要硬件提供一些指令,让我们实现同步的功能,这些是我们后面将要学习的内容。

如果你像我一样真正热爱计算机科学,喜欢研究底层逻辑,欢迎关注我的微信公众号:

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值