線程小記

線程概念

典型的UNIX進程可以看作只有一個控制線程,任務的執(zhí)行只能串行來做。有了多個控制線程后,就可以同時做多個事情。
每個線程都包含執(zhí)行環(huán)境所需要的信息:

  • 線程id
  • 寄存器值
  • 調(diào)度器優(yōu)先級和策略
  • 信號屏蔽字
  • errno變量
  • 線程私有變量

一個進程的可執(zhí)行程序代碼、程序的全局內(nèi)存和堆內(nèi)存、棧以及文件描述符對所有的線程共享。
本文討論的線程接口來自POSIX.1-2001。接口也稱為"pthread"或者"posix線程"。

線程標識

類似進程ID,每個線程有一個在進程上下文中唯一的ID值。
進程ID使用pid_t數(shù)據(jù)類型來表示,是一個非負整數(shù)。線程ID是用pthread_t數(shù)據(jù)類型來表示。
比較兩個線程id,相等,返回非0數(shù)值,否則,返回0

#include <pthread.h>
int pthread_equal(pthread_t tid1, pthread tid2);

線程也可以通過pthread_self函數(shù)獲取自身的線程id

pthread_t pthread_self(void);

線程創(chuàng)建

在POSIX線程的情況下,程序開始運行時,它是以單個控制線程運行的。新增的線程可以通過調(diào)用pthread_create函數(shù)創(chuàng)建。

#include <pthread.h>
int pthread_create(pthread_t *restrict tidp, 
                   const pthread_attr_t *restrict attr,
                    void *(*start_rtn) (void *), void *restrict arg);

新創(chuàng)建的線程ID會被設置為tidp指向的內(nèi)存單元。attr參數(shù)用于定制各種不同的線程屬性。
新創(chuàng)建的線程從start_rtn函數(shù)的地址開始運行,該函數(shù)只有一個無類型指針參數(shù)arg。
線程創(chuàng)建時不能保證哪個線程先運行:是新創(chuàng)建的線程,還是調(diào)用線程。

實例

如下程序,創(chuàng)建了一個線程,打印了進程ID、新線程ID以及初始線程的ID。

#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <pthread.h>
#include <sys/types.h>
pthread_t tid1;

void printtids(const char *s) {
  pid_t pid;
  pthread_t tid;

  pid = getpid();
  tid = pthread_self();
  printf("%s pid %lu tid %lu (0x%1x)\n", s, (unsigned long)pid, (unsigned long)tid, (unsigned long)tid);
}

void *thr_fn(void *arg) {
  printtids("new thread: ");
  return ((void *)0);
}

int main() {
  int err = 0;
  err = pthread_create(&tid1, NULL, thr_fn, NULL);
  if (0 != err) {
    //err_exit(err, "can't create thread");
  }
  printtids("main thread:");
  sleep(1);
  exit(0);
}

這個程序有兩個地方需要注意:

  • 一個是主線程需要休眠,不然新線程可能來不及執(zhí)行進程就結束了。這種行為依賴于操作系統(tǒng)的線程實現(xiàn)和調(diào)度算法。
  • 第二個地方是新線程通過pthread_self獲取自己的線程ID,而不是直接使用tid1從共享內(nèi)存中讀出。這是因為如果新線程在主線程調(diào)用pthread_create返回之前就運行了,那么新線程看到的是未經(jīng)初始化的tid1的內(nèi)容。

運行結果:

main thread: pid 91334 tid 4708181440 (0x18a125c0)
new thread:  pid 91334 tid 123145310978048 (0x844000)

如我們期望的,進程ID相同,線程ID不同。

線程終止

進程中的任意線程調(diào)用了exit、_Exit或者_exit,整個進程就會終止。
單個線程可以通過三種方式退出:

  1. 線程可以從啟動實例中返回,返回值是線程的退出碼。
  2. 線程可以被同一進程中的其它線程取消。
  3. 線程調(diào)用pthread_exit。
#include <pthread.h>
void pthread_exit(void *rval_ptr)

rval_ptr是一個無類型指針,與傳遞給啟動實例的單個參數(shù)類似。進程中的其它線程可以通過pthread_join函數(shù)訪問到這個指針。

調(diào)用pthread_join的線程會一直阻塞直到線程以上述三種方式退出。如果線程以第一種方式退出,rval_ptr中就包含退出碼。以第二種方式rval_ptr指向的內(nèi)存單元中就設置未PTHREAD_CANCELED。

如果對線程的返回值不感興趣,可以把rval_ptr設置未NULL。調(diào)用線程會等待指定的線程終止,但不獲取線程的終止狀態(tài)。

pthread_create和pthread_exit函數(shù)的無類型指針參數(shù)可以包含復雜的結構的地址,但是這個結構所使用的內(nèi)存要使用malloc或者為全局變量,必須確保在調(diào)用者完成調(diào)用后內(nèi)存仍然是有效的。比如線程在自己的棧上分配了一個結構,然后把指向這個結構的指針傳遞給pthread_exit,那么pthread_join試圖使用該結構時就可能出錯。

線程可以通過pthread_cancel函數(shù)來請求取消同一進程中的其它線程。

#include <pthread.h>
void pthread_cancel(pthread_t tid)

默認情況下,pthread_cancel函數(shù)會使得tid標識的線程如同調(diào)用了參數(shù)為PTHRED_CANCEL的pthread_exit函數(shù)。但是線程可以選擇忽略取消或者控制如何被取消。pthread_cancel并不等待線程終止,僅僅提出請求。

線程可以安排它退出時的清理函數(shù),這樣的函數(shù)稱為線程清理程序,可以有多個,被記錄在棧中,也就是說它們的執(zhí)行順序與注冊順序相反。

#include <pthread.h>
void pthread_cleanup_push(void (*rtn)(void *), void *arg);
void pthread_cleanup_pop(int execute)

當線程執(zhí)行以下動作時,清理函數(shù)rtn由pthread_cleanup_push函數(shù)調(diào)度的,調(diào)用時只有一個參數(shù)arg:

  1. 調(diào)用pthread_exit
  2. 相應取消請求
  3. 用非零execute參數(shù)調(diào)用pthread_cleanup_pop,為0則不調(diào)用清理函數(shù),依舊會pop

線程如果是從啟動實例中退出的(return ((void *)x)),將不會調(diào)用清理函數(shù)。

進程和線程函數(shù)之間的相似之處總結:

  • fork/pthread_create 創(chuàng)建新的控制流
  • exit/pthread_exit 從現(xiàn)有控制流中退出
  • waitpid/pthread_join 從控制流中得到退出狀態(tài)
  • atexit/pthread_cancel_push 注冊在退出控制流時調(diào)用的函數(shù)
  • getpid/pthread_self 獲取控制流的id
  • abort/pthread_cancel 請求控制流的非正常退出

默認情況下,線程的終止狀態(tài)會保存直到對線程調(diào)用pthread_join函數(shù)。如果線程被分離,線程的底層資源可以在線程終止時立即收回。這時不可以使用pthread_join等待它的終止狀態(tài),會產(chǎn)生未定義行為,可以調(diào)用pthread_detach分離線程。

#include <pthread.h>
void pthread_detach(pthread_t tid)

線程同步

當多個線程控制共享內(nèi)存時,要確保每個線程看到一致的視圖。如果每個線程修改的變量是其它線程不會讀取和修改的,比如棧上的變量,就不會出現(xiàn)不一致的問題。如果線程修改一個其它線程也可以讀取和修改的變量時,我們就需要對這些線程進行同步,確保它們在訪問變量時不會讀到無效的值。

在變量修改時間超過一個存儲器訪問周期的處理結構中,當存儲器讀與存儲器寫這兩個周期交叉時,不一致就會出現(xiàn)。為了解決這個問題,線程不得不實用鎖,同一時間只允許一個線程訪問該變量。

兩個或多個線程修改變量時也需要進行同步。增量操作通常分為以下步驟:

  1. 從內(nèi)存單元讀入寄存器
  2. 在寄存器中做增量操作
  3. 把新的值寫回內(nèi)存單元

如果修改操作是原子操作,就不存在競爭。在現(xiàn)代計算機系統(tǒng)中,存儲訪問需要多個總線周期,多處理器的總線周期通常在多個處理器上是交叉的,所以并不能保證數(shù)據(jù)是順序一致的。

互斥量

互斥量(mutex)從本質上說是一把鎖,在訪問共享資源前對互斥量加鎖,在訪問完成后釋放?;コ饬考渔i后,其它線程試圖再次對其加鎖都會被阻塞直到鎖被釋放。

互斥變量是用pthread_mutex_t數(shù)據(jù)類型表示的,在使用之前必須要進行初始化,可以設為常量PTHREAD_MUTEX_INITIALIZER(只適用于靜態(tài)分配的互斥量)。也可以通過pthread_mutex_init函數(shù)來進行初始化,如果動態(tài)分配互斥量,在釋放內(nèi)存前需要調(diào)用pthread_mutex_destroy。

#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *restrict mutex, 
                       const pthread_mutexattr_t *restrict attr)
int pthread_mutex_destroy(pthread_mutex_t *mutex)

attr設為NULL時使用默認的屬性初始化。

互斥量的加鎖,解鎖。

#include <pthread.h>
int pthread_mutex_lock(pthread_mutex *mutex)
int pthread_mutex_trylock(pthread_mutex *mutex)
int pthread_mutex_unlock(pthread_mutex *mutex)

使用pthread_mutex_lock對互斥量加鎖,如果互斥量已經(jīng)上鎖,則調(diào)用線程會一直阻塞直到互斥量被解鎖。如果不希望調(diào)用線程阻塞,可以使用pthread_mutex_trylock嘗試對互斥量加鎖,互斥量已經(jīng)上鎖時會失敗,返回EBUSY。

避免死鎖

當線程試圖對一個互斥量加鎖兩次,就會出現(xiàn)死鎖。使用一個以上的互斥量時,如果線程A占有互斥量1,試圖鎖住互斥量2,而線程B占有互斥量2,試圖占有互斥量1,兩個線程都在相互請求另一個線程擁有的資源,于是產(chǎn)生死鎖。
應用程序需要仔細控制加鎖順序來避免死鎖的發(fā)生,死鎖只會發(fā)生在一個線程試圖鎖住另一個線程以相反的順序鎖住的互斥量。
鎖的粒度較粗,比較容易編寫無死鎖的程序,但是會出現(xiàn)很多線程阻塞等待鎖,性能較差,而鎖的粒度較細,會極大增加編程的難度和出現(xiàn)死鎖等問題的風險,實際編程中需折中考慮。

當試圖獲取一個已加鎖的互斥量時,pthread_mutex_timedlock允許綁定線程等待時間。pthread_mutex_timedlock與pthread_mutex_lock基本是等價的。
在達到超時時間時,pthread_mutex_timedlock返回錯誤碼ETIMEDOUT。

#include <pthread.h>
#include <time.h>
int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex,
                            const struct timespec *restrict tsptr);

超時時間指絕對時間,而非相對時間。

讀寫鎖

讀寫鎖與互斥量類似,互斥量只有加鎖中與未加鎖兩種狀態(tài),一旦被線程加鎖占有,其它線程只能等待。而讀寫鎖有更多狀態(tài):未加鎖、讀模式加鎖和寫模式加鎖。一次只有一個線程可以進行寫模式加鎖,而多個線程可以同時占有讀模式的讀寫鎖。

當讀寫鎖以寫模式加鎖時,所有試圖對對線程加鎖的線程都會被阻塞。當讀寫鎖以讀模式加鎖時,所有以讀模式對它加鎖的線程都可以得到訪問權,所有以寫模式嘗試對它加鎖的線程將會被阻塞。當讀寫鎖以讀模式加鎖中時,這時有一個線程試圖以寫模式加鎖,讀寫鎖通常會阻塞后面的讀模式鎖請求,這樣可以避免讀模式鎖長期占有,而寫模式鎖請求一直在等待。

讀寫鎖非常適合讀請求遠大于寫請求次數(shù)的場景。讀寫鎖也稱作共享互斥鎖(sharded-exclusive lock),當以讀模式鎖住,可以稱為以共享模式鎖住的,反之可以認為是以互斥模式鎖住的。

與互斥量相比,讀寫鎖在使用前必須初始化,在釋放它們底層內(nèi)存之前必須銷毀。

#include <pthread.h>
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,
                        const pthread_rwlockattr_t *restrict attr);
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock)
#include <pthread.h>
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock); // 以讀模式加鎖
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock); // 以寫模式加鎖
nt pthread_rwlock_unlock(pthread_rwlock_t *rwlock); // 解鎖

// 讀寫鎖加鎖的條件版本
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock); // 以讀模式加鎖
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock); // 以寫模式加鎖

各種實現(xiàn)可能對共享模式下獲取讀寫鎖的次數(shù)有限制,所以要檢查函數(shù)的返回值。

帶有超時的讀寫鎖

#include <pthread.h>
#include <time.h>
int pthread_rwlock_timerdlock(pthread_rwlock_t *restrict rwlock,
                              const struct timespec *restrict tsptr); 
int pthread_rwlock_timewdlock(pthread_rwlock_t *restrict rwlock,
                              const struct timespec *restrict tsptr); 

條件變量

條件變量給多線程提供了一個會合的場所。條件變量與互斥量一起使用時,允許線程以無競爭的方式等待特定條件的發(fā)生。
使用條件變量之前必須對它進行初始化,可以把常量PTHREAD_COND_INITIALIZER賦給靜態(tài)分配的條件變量,如果條件變量是動態(tài)分配的,則需要使用pthread_cond_init進行初始化,使用pthread_cond_destroy釋放資源。

#include <pthread.h>
int pthread_cond_init(pthread_cond_t *restrict cond,
                      const pthread_condattr_t *restrict attr);
int pthread_cond_destroy(pthread_cond_t *cond);

使用pthread_cond_wait等待條件變?yōu)檎妗?/p>

#include <pthread.h>
int pthread_cond_wait(pthread_cond_t *restrict cond,
                      pthread_mutex_t *restrict mutex);
// 帶有時間條件的版本
int pthread_cond_timewait(pthread_cond_t *restrict cond,
                          pthread_mutex_t *restrict mutex,
                          const struct timespec *restrict tsptr); 

傳遞給pthread_cond_wait中的互斥量對條件進行保護。調(diào)用者把互斥量傳給函數(shù),函數(shù)把調(diào)用線程放到等待條件的線程列表上,對互斥量解鎖,這兩步必須是原子的。pthread_cond_wait返回時,互斥量再次被鎖住。

兩個函數(shù)用于通知線程條件已經(jīng)滿足。

//喚醒至少一個等待該條件的線程。
int pthread_cond_signal(pthread_cond_t *cond);
// 喚醒所有等待該條件的線程
int pthread_cond_broadcast(pthread_cond_t *cond);

使用條件變量實現(xiàn)簡單生產(chǎn)者-消費者模型示例:

#include <pthread.h>

struct msg {
    struct msg *m_next;
    /*more stuff here*/
}

struct msg *workq;
pthread_cond_t qready = PTHREAD_COND_INITIALIZER;
pthread_mutex_t qlock = PTHREAD_MUTEX_INITALIZER;

void process_msg(void)
{
    struct msg *mp;
    for (;;) {
        pthread_mutex_lock(&qlock);
        while (NULL == workq) {
            pthread_cond_wait(&qready, &qlock);
        }
        mp = workq;
        workq = mp->next; // 消費掉workq
        pthread_mutex_unlock(&qlock);
        /*now process the message mp*/
    }
}

void enqueue_msg(struct msg *mp)
{
    pthread_mutex_lock(&qlock);
    mp->next = workq;
    workq = mp;
    pthread_mutex_unlock(&qlock); // 與cond signal的順序問題
    pthread_cond_signal(&qready);
}

條件是工作隊列的狀態(tài)。使用互斥量保護條件,在while條件中,把消息放到工作隊列中需要占有互斥量,等待條件變量時不需要互斥量,上述提到這兩步必須是原子的,假如先放到了工作隊列中,這時生成者線程發(fā)送信號喚醒該線程,wait函數(shù)返回需要加鎖,就會加鎖兩次造成死鎖;如果順序反了,先釋放了鎖,然后發(fā)送信號時線程未在條件列表中,造成一直wait的問題。
這里涉及到pthread_mutex_unlock與pthread_cond_signal的執(zhí)行順序問題,是先放鎖呢還是先發(fā)送信號?

  1. 先unlock
    unlock之后有線程作了條件判斷并消費了這個workq,之后signal的發(fā)送是無效的。而且互斥量有可能被其它低優(yōu)先級的線程獲得。
  2. 先singal再unlock
    線程被喚醒后獲取不到互斥量再次進入sleep,浪費cpu資源。在linux實現(xiàn)中,有兩個隊列:cond_wait和lock_wait。cond signal只是將線程從cond wait中移到lock wait隊列中,不用返回用戶空間,所以不會有性能損耗。所以這種順序一般是linux下采用的方式。

自旋鎖

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容