Linux 内核101:[译]并发导论
原文:Operating Systems: Three Easy Pieces:Concurrency: An Introduction
進(jìn)程和線程
進(jìn)程和線程在底層的區(qū)別
在單線程進(jìn)程中,只有一個execution flow,進(jìn)程只能從一個 PC(Program counter)里面獲取指令。多線程的進(jìn)程有多個 execution flow,能夠從多個 PCs 獲取指令。要簡單的對比一下進(jìn)程和線程的話,就是每個 thread 很像一個獨立的進(jìn)程,但是同一個進(jìn)程里面的線程共享一部分?jǐn)?shù)據(jù),同時共享地址空間。
操作系統(tǒng)如何調(diào)度線程
每個線程有自己獨立的PC和寄存器。也就是說,運行在同一個核的的兩個線程 T1、T2,當(dāng)CPU 從 T1切換到 T2執(zhí)行的時候,會像進(jìn)程切換一樣,發(fā)生一次context switch。 CPU 需要把 T1 的運行狀態(tài)和寄存器的數(shù)據(jù)保存起來,然后 restore T2的狀態(tài)和寄存器數(shù)據(jù)。對于進(jìn)程,狀態(tài)被保存在 PCB(process control block);對于線程,使用的是 TCBs(Thread control block)。
線程和進(jìn)程切換還有一點不同是:如果操作系統(tǒng)調(diào)度切換的兩個線程是屬于同一個進(jìn)程的,那么地址空間就不需要切換,因為線程間是共享同一個地址空間的。這也就意味著線程切換相對于進(jìn)程切換更加輕量級。
進(jìn)程和線程能實現(xiàn)并行
首先,一個核在同一時刻只能執(zhí)行一個進(jìn)程(或者線程,下同)。如下圖左所示。
要在同一時刻運行多個進(jìn)程,必須要有多個核。因為操作系統(tǒng)有一套調(diào)度系統(tǒng),所以能把多個進(jìn)程分配給多個核。
線程調(diào)度全看操作系統(tǒng)喜歡
我們假設(shè)下面這個例子中:只有一個核。
下面這個程序主線程先用Pthread_create創(chuàng)建兩個線程,這兩個線程的作用就是簡單的打印A或者B,然后主線程調(diào)用Pthread_join等待兩個線程結(jié)束,最后主線程退出。
#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; } 復(fù)制代碼有兩點:
可能會出現(xiàn)下面三種情況:
第一種:A 在 B 之前被執(zhí)行。
第二種:線程被創(chuàng)建之后立即被執(zhí)行,Pthread_join將會立即返回。
第三種: B 在 A 之前被執(zhí)行。
從這個例子我們可以看到,線程的創(chuàng)建和調(diào)度是由操作系統(tǒng)來調(diào)度地,你無法判斷哪個線程會先被執(zhí)行,什么時候被執(zhí)行。
線程共享變量帶來的問題
下面這個程序創(chuàng)建兩個線程,每個線程將共享的全局變量counter做N次加一,所以我們預(yù)期最終的結(jié)果將會是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; } 復(fù)制代碼有的時候,結(jié)果和我們預(yù)期的一致:
有時候又不一致:
N越大偏離地越離譜。
上述問題的根源:不可控的調(diào)度
將 counter加1的操作,生成的匯編代碼如下:
mov 0x8049a1c, %eax add $0x1, %eax mov %eax, 0x8049a1c 復(fù)制代碼- 假設(shè)counter變量在內(nèi)存地址0x8049a1c處。
- mov 0x8049a1c, %eax把內(nèi)存0x8049a1c的值加載到寄存器%eax。
- add $0x1, %eax將寄存器%eax地值加一。
- mov %eax, 0x8049a1c把寄存器%eax地值寫入0x8049a1c。
想象一下兩個線程一起運行上面這段代碼時會發(fā)生什么不可預(yù)期的情況:
假如現(xiàn)在counter的值為50,T1執(zhí)行了前面兩行,那么它寄存器的值將會是51。如果這時候 interrupt 發(fā)生,操作系統(tǒng)會把T1地當(dāng)前狀態(tài)保存到它的 TCB,當(dāng)然這也就包括了它的寄存器%eax的值。所以,當(dāng)前的情況是:T1寄存器的值為51,但是內(nèi)存0x8049a1c處的值還是50,因為 T1還沒來得及把值寫到內(nèi)存里面去。
這個時候一個 context switch 就會發(fā)生,操作系統(tǒng)有兩種選擇:運行 T1或者運行 T2。如果是繼續(xù)運行 T1,一切都是正常的,T1會接著執(zhí)行第三行代碼,把值51寫入內(nèi)存相應(yīng)位置。這里我們假設(shè)操作系統(tǒng)會運行 T2,那問題就來了。T1執(zhí)行第一行的時候,內(nèi)存中的值還是51,如果 T2成功執(zhí)行了完整的三行代碼,就會把值51寫入內(nèi)存。
又一次 context switch 發(fā)生,這次假設(shè)是 T1運行。T1接著運行第三行代碼,把自己獨立寄存器的值(這里是51)寫入內(nèi)存,內(nèi)存的值將還是51。
發(fā)現(xiàn)了嗎?兩個線程做了兩次相加操作,但是counter的值只增加了1。
假如上訴匯編代碼在內(nèi)存中的地址如下(第一條在地址100處):
100 mov 0x8049a1c, %eax 105 add $0x1, %eax 108 mov %eax, 0x8049a1c 復(fù)制代碼下面這個圖展示了上述發(fā)生的過程:執(zhí)行兩次相加,但是結(jié)果只增加了1。
對原子化操作的渴望
解決上訴問題的思路很簡單,那就是原子化執(zhí)行。如果加一的操作能用一條指令完成,那就不存在interrupt 帶來的問題了:如果這條指令沒有"中間狀態(tài)",事情就能夠往我們預(yù)期的方向發(fā)展。
memory-add 0x8049a1c, $0x1 復(fù)制代碼但是現(xiàn)實是,沒有這么多強大的原子化指令。所以就需要硬件提供一些指令,讓我們實現(xiàn)同步的功能,這些是我們后面將要學(xué)習(xí)的內(nèi)容。
如果你像我一樣真正熱愛計算機科學(xué),喜歡研究底層邏輯,歡迎關(guān)注我的微信公眾號:
總結(jié)
以上是生活随笔為你收集整理的Linux 内核101:[译]并发导论的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: (1)计算机的组成及其功能
- 下一篇: 关于Linux的基础中的基础和一些基础小