原文: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 finishPthread_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 variablevoid *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 finishPthread_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
复制代码

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

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

Linux 内核101:[译]并发导论相关推荐

  1. 编译3.0的linux内核,1-3-编译Linux内核

    1-3-编译Linux内核 1.将Linux源码包拷贝到共享文件夹. 2.进入共享文件夹. 3.解压,命令#tar xvfj Kernel_3.0.8_TQ210_for_Linux_v2.2.tar ...

  2. Linux内核争抢式并发在SMP多核扩展上的不足

    本文来自:<被神话的Linux, 一文带你看清Linux在多核可扩展性设计上的不足> 我们先来看一段来自猛士王垠的话: 跟有些人聊操作系统是件闹心的事,因为我往往会抛弃一些术语和概念,从零 ...

  3. Linux 内核101:[译]地址空间发展简史

    原文:The Abstraction: Address Spaces 在早期,构建计算机系统是很简单的.为什么?你可能会想.因为用户期望值不高.都是那些『该死』的用户,想要一个"易用&quo ...

  4. 基于Nginx实现10万+并发,你应该做的Linux内核优化

    由于默认的linux内核参数考虑的是最通用场景,这明显不符合用于支持高并发访问的Web服务器的定义,所以需要修改Linux内核参数,是的Nginx可以拥有更高的性能: 在优化内核时,可以做的事情很多, ...

  5. Linux内核的并发与竞态、信号量、互斥锁、自旋锁

    /************************************************************************************ *本文为个人学习记录,如有错 ...

  6. 为支持nginx高并发而修改的一些Linux内核参数

    前言 由于默认的Linux内核参数考虑的是最通用的场景,这明显不符合用于支持高并发访问的Web服务器定义,所以需要修改Linux内核参数,使的nginx拥有更高的性能. 在优化内核时, 可以做的事情很 ...

  7. linux内核工程导论,Linux内核工程导论——内存管理(3)

    Linux内核工程导论--内存管理(三) 用户端内核内存参数调整 /proc/sys/vm/ (需要根据内核版本调整) 交换相关 swap_token_timeout Thisfile contain ...

  8. [译] LINUX内核内存屏障

    ================= LINUX内核内存屏障 ================= By: David Howells <dhowells@redhat.com> Paul E ...

  9. linux内核并发教程,修改Linux内核参数提高Nginx服务器并发性能

    当linux下Nginx达到并发数很高,TCP TIME_WAIT套接字数量经常达到两.三万,这样服务器很容易被拖死.事实上,我们可以简单的通过修改Linux内核参数,可以减少Nginx服务器 的TI ...

最新文章

  1. 快速交付 敏捷开发的特点_什么是敏捷开发?它有什么特点
  2. Altair Compose2020中文版
  3. MSVCRTD.lib(crtexew.obj) : error LNK2019: 无法解析的外部符号 _WinMain@16
  4. boost::container实现显式实例化平面集测试程序
  5. 初次联系导师短信模板_2020考研复试:提前联系导师的6点注意事项(附邮件模板)...
  6. linux安装多个mysql数据库_linux下多个mysql5.7.19(tar.gz)安装图文教程
  7. php xcat createadmin,php xcat update升级后出错,不知怎么弄
  8. mysql最大值最小值_mysql最大值,最小值,总和查询与计数查询
  9. linux system更好方法,Linux将程序添加到服务的方法(通用【但最好还是用systemd】)...
  10. 多线程-AbstractQueuedSynchronizer(AQS)
  11. 设置php中字符编码_php如何设置字符编码
  12. 光流 速度_科学家研制新型“时空波包”激光束 能够以相同速度穿越不同的介质...
  13. 餐饮营销策划案例合集(共18份)
  14. 2018.2 IDEAIU版激活说明
  15. Linux文件补全功能,Linux系统自动补全命令有哪些
  16. 人民币金额小写转换大写
  17. Unity学习笔记:Rule Tile、Advance Rule Overide Tile、Rule Override Tile的用法【By Chutianto】
  18. Windows 10打开Java控制面板
  19. 使用Python按时间顺序批量重命名文件
  20. mysql 电商实战_SQL电商数据分析实战

热门文章

  1. 以 OSGi 包的形式开发和部署 Web 服务
  2. 深入理解多线程(五)—— Java虚拟机的锁优化技术
  3. 【深入Java虚拟机】之四:类加载机制
  4. Eclipse中的checkstyle插件
  5. 【最优化方法】穷举法 vs. 爬山法 vs. 模拟退火算法 vs. 遗传算法 vs. 蚁群算法
  6. 8个实用而有趣Bash命令提示行
  7. 二倍图(精灵图的用法)
  8. Spring Boot 单元测试二三事
  9. Openstack平台搭建(先电版)
  10. mysql启动warning: World-writable config file