2017-2018-1 20155312 學習《深刻理解計算機系統》第十二章:併發編程

併發編程

目錄


併發編程

在本章的學習中,咱們的學習目標以下:程序員

  1. 掌握三種併發的方式:進程、線程、I/O多路複用
  2. 掌握線程控制及相關係統調用
  3. 掌握線程同步互斥及相關係統調用

併發的概念:數據庫

若是邏輯控制流在時間上是重疊的,那麼它們就是併發的。編程

應用級併發的應用:安全

  1. 訪問慢速I/O設備。當一個應用正在等待來自慢速I/O設備(例如磁盤)的數據到達時,內核會運行其餘進程,使CPU保持繁忙。這是經過交替執行I/O請求和其餘有用的工做來使用併發。
  2. 與人交互。用戶但願計算機有同時執行多個任務的能力。每次用戶請求某種操做(如單擊鼠標)時,一個獨立的併發邏輯流被建立來執行這個操做。
    經過推遲工做來下降延遲。
  3. 服務多個網絡客戶端。咱們指望服務器每秒爲成百上千的客戶端提供服務,併發服務器爲每一個客戶端建立一個單獨的邏輯流。
  4. 在多核機器上進行並行計算。被劃分紅併發流的應用程序一般在多核機器上比在單處理器上運行得快,由於這些流會並行執行,而不是交錯執行。

現代操做系統中三種構造併發程序的方法:服務器

  1. 進程。每一個邏輯流都是一個進程,由內核來調度和維護。
  2. I/O多路複用。在這種形式中,應用程序在一個進程的上下文中顯式地調度它們本身的邏輯流。邏輯流被模型化爲狀態機。由於程序是一個單獨的進程,因此全部的流都共享同一個地址空間。
  3. 線程。線程是運行在單一進程上下文中的邏輯流,由內核進行調度

線程與進程的區分:網絡

  • 進程是具備必定獨立功能的程序關於某個數據集合上的一次運行活動,是系統進行資源分配和調度的一個獨立單位;
  • 線程是進程的一個實體,是CPU調度和分派的基本單位。它是比進程更小的能獨立運行的基本單位.線程本身基本上不擁有系統資源,只擁有一點在運行中必不可少的資源(如程序計數器,一組寄存器和棧),一個線程能夠建立和撤銷另外一個線程;

返回目錄多線程

1、基於進程的併發編程

構造併發編程最簡單的方法就是用進程,使用那些你們都很熟悉的函數,像fork、exec和waitpid。併發

步驟app

  1. 服務器監聽一個監聽描述符上的鏈接請求。
  2. 服務器接受了客戶端1的鏈接請求,並返回一個已鏈接描述符。
  3. 在接受了鏈接請求以後,服務器派生一個子進程,這個子進程得到服務器描述符表的完整拷貝。子進程關閉它的拷貝中的監聽描述符3,而父進程關閉它的已鏈接描述符4的拷貝,由於再也不須要這些描述符了。
  4. 子進程正忙於爲客戶端提供服務,父進程繼續監聽新的請求。

四個步驟的示意圖以下:socket

注意:子進程關閉監聽描述符和父進程關閉已鏈接描述符是很重要的,由於父子進程共用同一文件表,文件表中的引用計數會增長,只有當引用計數減爲0時,文件描述符纔會真正關閉。因此,若是父子進程不關閉不用的描述符,將永遠不會釋放這些描述符,最終將引發存儲器泄漏而最終消耗盡能夠的存儲器,是系統崩潰。

使用進程併發編程要注意的問題:

  1. 首先,一般服務器會運行很長的時間,因此咱們必需要包括一個SIGCHLD處理程序,來回收僵死子進程的資源。由於當SIGCHLD處理程序執行時,SIGCHLD信號時阻塞的,而Unix信號時不排隊的,因此SIGCHLD處理程序必須準備好回收多個僵死子進程的資源。
  2. 其次,子進程必須關閉它們各自的connfd拷貝。就像咱們已經提到過的,這對父進程而言尤其重要,它必須關閉它的已鏈接描述符,以免存儲器泄漏。
  3. 最後,由於套接字的文件表表項中的引用計數,直到父子進程的connfd都關閉了,到客戶端的鏈接纔會終止。

進程的優劣:

對於在父、子進程間共享狀態信息,進程有一個很是清晰的模型:共享文件表,可是不共享用戶地址空間。進程有獨立的地址控件愛你既是優勢又是缺點。因爲獨立的地址空間,因此進程不會覆蓋另外一個進程的虛擬存儲器。可是另外一方面進程間通訊就比較麻煩,至少開銷很高。

編寫的代碼以下:

#include "csapp.h"  
void echo(int connfd);  
  
void sigchld_handler(int sig)   
{  
    while (waitpid(-1, 0, WNOHANG) > 0)  
    ;  
    return;  
}  
  
int main(int argc, char **argv)   
{  
    int listenfd, connfd, port, clientlen=sizeof(struct sockaddr_in);  
    struct sockaddr_in clientaddr;  
  
    if (argc != 2) {  
    fprintf(stderr, "usage: %s <port>\n", argv[0]);  
    exit(0);  
    }  
    port = atoi(argv[1]);  
  
    Signal(SIGCHLD, sigchld_handler);  
    listenfd = Open_listenfd(port);  
    while (1) {  
    connfd = Accept(listenfd, (SA *) &clientaddr, &clientlen);  
    if (Fork() == 0) {   
        Close(listenfd); /* Child closes its listening socket */  
        echo(connfd);    /* Child services client */  
        Close(connfd);   /* Child closes connection with client */  
        exit(0);         /* Child exits */  
    }  
    Close(connfd); /* Parent closes connected socket (important!) */  
    }  
}

返回目錄


2、基於i/o多路複用的併發編程

面對困境——服務器必須響應兩個互相獨立的I/O事件:

  1. 網絡客戶端發起的鏈接請求
  2. 用戶在鍵盤上鍵入的命令

針對這種困境的一個解決辦法就是I/O多路複用技術。

I/O多路複用技術基本思想是:

可使用select、poll和epoll來實現I/O複用。使用select函數,要求內核掛起進程,只有在一個或者多個I/O事件發生後,纔將控制返給應用程序。

select函數以下圖所示:

使用select函數的過程以下:

  1. 初始化fd_set集
  2. 調用select
  3. 根據fd_set集合如今的值,判斷是哪一種I/O事件

基於i/o多路複用的併發事件驅動服務器:

I/O多路複用能夠用作併發事件驅動程序的基礎,在事件驅動程序中,流是由於某種事件而前進的,通常概念是將邏輯流模型化爲狀態機,不嚴格地說,一個狀態機就是一組狀態,輸入事件和轉移,其中轉移就是將狀態和輸入事件映射到狀態,每一個轉移都將一個(輸入狀態,輸入事件)對映射到一個輸出狀態,自循環是同一輸入和輸出狀態之間的轉移,一般把狀態機畫成有向圖,其中節點表示狀態,有向弧表示轉移,而弧上的標號表示輸人事件,一個狀態機從某種初始狀態開始執行,每一個輸入事件都會引起一個從當前狀態到下一狀態的轉移,對於每一個新的客戶端k,基於I/O多路複用的併發服務器會建立一個新的狀態機S,並將它和已鏈接描述符d聯繫起來。

I/O多路複用技術的優勢:

  1. 使用事件驅動編程,這樣比基於進程的設計給了程序更多的對程序行爲的控制。

  2. 一個基於I/O多路複用的事件驅動服務器是運行在單一進程上下文中的,所以每一個邏輯流都訪問該進程的所有地址空間。這使得在流之間共享數據變得很容易。一個與做爲單進程運行相關的優勢是,你能夠利用熟悉的調試工具,例如GDB來調試你的併發服務器,就像對順序程序那樣。最後,事件驅動設計經常比基於進程的設計要高效不少,由於它們不須要進程上下文切換來調度新的流。

缺點:

事件驅動設計的一個明星的缺點就是編碼複雜。咱們的事件驅動的併發服務器須要比基於進程的多三倍。不幸的是,隨着併發粒度的減少,複雜性還會上升。這裏的粒度是指每一個邏輯流每一個時間片執行的指令數量。

基於事件的設計的另外一重大的缺點是它們不能充分利用多核處理器。

編寫的代碼以下:

#include "csapp.h"  
void echo(int connfd);  
void command(void);  
  
int main(int argc, char **argv)   
{  
    int listenfd, connfd, port, clientlen = sizeof(struct sockaddr_in);  
    struct sockaddr_in clientaddr;  
    fd_set read_set, ready_set;  
  
    if (argc != 2) {  
    fprintf(stderr, "usage: %s <port>\n", argv[0]);  
    exit(0);  
    }  
    port = atoi(argv[1]);  
    listenfd = Open_listenfd(port);  
  
    FD_ZERO(&read_set);   
    FD_SET(STDIN_FILENO, &read_set);   
    FD_SET(listenfd, &read_set);   
  
    while (1) {  
    ready_set = read_set;  
    Select(listenfd+1, &ready_set, NULL, NULL, NULL);   
    if (FD_ISSET(STDIN_FILENO, &ready_set))  
        command(); /* read command line from stdin */  
    if (FD_ISSET(listenfd, &ready_set)) {  
        connfd = Accept(listenfd, (SA *)&clientaddr, &clientlen);  
        echo(connfd); /* echo client input until EOF */  
    }  
    }  
}  
  
void command(void) {  
    char buf[MAXLINE];  
    if (!Fgets(buf, MAXLINE, stdin))  
    exit(0); /* EOF */  
    printf("%s", buf); /* Process the input command */  
}

返回目錄


3、基於線程的併發編程

在使用進程併發編程中,咱們爲每一個流使用了單獨的進程。內核會自動調用每一個進程。每一個進程有它本身的私有地址空間,這使得流共享數據很困難。在使用I/O多路複用的併發編程中,咱們建立了本身的邏輯流,並利用I/O多路複用來顯式地調度流。由於只有一個進程,全部的流共享整個地址空間。而基於線程的方法,是這兩種方法的混合。

線程執行模型:

線程和進程的執行模型有些類似。每一個進程的聲明週期都是一個線程,咱們稱之爲主線程。線程是對等的,主線程跟其餘線程的區別就是它先執行。

線程就是運行在進程上下文的邏輯流,以下圖所示。線程由內核自動調度。每一個線程都有它本身的線程上下文,包括一個惟一的整數線程ID、棧、棧指針、程序計數器、通用目的寄存器和條件碼。全部的運行在一個進程裏的線程共享該進程的整個虛擬地址空間。

基於線程的邏輯流結合了基於線程和基於I/O多路複用的流的特性。同進程同樣,線程由內核自動調度,而且內核經過一個整數ID來標識線程。同基於I/O多路複用的流同樣,多個線程運行在單一進程的上下文中,所以共享這個線程虛擬地址空間的整個內容,包括它的代碼、數據、堆、共享庫和打開的文件。

posix線程

POSIX線程是在C程序中處理線程的一個標準接口。它最先出如今1995年,並且在大多數Unix系統上均可用。Pthreads定義了大約60個函數,容許程序建立、殺死和回收線程,與對等線程安全地共享數據,還能夠通知對等線程系統狀態的變化。

建立線程:

pthread_create函數用來建立其餘進程。

pthread_create函數建立一個新的線程,並帶着一個輸入變量arg,在新線程的上下文中運行線程例程f。能用attr參數來改變新建立線程的默認屬性。

當pthread_create返回時,參數tid包含新建立線程的ID。

獲取自身ID:

pthread_self函數用來獲取自身ID。

終止線程:

一個線程是如下列方式之一來終止的:

  1. 當頂層的線程例程返回時,線程會隱式地終止
  2. 經過調用pthread_exit函數,線程會顯式地終止。若是主線程調用pthread_exit,它會等待全部其餘對等線程終止,而後再終止主線程和這個進程,返回值爲thread_return。
  3. 某個對等線程調用exit函數,則函數終止進程和全部與該進程相關的線程;
  4. 另外一個對等線程調用以當前ID爲參數的函數ptherad_cancel來終止當前線程。

pthread_exit函數和ptherad_cancel函數函數以下所示:

回收已終止線程的資源:

pthread_join函數會終止,直到線程tid終止。和wait不一樣,該函數只能回收指定id的線程,不能回收任意線程。

** 分離線程:**

在任何一個時間點上,線程是可結合的或者是分離的。一個可結合的線程可以被其餘線程收回其資源和殺死。在被其餘線程回收以前,它的存儲器資源(例如棧)式沒有被釋放的。相反,一個分離的線程是不能被其餘線程回收和殺死的。它的存儲器資源在它終止時由系統自動釋放。

默認狀況下,線程被建立成可結合的。爲了不存儲器泄漏,每一個可結合線程都應該要麼被其餘線程顯式地收回,要麼經過調用pthread_detach函數被分離。

pthread_detach函數分離可結合線程tid。線程可以經過以pthread_self()爲參數的pthread_detach調用來分離它們本身。

初始化線程:

pthread_once()函數用來初始化多個線程共享的全局變量。

編寫代碼以下:

/*  
 * echoservert.c - A concurrent echo server using threads 
 */  
/* $begin echoservertmain */  
#include "csapp.h"  
  
void echo(int connfd);  
void *thread(void *vargp);  
  
int main(int argc, char **argv)   
{  
    int listenfd, *connfdp, port, clientlen=sizeof(struct sockaddr_in);  
    struct sockaddr_in clientaddr;  
    pthread_t tid;   
  
    if (argc != 2) {  
    fprintf(stderr, "usage: %s <port>\n", argv[0]);  
    exit(0);  
    }  
    port = atoi(argv[1]);  
  
    listenfd = Open_listenfd(port);  
    while (1) {  
    connfdp = Malloc(sizeof(int));  
    *connfdp = Accept(listenfd, (SA *) &clientaddr, &clientlen);  
    Pthread_create(&tid, NULL, thread, connfdp);  
    }  
}  
  
/* thread routine */  
void *thread(void *vargp)   
{    
    int connfd = *((int *)vargp);  
    Pthread_detach(pthread_self());   
    Free(vargp);  
    echo(connfd);  
    Close(connfd);  
    return NULL;  
}

返回目錄


4、多線程程序中的共享變量

每一個線程都有它本身獨自的線程上下文,包括線程ID、棧、棧指針、程序計數器、條件碼和通用目的寄存器值。每一個線程和其餘線程一塊兒共享進程上下文的剩餘部分。寄存器是從不共享的,而虛擬存儲器老是共享的。線程化的c程序中變量根據它們的存儲器類型被映射到虛擬存儲器:全局變量,本地自動變量(不共享),本地靜態變量。

線程存儲器模型:

一組併發線程運行在一個進程的上下文中。每一個線程都有它本身獨立的線程上下文,包括線程ID、棧、棧指針、程序計數器、條件碼和通用目的寄存器值。每一個線程和其餘線程一塊兒共享進程上下文的剩餘部分。這包括整個用戶虛擬地址空間,它是由只讀文本代碼、讀/寫數據、堆以及全部的共享庫代碼和數據區域組成的。線程也共享一樣的打開文件的集合。

從實際操做的角度來講,讓一個線程去讀或寫另外一個線程的寄存器值是不可能的。另外一方面,任何線程均可以訪問共享虛擬存儲器的任意位置。若是某個線程修改了一個存儲器位置,那麼其餘每一個線程最終都能在它讀這個位置時發現這個變化。所以,寄存器是從不共享的,而虛擬存儲器老是共享的

各自獨立的線程棧的存儲器模型不是那麼整齊清楚的。這些棧被保存在虛擬地址空間的棧區域中,而且一般是被相應的線程獨立地訪問的。咱們說一般而不是老是,是由於不一樣的線程棧是不對其餘線程設防的因此,若是個線程以某種方式獲得個指向其餘線程棧的指慧:那麼它就能夠讀寫這個棧的任何部分。

線程化的C程序中變量根據它們的存儲類型被映射到虛擬存儲器:

  1. 全局變量。全局變量是定義在函數以外的變量,在運行時,虛擬存儲器的讀/寫區域域只包含每一個全局變量的一個實例,任何線程均可以引用。例如第5行聲明的全局變量ptr在虛擬存儲器的讀/寫區域中有個運行時實例,咱們只用變量名(在這裏就是ptr)來表示這個實例。
  2. 本地自動變量,本地自動變量就是定義在函數內部可是沒有static屬性的變量,在運行時,每一個線程的棧都包含它本身的全部本地自動變量的實例。即便當多個線程執行同一個線程例程時也是如此。例如,有個本地變量tid的實例,它保存在主線程的棧中。咱們用tid.m來表示這個實例
  3. 本地靜態變量

共享變量:

咱們說一個變量v是共享的,當且僅當它的一個實例被一個以上的線程引用。

編寫代碼以下:

#include "csapp.h"  
#define N 2  
void *thread(void *vargp);  
  
char **ptr;  /* global variable */  
  
int main()   
{  
    int i;    
    pthread_t tid;  
    char *msgs[N] = {  
    "Hello from foo",    
    "Hello from bar"     
    };  
  
    ptr = msgs;   
    for (i = 0; i < N; i++)    
        Pthread_create(&tid, NULL, thread, (void *)i);   
    Pthread_exit(NULL);   
}  
  
void *thread(void *vargp)   
{  
    int myid = (int)vargp;  
    static int cnt = 0;  
    printf("[%d]: %s (cnt=%d)\n", myid, ptr[myid], ++cnt);  
}

返回目錄


5、用信號量同步線程

  • 進程競爭資源首先必須解決互斥問題。某些資源必須互斥使用,如打印機、共享變量、表格、文件等。
    • 這類資源又稱爲臨界資源,訪問臨界資源的那段代碼稱爲臨界區(critical section)。
    • 任什麼時候刻,只容許一個進程進入臨界區,以此實現進程對臨界資源的互斥訪問。
  • 同步問題:當緩衝區爲空,打印進程沒法取數據;當緩衝區爲滿,計算進程沒法存數據。進程間須要協做。

臨界區使用規則

  • 有空讓進:若是臨界區空閒,則只要有進程申請就當即讓其進入;
  • 無空等待:每次只容許一個進程處於臨界區;
    多中擇一:當沒有進程在臨界區,而同時有多個進程要求進入臨界區,只能讓其中之一進入臨界區,其餘進程必須等待;
  • 讓權等待:進入臨界區的進程,不能在臨界區內長時間阻塞等待某事件,使其它進程在臨界區外無限期等待;
  • 不能限制進程的併發數量和執行進度。

互斥與同步的解決策略:信號量

OS以一個地位高於進程的管理者的角度來解決公有資源的使用問題,信號量就是OS提供的管理公有資源的有效手段。

  • 兩個或多個進程經過傳遞信號進行合做,能夠迫使進程在某個位置暫時中止執行(阻塞等待),直到它收到一個能夠「向前推動」的信號(被喚醒);
  • 將實現信號燈做用的變量稱爲信號量,常定義爲記錄型變量s,其一個域爲整型,另外一個域爲隊列,其元素爲等待該信號量的阻塞進程(FIFO)。

信號量的定義

type semaphore=record
        count: integer;
        queue: list of process
    end;
    var  s:semaphore;

對信號量的兩個原子操做:

進程進入臨界區以前,首先執行wait(s)原語,若s.count小於0,則進程調用阻塞原語,將本身阻塞,並插入到s.queue隊列排隊;

一旦其它某個進程執行了signal(s)原語中的s.count+1操做後,發現s.count ≤0,即阻塞隊列中還有被阻塞進程,則調用喚醒原語,把s.queue中第一個進程修改成就緒狀態,送就緒隊列,準備執行臨界區代碼。

  • wait操做用於申請資源(或使用權),進程執行wait原語時,可能會阻塞本身;
  • signal操做用於釋放資源(或歸還資源使用權),進程執行signal原語時,有責任喚醒一個阻塞進程。
wait(s) 
s.count :=s.count-1;
   if s.count<0 then 
   begin
    進程阻塞;
    進程進入s.queue隊列;
   end;
signal(s) 
s.count :=s.count+1;
   if s.count ≤0 then
   begin
     喚醒隊首進程;
     將進程從s.queue阻塞隊列中移出;
    end;
  • 信號量分爲:互斥信號量和資源信號量。
    • 互斥信號量用於申請或釋放資源的使用權,常初始化爲1;
    • 資源信號量用於申請或歸還資源,能夠初始化爲大於1的正整數,表示系統中某類資源的可用個數。

經典進程互斥與同步問題:

  • 生產者/消費者問題
    • 要求:必須保證對緩衝區的訪問是互斥的;還須要調度對緩衝區的訪問,即,若是緩衝區是滿的(沒有空的槽位),那麼生產者必須等待直到有一個空的槽位爲止,若是緩衝區是空的(即沒有可取的項目),那麼消費者必須等待直到有一個項目變爲可用。
  • 讀者/寫者問題
    • 修改對象的線程叫作寫者;只讀對象的線程叫作讀者。寫着必須擁有對對象的獨佔訪問,而讀者能夠和無限多個其餘讀者共享對象。讀者——寫者問題基本分爲兩類:第一類,讀者優先,要求不要讓讀者等待,除非已經把使用對象的權限賦予了一個寫者。換句話說,讀者不會由於有一個寫者等待而等待;第二類,寫者優先,要求必定能寫者準備好能夠寫,它就會盡量地完成它的寫操做。同第一類問題不一樣,在一個寫者後到達的讀者必須等待,即便這個寫者也是在等待。

基於預線程化的併發服務器

在如圖所示的併發服務器中,咱們爲每個新客戶端建立了一個新線程這種方法的缺點是咱們爲每個新客戶端建立一個新線程,致使不小的代價。一個基於預線程化的服務器試圖經過使用如圖所示的生產者-消費者模型來下降這種開銷。服務器是由一個主線程和一組工做者線程構成的。主線程不斷地接受來自客戶端的鏈接請求,並將獲得的鏈接描述符放在一個不限緩衝區中。每個工做者線程反覆地從共享緩衝區中取出描述符,爲客戶端服務,而後等待下一個描述符。

編寫代碼以下:

/*  
 * badcnt.c - An improperly synchronized counter program  
 */  
/* $begin badcnt */  
#include "csapp.h"  
  
#define NITERS 200000000  
void *count(void *arg);  
  
/* shared counter variable */  
unsigned int cnt = 0;  
  
int main()   
{  
    pthread_t tid1, tid2;  
  
    Pthread_create(&tid1, NULL, count, NULL);  
    Pthread_create(&tid2, NULL, count, NULL);  
    Pthread_join(tid1, NULL);  
    Pthread_join(tid2, NULL);  
  
    if (cnt != (unsigned)NITERS*2)  
    printf("BOOM! cnt=%d\n", cnt);  
    else  
    printf("OK cnt=%d\n", cnt);  
    exit(0);  
}  
  
/* thread routine */  
void *count(void *arg)   
{  
    int i;  
    for (i = 0; i < NITERS; i++)  
    cnt++;  
    return NULL;  
}

返回目錄


6、使用線程提升並行性

到目前爲止,在對併發的研究中,咱們都假設併發線程是在單處許多現代機器具備多核處理器。併發程序一般在這樣的機器上運理器系統上執行的。然而,在多個核上並行地調度這些併發線程,而不是在單個核順序地調度,在像繁忙的Web服務器、數據庫服務器和大型科學計算代碼這樣的應用中利用這種並行性是相當重要的。

返回目錄


7、其餘併發問題

1.四種不安全函數

(1):不保護共享變量的函數。

(2):保持跨越多個調用的狀態的函數。一個僞隨機數生成器是這類線程不安全函數的簡單例子。rand函數是線程不安全的,由於檔期調用的結果依賴於前次調用的中間結果。當調用srand爲rand設置了一個終止後,咱們從一個但線程中反覆地調用rand,可以預期獲得一個可重複的隨機數字序列。

(3):返回指向靜態變量的指針的函數。某些函數,例如ctime和gethostbyname,將計算結果放在一個static變量中,而後返回一個指向這個變量的指針。若是咱們從併發線程中調用這些函數,那麼將可能發生災難,由於正在被一個線程使用的結果會被另外一個線程悄悄地覆蓋了。

有兩種方法來處理這類線程不安全函數。一種選擇是重寫函數,使得調用者傳遞存放結果的變量的地址。這就消除了全部共享數據,可是它要求程序員可以修改函數的源代碼。

若是線程不安全是難以修改或不可能修改的,那麼另一種選擇是使用加鎖-拷貝技術。基本思想是將線程不安全函數與互斥鎖聯繫起來,在每個調用位置,對互斥鎖加鎖,調用線程不安全函數,將函數返回的結果拷貝到一個私有的存儲器位置,而後對互斥鎖解鎖。爲了儘量減小對調用者的修改,你應該定義一個線程安全的包裝函數,它執行加鎖-拷貝,而後經過調用這個包裝函數來取代對線程不安全函數的調用。

(4):調用線程不安全函數的函數。若是函數f調用線程不安全函數g,那麼f就是線程不安全的嗎?不必定。若是g是第二類資源,即依賴於跨越屢次調用的狀態,那麼f也是線程不安全的,並且除了重寫g覺得,沒有辦法。然而,若是g是第一類或第三類函數,那麼只要你用一個互斥鎖保護調用位置和任何獲得的共享數據,f仍然多是線程安全的。

2.可重入函數。可重入函數是線程安全函數的一個真子集,它不訪問任何共享數據。可重入安全函數一般比不可重入函數更有效,由於它們不須要任何同步原語。

3.競爭。當程序員錯誤地假設邏輯流該如何調度時,就會發生競爭。爲了消除競爭,一般咱們會動態地分配內存空間。

4.死鎖。當一個流等待一個永遠不會發生的事件時,就會發生死鎖。

返回目錄

相關文章
相關標籤/搜索