POSIX Thread

概述

在傳統的unix模型中,當一個進程須要另外一個實體來完成某項任務時,它就fork一個子進程出來處理,好比在一個網絡服務器程序中,父進程accept一個鏈接,而後fork一個子進程,由該子進程處理與鏈接對端的客戶端之間的通訊。html

儘管這種範式好久以來一直用得很好,可是fork調用卻存在一些問題:java

  • fork是昂貴的。fork要把父進程的內存映像複製到子進程中,並在子進程中複製全部描述符。當前的實現使用寫時複製(COW)的技術,來避免子進程切實須要本身的副本以前把父進程的數據複製到子進程。然而即便有這樣的優化措施,fork仍然是昂貴的。
  • fork返回以後,複製進程須要經過進程間通訊(IPC)來傳遞信息。從父進程傳遞信息到子進程至關容易,由於子進程將從父進程的數據空間和描述符的副本開始運行,然而從子進程往父進程傳遞消息卻比較費力。

線程有助於解決這兩個問題。線程有時稱爲輕量進程(lightweight process),由於線程比進程更輕量。也就是說,線程的建立可能比進程的建立快10~100倍。同一進程內的全部線程共享相同的全局內存。這使得線程之間易於共享信息,然而伴隨這種簡易性而來的倒是同步(synchronization)問題。linux

同一進程內的全部線程除了共享全局變量外還共享:編程

  • 進程指令
  • 大多數數據
  • 打開的文件(即描述符)
  • 信號處理函數和信號處置
  • 當前工做目錄
  • 用戶ID和組ID

不過每一個線程有各自的:服務器

  • 線程ID
  • 寄存器集合,包括程序計數器和棧指針
  • errno
  • 信號掩碼
  • 優先級

本文中講述的是POSIX線程,也成爲Pthread。POSIX線程做爲POSIX.1c標準的一部分在1995年獲得標準化,大多數unix版本支持這類線程。咱們將看到全部的Pthread函數都以ptread_開頭。網絡

基本線程函數

pthread_create

當一個程序由exec啓動執行時,稱爲初始線程(initial thread)或者主線程(main thread)的單個線程就被建立了,其他線程則由ptread_create函數建立。併發

pthread_t pthread_create(pthread_t *tid,
    const pthread_attr_t *attr, void *(*func)(void *), void * arg);
複製代碼

一個進程內的每一個線程都由一個線程ID(thread ID)標識,其數據類型爲pthread_t(每每都是 unsigned int)。若是新的線程建立成功,其ID就經過tid指針返回。oracle

每一個線程都有許多屬性(attribute):優先級、初始棧大小、是否爲守護線程等等。咱們能夠在建立線程時經過初始化一個取代默認設置的pthread_attr_t變量指定這些屬性。一般狀況咱們採用默認的設置,這時咱們把attr參數指定爲空指針。函數

建立一個線程時咱們最後指定的參數是由該線程執行的函數及其參數。該線程經過調用該函數開始執行,而後顯式(調用pthread_exit)或者隱式(函數返回)地終止。該函數的地址由func參數指定,該函數的惟一調用參數是指針arg。若是咱們須要給該函數傳遞多個參數,咱們就得把它們打包成一個結構,而後把這個結構的地址做爲單個參數傳遞給func測試

注意funcarg的聲明,func所指函數做爲參數接受一個通用指針(void *),又做爲返回值返回一個通用指針(void *)。這時的咱們能夠把一個指針(它指向咱們指望的任何內容)傳遞給一個線程,又容許線程返回一個指針(它一樣指向咱們所指望的任何內容)。

一般狀況下Ptread函數的返回值成功時爲0,出錯時爲某個非0值。與套接字及大多數系統調用出錯時返回-1並置errno爲某個正值的作法不一樣的是,Pthread函數出錯時做爲函數返回值返回正值錯誤指示。舉個例子,若是pthread_create因在線程數目上超過某個系統限制而不能建立新線程,函數返回值將是EAGAIN。Pthread函數不設置errno

pthread_join

咱們能夠經過調用pthread_join等待一個給定線程終止。對比線程和unix進程,pthread_create相似於forkpthread_join相似於waitpid

pthread_t pthread_join(pthread_t *tid, void ** status);
複製代碼

咱們必須制定要等待的線程的tid。不幸的是,Pthread沒有辦法等待任意一個線程(相似在waitpid中指定參數爲-1)。若是status指針非空,來自所等待的線程的返回值(一個指向某個對象的指針)將存入status所指向的位置。

pthread_self

每一個線程都有一個在進程內標識自身的ID。線程ID由pthread_create返回,而咱們能夠在pthread_join中使用它。每一個線程能夠經過ptread_self獲取自身的線程ID。

pthread_t pthread_self(void);
複製代碼

對比unix線程,pthread_self相似於getpid

pthread_detach

一個線程或者是可匯合的(joinable,默認值),或者是脫離的(detached)。當一個可匯合的線程終止時,它的線程ID和退出狀態將留存到另外一個線程對它調用pthread_join。脫離的線程卻像守護進程,當它們終止時,全部相關資源都將被釋放,咱們不能等待它們終止。

pthread_detach函數把指定的線程變爲脫離狀態。

pthread_t pthread_detach(pthread_t tid);
複製代碼

pthread_exit

讓一個線程終止的方法之一是調用pthread_exit

void pthread_exit(void *status);
複製代碼

若是該線程不曾脫離,它的線程ID和退出狀態將一直留存到調用進程內某個其餘線程對它調用pthread_join

指針status不能指向局部於調用線程的對象,由於線程終止時,這樣的對象也會消失。

讓一個線程終止的另外兩個方法是:

  1. 啓動線程的函數(即pthread_create的第三個參數)能夠返回。該函數的返回值就是相應線程的終止狀態。
  2. 若是進程的main函數返回或者任何線程調用了exit,整個進程就終止。其中包括它的任何線程。

互斥鎖

線程編程稱爲併發編程(concurrent programming)或者並行編程(parallel programming),由於多個線程能夠併發(或者並行)地運行且訪問相同的變量。在併發編程中更改同一個變量時可能會產生同步問題,其解決辦法是使用一個互斥鎖(mutex,表示mutual exclusion)保護共享變量;訪問該變量的前提條件是持有該互斥鎖。按照Pthread,互斥鎖是類型爲pthread_mutex_t的變量。咱們使用如下兩個函數爲一個互斥鎖上鎖和解鎖。

int pthread_mutex_lock(pthread_mutex_t *mptr);
int pthread_mutex_unlock(pthread_mutex_t *mptr);
複製代碼

若是試圖上鎖一個已被另外某個線程鎖住的互斥鎖,本線程將會被阻塞,直到該互斥鎖被解鎖爲止。

若是某個互斥鎖變量是靜態分配的,咱們就必須把它初始化爲常值PTHREAD_MUTEX_INITIALIZER。若是咱們在共享內存區中分配一個互斥鎖,那麼必須經過調用pthread_mutext_init函數在運行時將其初始化。

如下是一個利用互斥鎖操做計數器的例子:

#include <stdio.h>
#include <pthread.h>
#define NLOOP 5000

int counter; /* 由線程進行遞增操做 */
pthread_mutex_t counter_mutex = PTHREAD_MUTEX_INITIALIZER;

void *doit(void *);

int main(int argc, char **argv) {
    pthread_t tidA, tidB;
    pthread_create(&tidA, NULL, &doit, NULL);
    pthread_create(&tidB, NULL, &doit, NULL);
    /* 等待線程退出 */
    pthread_join(tidA, NULL);
    pthread_join(tidB, NULL);
    exit(0);
}

void *doit(void *vptr) {
    int i, val;
    /* 先打印,再遞增 */
    for (i = 0; i < NLOOP; i++) {
        pthread_mutex_lock(&counter_mutex);
        val = counter;
        printf("%d: %d\n", pthread_self(), val + 1);
        counter = val + 1;
        pthread_mutex_unlock(&counter_mutex);
    }
    return(NULL);
}
複製代碼

使用互斥鎖上鎖會帶來額外的開銷,但並不會太大。

條件變量

互斥鎖適合於防止同時訪問某個共享變量,但咱們須要另外某種在等待期間讓咱們進入睡眠的方式。條件變量(condition variable)結合互斥鎖可以提供這樣的功能。互斥鎖提供互斥機制,條件變量提供信號機制。

按照Pthread,條件變量是類型爲pthread_cond_t的變量。如下兩個函數用來使用條件變量:

int pthread_cond_wait(pthread_cond_t *cptr, pthread_mutex_t *mptr);
int pthread_cond_signal(pthread_cond_t *cptr);
複製代碼

如下是一個使用條件變量的例子:

#include <stdio.h>
#include <pthread.h>
#define NLOOP 5000

int counter; /* 由線程進行遞增操做 */
pthread_mutex_t counter_mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t counter_cond = PTHREAD_COND_INITIALIZER;
void *doit(void *);
int main(int argc, char **argv) {
    pthread_t tidA, tidB;
    pthread_create(&tidA, NULL, &doit, NULL);
    pthread_create(&tidB, NULL, &doit, NULL);
    /* 主線程循環等待操做完成 */
    pthread_mutex_lock(&counter_mutex);
    while (counter < NLOOP)
    {
        pthread_cond_wait(&counter_cond, &counter_mutex);
    }
    printf("main: %d\n", counter);
    pthread_mutex_unlock(&counter_mutex);
    exit(0);
}
void *doit(void *vptr) {
    int i, val;
    /* 先打印,再遞增 */
    for (i = 0; i < NLOOP; i++) {
        pthread_mutex_lock(&counter_mutex);
        val = counter;
        if (val < NLOOP)
        {
            counter = val + 1;
        }
        pthread_cond_signal(&counter_cond);
        pthread_mutex_unlock(&counter_mutex);
    }
    return(NULL);
}
複製代碼

主循環阻塞在pthread_cond_wait調用中,等待某個即將終止的線程發送發送信號到與counter關聯的條件變量。主循環只在持有互斥鎖期間才檢查counter變量,若是發現無事可作,那麼就調用pthread_cond_wait。該函數把調用線程投入睡眠並釋放調用線程持有的互斥鎖。此外,當pthread_cond_wait返回時(其餘某個線程發送信號到與counter關聯的條件變量以後),該線程再次持有該互斥鎖。

爲何每一個條件變量要關聯一個互斥鎖呢?由於「條件」一般是線程之間共享的某個變量的值。容許不一樣線程設置和測試該變量要求有一個與該變量關聯的互斥鎖。舉例來講,若是上面的例子中沒有使用互斥鎖,那麼主循環就是這樣:

while (counter < NLOOP)
{
    pthread_cond_wait(&counter_cond, &counter_mutex);
}
複製代碼

這裏存在這樣的可能:主線程外最後一個線程在主循環測試counter < NLOOP以後但在調用pthread_cond_wait以前遞增了counter。若是發生這樣的狀況,最後那個「信號」就丟失了,形成主循環永遠阻塞在pthread_cond_wait調用中,等待永遠再也不發生的某事再次出現。

一樣,要求pthread_cond_wait被調用時其所關聯的互斥鎖必須是上鎖的,該函數做爲單個原子操做解鎖該互斥鎖並把調用線程投入睡眠也是出於這個理由。要是該函數不先解鎖該互斥鎖,到返回時再給它上鎖,調用線程就不得不實現解鎖過後上鎖該互斥鎖,測試變量counter的代碼將變爲:

pthread_mutex_lock(&counter_mutex);
while (counter < NLOOP)
{
    pthread_mutex_unlock(&counter_mutex);
    pthread_cond_wait(&counter_cond, &counter_mutex);
    pthread_mutex_lock(&counter_mutex);
}
複製代碼

然而這裏也可能存在:主線程外最後一個線程在主線程調用pthread_mutex_unlockpthread_cond_wait之間終止並遞增了counter的值。

pthread_cond_signal一般喚醒等在相應條件變量上的單個線程。有時候一個線程知道本身應該喚醒多個線程,這時它能夠調用pthread_cond_broadcast喚醒等在相應條件變量上的全部線程。

int pthread_cond_broadcast(pthread_cond_t *cptr);
int pthread_cond_timedwait(pthread_cond_t *cptr, pthread_mutex_t *mptr, const struct timespec *abstime);
複製代碼

pthread_cond_timedwait容許線程設置一個阻塞時間的限制。abstime是一個timespec結構,指定該函數必須返回時刻的系統時間,即到時候相應條件變量還沒有收到信號的話,就會返回ETIME錯誤。

這裏的abstime是一個絕對時間(absolute time),而不是一個時間增量(time delta)。這一點不一樣於selectpselect。使用絕對時間的優勢在於,若是該函數過早返回(多是由於捕獲了某個信號),那麼沒必要改動timespec結構就能夠再次調用該函數;缺點是首次調用該函數以前不得不調用gettimeofday

總結

建立一個線程一般比調用fork派生一個進程快得多。僅僅這一點就可以體現線程在繁重使用的網絡服務器上的優點。

同一進程內的全部線程共享全局變量和描述符,從而容許不一樣線程之間共享信息。然而這種共享卻引入了同步問題,咱們必須使用Pthread同步原語「互斥鎖」和「條件變量」來解決。共享數據的同步幾乎是每一個線程化程序必不可少的部分。

條件變量必須和互斥鎖配合使用,這是規範的一部分。這麼規定的緣由在於若是不配合互斥鎖,條件變量會面臨可能的信號丟失的問題。這個信號丟失的問題有個專門的名字,叫作lost wake-up problem

關聯到java

在java 1.2以後的版本,在java中建立的Thread,在linux平臺下實際上就是Pthread。能夠看出java中Thread的各個屬性與Pthread比較相似(可是沒有detached屬性)。在同步方面,java有本身的同步機制(synchronized關鍵字),並無直接使用Pthread中的同步原語。java 1.5以後引入的java.util.concurrent.locks中的庫函數,則與Pthread同步原語有更多的類似的地方。

另外,java中Objectwait/notify/notifyAllConditionawait/signal必需要在同步塊中,其道理跟條件變量同樣,都是爲了不信號丟失的問題。

相關文章
相關標籤/搜索