天天看點

UNIX上C++程式設計守則(信号和線程)(下)

線程的異步撤銷是指:某個線程的執行立刻被其他線程給強制終止了

請不要單單為了讓“設計更簡單”或者“看起了更簡單”而使用線程的異步撤消

咋一看還是挺簡單的。但是搞不好可能會引起各種各樣的問題。請不要在不能把握問題的實質就做出使用線程的異步撤消的設計!

在pthread的規格說明中,允許一個線程可以強制中斷某個線程的執行。這就是所說的異步撤消。

線程的撤消有下面的兩種方式。

方式1: 異步撤消(PTHREAD_CANCEL_ASYNCHRONOUS)

撤銷動作是馬上進行的

方式2: 延遲撤銷(PTHREAD_CANCEL_DEFERRED) (預設設定)

撤消動作,是讓線程的處理一直被延遲到撤消點才會去執行

會造成什麼問題呢

      那麼,讓我看看亂用線程的異步撤消會引起什麼問題呢。看過準則3的人可能會知道,在下面的腳本裡,被撤銷線程以外的任意一個線程會被死鎖。

1.   線程1中調用malloc函數正在做記憶體配置設定的過程中,線程2異步撤消了線程1的處理

2.   線程1馬上被撤銷,但是malloc函數中的互斥鎖就沒有線程去解除了

3.   後面的任意一個線程如果再次調用malloc函數的話就會馬上導緻該線程死鎖

      在這個例子中使用了malloc函數,但是其他的危險函數還有很多。

pthread_cancel

pthread_setcancelstate

pthread_setcanceltype

      而且,裡面還有"No other functionsare required to be async-cancel-safe"這樣的記載。是以,Linux的場合,如果在文檔裡沒有記載成async-cancel safety的函數,我們還是把它假定成不安全的函數為好!

如何避免這些問題呢

     在多線程程式設計中為了安全的使用異步撤消處理、有沒有回避死鎖的方法呢?我們試着想了幾個。他們與準則3裡的線程+fork的場合的回避策很相似。

回避方法1: 被撤銷線程中,隻能使用異步撤消安全函數

首先,被撤銷線程中,隻能使用異步撤消安全函數。但是這個方法

在規格說明中隻有3個異步撤消安全的函數

這些以外的函數是不是異步撤消安全(商用UNIX)、因為沒有說明文檔我們不清楚(Linux)

中有以上的兩點,是以這個回避方法幾乎不現實。

回避方法2: 被撤銷線程中,在做非異步撤消安全處理的過程中,先把撤消方式設定成「延遲」或者是「禁止」

第二個是,被撤銷線程在做非異步撤消安全處理的過程中,把撤消方式再設定成「延遲」或者「禁止」。對于這個方法

就像方法1寫的那樣、要把我那個函數是異步撤消安全的一時還是挺麻煩的

在任意的場所并不能保證撤消動作會被馬上執行

例如,再設定成「延遲」後的一段時間内如果撤消發生時、某個正在阻塞的I/O函數是否能夠被解除阻塞還是挺微妙的

如果設定成撤消禁止的話,則撤消會被屏蔽掉

有上面樣的問題、會導緻「一精心設計撤消方式的替換,從一開始就使用延遲撤消還不夠好」這樣的結果。是以這幾乎是不好的一個回避策。

回避方法3: 使用pthread_cleanup_push函數,登入異步撤消時的線程資料清除的回調函數

第三種則是,用pthread_cleanup_push函數、登入一個在異步撤消發生時的資料清除的回調函數。這和在準則3中介紹的pthread_atfork函數有點兒類似。用這個函數登入的回調函數來清除線程的資料和鎖,就可以回避死鎖了。

回避方法4: 不要執行異步撤消處理

最後是、不要執行異步撤消處理。反而代之的是、

設計成不依賴使用異步撤消那樣的處理

不得不使用線程撤消的話,不做異步撤消而作延遲撤消的處理

這是比較實際的做法,是我們值得推薦的。

線程的異步撤消是指:一個線程發出中斷其他線程的處理的一個動作

延遲撤消因為是規格自由度比較高,是以根據OS和C庫函數的版本它也有各式各樣的動作

要想在不同的環境下都能穩定的動作的話,就必須要詳細調查運作環境和對C庫函數進行抽象化,做必要的條件編譯

在C++中,「撤消發生時的對象釋放」的實作不具有可移植性

線程撤銷要慎重使用。在C++裡不要使用

說明:

在前面我們已經講過,線程的撤消分為「異步」「延遲」這兩種類型,并且「異步撤消」也是非常容易引起各種複雜問題的元兇。

那麼,現在要在程式中除掉「延遲撤消」。延遲撤消雖然不會像異步撤消那樣會引起各種各樣的問題、但是,注意事項還是有很多的。隻有把下面的這些注意事項全部都把握之後才能放心使用。

注意事項1: 要好好把握撤消點

      和異步撤消不一樣的是:撤消處理一直會被延遲到在代碼上明示出來的撤消點之後才會被執行。如果編寫了一個具有延遲撤消可能的代碼,代碼中的那條語句是撤消點,必須要正确的把握。

下面的函數是撤消點

accept, aio_suspend, clock_nanosleep, close, connect,creat, fcntl, fdatasync,fsync, getmsg, getpmsg, lockf, mq_receive, mq_send,mq_timedreceive,mq_timedsend, msgrcv, msgsnd, msync, nanosleep, open, pause,poll, pread,pselect, pthread_cond_timedwait,pthread_cond_wait, pthread_join,pthread_testcancel, putmsg, putpmsg,pwrite, read, readv, recv, recvfrom,(略)

下面的函數不是撤消點

access, asctime, asctime_r, catclose, catgets, catopen,closedir, closelog,ctermid, ctime, ctime_r, dbm_close, dbm_delete, dbm_fetch,dbm_nextkey, dbm_open,dbm_store, dlclose, dlopen, endgrent, endhostent,endnetent, endprotoent,endpwent, endservent, endutxent, fclose, fcntl, fflush,fgetc, fgetpos, fgets,fgetwc, fgetws, fmtmsg, fopen, fpathconf, fprintf, fputc,fputs, fputwc, fputws,(略)

即使是這樣那還想要使用延遲撤消嗎?

注意事項2: 實作要知道cleanup函數的必要性

      可能被延遲撤銷的線程在運作的過程中,要申請資源的場合,一定要考慮到以下的幾點,否則就會編制出含有資源丢失和死鎖的軟體産品。

例如編寫的下面的函數就不能被安全的延遲撤銷掉。

void* cancel_unsafe(void*) {

    static pthread_mutex_t mutex =PTHREAD_MUTEX_INITIALIZER;

    pthread_mutex_lock(&mutex);                        // 此處不是撤消點

    struct timespec ts = {3, 0};nanosleep(&ts, 0); // 經常是撤消點

    pthread_mutex_unlock(&mutex);                    // 此處不是撤消點

    return 0;

}

int main(void) {

    pthread_t t;

    // pthread_create後馬發上收到一個有效的延遲撤消的要求

    pthread_create(&t, 0,cancel_unsafe, 0);

    pthread_cancel(t);

    pthread_join(t, 0);

    cancel_unsafe(0); // 發生死鎖!

      為了回避這個問題,利用pthread_cleanup_push函數在撤消時釋放掉互斥鎖的話就OK了,也就不會死鎖了。

// 新增清除函數

void cleanup(void* mutex) { 

   pthread_mutex_unlock((pthread_mutex_t*)mutex);

// 粗體字部分是新增的語句

    pthread_cleanup_push(cleanup,&mutex);

    pthread_mutex_lock(&mutex);

    struct timespec ts = {3, 0};nanosleep(&ts, 0);

    pthread_mutex_unlock(&mutex);

    pthread_cleanup_pop(0);

注意事項3: 實作要清楚延遲撤消和C++之間的相容度

      使用C語言的場合,利用上面的pthread_cleanup_push/pop函數就能安全地執行延遲撤消的動作,但是在C++語言的場合就會出現其他的問題。C++與延遲撤消之間的相容度是非常差的。具體的表現有以下兩個問題:

執行延遲撤消的時候,記憶體棧上的對象的析構函數會不會被調用跟具體的開發環境有關系

GCC3版本就不會調用。

Solaris和Tru64 UNIX下的原生編譯器的場合,就調用析構函數(好像)

pthread_cleanup_push/pop函數和C++的異常處理機制之間有着怎樣的互相影響也能具體環境有關

[Q] Why isn't thread cancellation or termination provided?

[A] There's a valid need for thread termination, so at some point Boost.Threads probably will include it, but only after we can find a truly safe(and portable) mechanism for this concept.

先必須確定對象的自由存儲,而後全都讓cleanup函數去釋放對象的方法也有,但是這次是犧牲了異常安全性。

應該說的是,在使用C++的工程裡不對線程進行延遲撤消處理還是比較實際的。

要準确把握在POSIX标準的函數中,那些函數是非線程安全的,一定不要使用

要讓自己編寫的函數符合線程安全

在通路共享資料/變量之前一定要先鎖定

如果使用C++的話,一定要注意函數的同步方法

 (1) 要準确把握那些非線程安全的函數,一定不要使用

      如果在POSIX平台上進行多線程程式設計時,有幾個最基本的知識,也就是所說的“常識”,希望大家一定要嚴格遵守。

      首先,我們要了解“線程安全”的意思。線程安全的函數就是指,“一個能被在多個線程同時調用也不會發生問題的函數”。這樣的函數通常要滿足以下幾個的特質。

不要操作局部的靜态變量(函數内的static變量)和全局靜态資料(全局變量,函數外的靜态變量)。而且,也不要調用其他的非線程安全的函數

如果要操作這樣的變量的話,事先必須使用互斥鎖mutex進行同步,否則一定要限制多個線程同時對它的通路

struct tm *localtime(const time_t *timer);

      localtime 函數是,把一個用整數形式表示的時刻(從1970/1/1到現在為止的秒數)、轉換成一個能讓人容易明白的年月日形式表示出來的tm結構體并傳回給調用者的函數。根據規格說明、傳回出來的tm結構體是不需要free()掉,也不能釋放的。這個函數典型的實作就像下面的代碼那樣:

struct tm *localtime(const time_t *timer) {

  static struct tm t;

  /* ... 從timer參數裡算出年月日等數值 ... */

  t.tm_year = XXX;

  /* ...把它們填入到結構體内... */

  t.tm_hour = XXX;

  t.tm_min  = XXX;

  t.tm_sec  = XXX;

  return &t;

這個函數如果被像下面那樣使用的話,就會有漏洞:

1.   線上程A裡執行 ta = localtime(x);

2.   線上程B裡執行 tb = localtime(y);

3.   線程A參照ta結構體裡的資料 → 就發現這些資料是一些奇怪的值!

      在函數的說明手冊裡對這個問題也沒有做過詳細的說明。關于這個漏洞,在localtime函數即使使用了mutex鎖也不能被回避掉。是以,這個函數定義的識别辨別是不行滴。

[譯 者lymons注:在多個線程裡調用localtime函數之是以有問題的原因是,localtime函數裡傳回的tm構造體是一個靜态的結構體,是以在 線程A裡調用localtime函數時,該結構體被賦予正确的值;而線上程A參照這個結構體之前,線程B又調用localtime的話,這個靜态的結構體 又被賦予新的一個值。是以線上程A對這個結構體的通路都是基于一個錯誤的值進行的]

asctime, basename, catgets, crypt, ctime, dbm_clearerr, dbm_close, dbm_delete,dbm_error, dbm_fetch, dbm_firstkey, dbm_nextkey, dbm_open, dbm_store, dirname,dlerror, drand48, ecvt, encrypt, endgrent, endpwent, endutxent, fcvt, ftw,gcvt, getc_unlocked, getchar_unlocked, getdate, getenv, getgrent, getgrgid,getgrnam,

(省略)

      對于在規格中被定義為非線程安全的函數,應該制定一個避免使用它們的規則出來,并且制作一個能夠自動檢查出是否使用了這些函數的開發環境,應該是比較好的。

      反之,在這裡沒有被登載的POSIX标準函數都被假定為"shall be thread-safe" 的、是以在實際的使用中可以認為在多線程環境裡是沒有問題的(而且在使用的平台上沒有特别地說明它是非線程安全的話)。

[TSF] int rand_r(unsigned *seed);

asctime_r, ctime_r, getgrgid_r, getgrnam_r, getpwnam_r, getpwuid_r,gmtime_r, localtime_r, rand_r, readdir_r, strerror_r, strtok_r

      還有,在規格以外,還準備了很多的下面那樣的函數。

gethostbyname_r, gethostbyname2_r

The freeaddrinfo() and getaddrinfo() functions shall bethread-safe.

     在多線程程式設計中,不要使用非線程安全的函數,而他們的備用函數可以放心地積極的去使用。

<a href="http://d.hatena.ne.jp/yupo5656/20040809/p2" target="_blank">後續</a>

(2)要讓自己編寫的函數符合線程安全

      在寫多線程的應用程式時,在多個線程裡共享的變量要先鎖定然後在更新它。那麼在多線程裡共享的變量主要有全局變量和函數内的靜态變量。而且,即使是short型和int型的共享變量也要先鎖定後更新才能保證其安全。

      還有,在使用C++程式設計的場合要注意函數的方步方法。一般的說來下面的寫法是錯誤的。Mutex在函數内被聲明成靜态變量是不允許的。

int incr_counter(void) {

  static Mutex m;  // 這麼寫不行

  m.Lock();

  static int counter = 0;

  int ret = ++counter;

  m.Unlock();

  return ret;

應該用下面的方式來代替,

Mutex m;

  // ...

把Mutex聲明成全局變量的話比較好(稍微比上一個好)。

UNIX上C++程式設計守則(6)-- 補記

線程安全函數是像下面那樣

不要操作局部的靜态變量(函數内的static型的變量)和非局部的靜态資料(全局變量)。并且,其它的非線程安全函數不要調用

要操作這樣的變量的話, 就要使用mutex進行同步處理,來限制多個線程同時對它進行操作

被定義的,但是

特别是前者, 和被叫做可重入的(reentrant)函數有差別

反之, 後者特别是和叫做"Serializable"(不單單是MT-Safe)"Safe"的函數有差別

嗯, 因為比較詳細的, 如果不是在對于執行速度要求比較苛刻的環境中編寫代碼的話, 單單地意識到「是否線程安全」就足夠了,不是嗎。

---------------------------------------------------

歡迎轉載,請注明作者和出處

本文轉自 zhenjing 部落格園部落格,原文連結:http://www.cnblogs.com/zhenjing/archive/2010/12/23/thread_cancel.html   ,如需轉載請自行聯系原作者