日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 运维知识 > linux >内容正文

linux

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

發(fā)布時間:2025/3/21 linux 35 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Linux 内核101:[译]并发导论 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

原文: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ù)制代碼

有兩點:

  • 一個線程先被創(chuàng)建,但它不一定會先被執(zhí)行。
  • 一個線程被創(chuàng)建,但它不一定會立即被執(zhí)行。
  • 可能會出現(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)容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。