Linux 內核101:[譯]併發導論

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

進程和線程

進程和線程在底層的區別

在單線程進程中,只有一個execution flow,進程只能從一個 PC(Program counter)裏面獲取指令。多線程的進程有多個 execution flow,可以從多個 PCs 獲取指令。要簡單的對比一下進程和線程的話,就是每一個 thread 很像一個獨立的進程,可是同一個進程裏面的線程共享一部分數據,同時共享地址空間微信

操做系統如何調度線程

每一個線程有本身獨立的PC和寄存器。也就是說,運行在同一個核的的兩個線程 T一、T2,當CPU 從 T1切換到 T2執行的時候,會像進程切換同樣,發生一次context switch。 CPU 須要把 T1 的運行狀態和寄存器的數據保存起來,而後 restore T2的狀態和寄存器數據。對於進程,狀態被保存在 PCB(process control block);對於線程,使用的是 TCBs(Thread control block)。多線程

線程和進程切換還有一點不一樣是:若是操做系統調度切換的兩個線程是屬於同一個進程的,那麼地址空間就不須要切換,由於線程間是共享同一個地址空間的。這也就意味着線程切換相對於進程切換更加輕量級。oop

進程和線程能實現並行

首先,一個核在同一時刻只能執行一個進程(或者線程,下同)。以下圖左所示。學習

要在同一時刻運行多個進程,必需要有多個核。由於操做系統有一套調度系統,因此能把多個進程分配給多個核。ui

線程調度全看操做系統喜歡

咱們假設下面這個例子中:只有一個核。spa

下面這個程序主線程先用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. 一個線程被建立,但它不必定會當即被執行。

可能會出現下面三種狀況:3d

第一種: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
複製代碼

可是現實是,沒有這麼多強大的原子化指令。因此就須要硬件提供一些指令,讓咱們實現同步的功能,這些是咱們後面將要學習的內容。

若是你像我同樣真正熱愛計算機科學,喜歡研究底層邏輯,歡迎關注個人微信公衆號:

相關文章
相關標籤/搜索