← 返回首页
读书笔记

《Operating System:Three Easy Pieces》第二十六章 并发:介绍

本章将介绍为单个运行进程提供的新抽象:线程(thread)。

经典观点是一个程序只有一个执行点(一个程序计数器,用来存放要执行的指令),但多线程(multi-threaded)程序会有多个执行点(多个程序计数器,每个都用于取指令和执行)。换一个角度来看,每个线程类似于独立的进程,只有一点区别:它们共享地址空间,从而能够访问相同的数据。

单个线程的状态与进程状态非常类似。

线程和进程之间的另一个主要区别在于栈。在简单的传统进程地址空间模型 [我们现在可以称之为单线程(single-threaded)进程] 中,只有一个栈,通常位于地址空间的底部。

在多线程的进程中,

实例:线程创建

c
#include <stdio.h> #include <assert.h> #include <pthread.h> void*mythread(void*arg) { printf("%s\n", (char*) arg); return NULL; } int main(int argc,char*argv[]) { pthread_t p1, p2; int rc; printf("main: begin\n"); rc= pthread_create(&p1, NULL, mythread, "A"); assert(rc== 0); rc= pthread_create(&p2, NULL, mythread, "B"); assert(rc== 0); // join waits for the threads to finish rc= pthread_join(p1, NULL); assert(rc== 0); rc= pthread_join(p2, NULL); assert(rc== 0); printf("main: end\n"); return 0; }

但请注意,这种排序不是唯一可能的顺序。实际上,给定一系列指令,有很多可能的顺序,这取决于调度程序决定在给定时刻运行哪个线程。例如,创建一个线程后,它可能会立即运行

如你所见,线程创建有点像进行函数调用。然而,并不是首先执行函数然后返回给调用者,而是为被调用的例程创建一个新的执行线程,它可以独立于调用者运行,可能在从创建者返回之前运行,但也许会晚得多。(不确定性) 从这个例子中也可以看到,线程让生活变得复杂:已经很难说出什么时候会运行了! 没有并发,计算机也很难理解。遗憾的是,有了并发,情况变得更糟,而且糟糕得多。

为什么更糟糕:共享数据

c
1 #include <stdio.h> 2 #include <pthread.h> 3 #include "mythreads.h" 4 5 static volatile int counter = 0; 6 7 // 8 // mythread() 9 // 10 // Simply adds 1 to counter repeatedly, in a loop 11 // No, this is not how you would add 10,000,000 to 12 // a counter, but it shows the problem nicely. 13 // 14 void * 15 mythread(void *arg) 16 { 17 printf("%s: begin\n", (char *) arg); 18 int i; 19 for (i = 0; i < 1e7; i++) { 20 counter = counter + 1; 21 } 22 printf("%s: done\n", (char *) arg); 23 return NULL; 24 } 25 26 // 27 // main() 28 // 29 // Just launches two threads (pthread_create) 30 // and then waits for them (pthread_join) 31 // 32 int 33 main(int argc, char *argv[]) 34 { 35 pthread_t p1, p2; 36 printf("main: begin (counter = %d)\n", counter); 37 Pthread_create(&p1, NULL, mythread, "A"); //Pthread_create为封装好的函数 38 Pthread_create(&p2, NULL, mythread, "B"); 39 40 // join waits for the threads to finish 41 Pthread_join(p1, NULL); 42 Pthread_join(p2, NULL); 43 printf("main: done with both (counter = %d)\n", counter); 44 return 0; 45 }

我们现在可以看看每个工作线程正在尝试做什么:向共享变量计数器添加一个数字,并在循环中执行 1000 万(107)次。因此,预期的最终结果是:20000000。 我们现在编译并运行该程序,观察它的行为。有时候,一切如我们预期的那样:

c
prompt> gcc -o main main.c -Wall -pthread prompt> ./main main: begin (counter = 0) A: begin B: begin A: done B: done main: done with both (counter = 20000000)

遗憾的是,即使是在单处理器上运行这段代码,也不一定能获得预期结果。有时会这样:

c
prompt> ./main main: begin (counter = 0) A: begin B: begin A: done B: done main: done with both (counter = 19345221)

让我们再试一次,看看我们是否疯了。毕竟,计算机不是应该产生确定的(deterministic) 结果,像教授讲的那样?!也许教授一直在骗你?(大口地吸气)

c
prompt> ./main main: begin (counter = 0) A: begin B: begin A: done B: done main: done with both (counter = 19221041)

每次运行不但会产生错误,而且得到不同的结果!有一个大问题:为什么会发生这种情况?

原子性愿望

我们要做的是要求硬件提供一些有用的指令,可以在这些指令上构建一个通用的集合,即所谓的同步原语(synchronization primitive)。通过使用这些硬件同步原语,加上操作系统的一些帮助,将能够构建多线程代码,以同步受控的方式访问临界区,从而可靠地产生正确的结果—— 尽管有并发执行的挑战。

参考

本文由 GJJ 创作,内容来源于 Notion 数据库,随时可在 Notion 中编辑更新。 本站由 DeepSeek-v4-flash 辅助构建,项目参考 NotionNext

← 返回首页
61
文章
6
标签
3
分类
962
运行天数