咱們都知道,在編寫多線程程序時,咱們應該記住不少細節,好比鎖,使用線程安全庫等。這裏有一個不太明顯的bug的列表,特定於多線程程序。其中許多都沒有在初學者的文檔或教程中提到,但我認爲每一個使用線程的人最終都會中槍。html
並不是全部的系統函數或者庫函數都能被安全地使用。最明顯的例子之一是strtok(3),它執行字符串符號化。它在每次調用中返回下一個token,並使用全局狀態來保持源字符串中的當前位置。當您閱讀此函數的手冊頁時,linux
您將看到有thread-safe版本:strtok_r(3)帶有附加參數:使用狀態變量的指針,而不是全局變量的指針。有這種功能的其餘例子還有:數據庫
你可能認爲你只是在使用一個共享的「簡單」變量,好比它是一個沒有mutex的布爾變量。安全
1 bool stop = false; 2 3 while (!stop) { 4 sleep (1); 5 }
上述代碼在開啓編譯優化的狀況下是不可能被其餘線程經過設置stop變量爲true來中斷的。這是由於編譯器能夠自由應用優化:一種緣由是當編譯器發現該變量在循環中沒有被修改時,它能夠省略while條件。另外一種緣由是,根據系統的架構,網絡
這種內存上的變化可能沒有被其餘處理器注意到。第一種狀況,當時在調試一個數據庫應用時有遇到過,當時狀況是:在一個過程當中,初始化一個局部變量後,balabala進行了一大堆操做,而後才使用該變量。最後測試發現結果不對,在我多多線程
次調試後才發現該變量一直處於未初始化狀態的默認值。這還一度讓我認爲該不會是給該變量的賦值操做沒起做用形成的,最後沒招,我嘗試提早了該變量的使用位置,結果就行了。。。這時我才忽然意識到極可能是編譯優化的問題形成的。架構
這種因爲編譯優化形成的bug排查仍是很費勁的。dom
volatile關鍵字有時被視爲是一種解決方案,但它與線程無關。此關鍵字旨在用於底層代碼(如設備驅動程序),只是爲了確保寫入設備的內存等。在多線程進程中它並不能作到咱們須要的:它不能使內存中的內容的變化被其餘處理器可見。socket
在一些架構上它可能能夠,但不該該這樣使用。函數
正確的解決方案偏偏是在訪問stop變量時使用mutex,即便它是如此「簡單」的內存訪問。
考慮以下代碼片斷:
1 fd = open ("file", O_RDONLY); 2 if (fd < 0) exit (1); 3 4 while ((res = read (fd, buf, sizeof(buf)))) { 5 if (res < 0) { 6 close(fd); 7 fprintf (stderr, "Read error!\n"); 8 break; 9 } 10 else { 11 printf ("Read %zd bytes\n", res); 12 } 13 } 14 15 close(fd);
哪有問題?在單線程程序中,它能正常工做,即便有bug存在:在第4行發生讀取錯誤的狀況下,文件描述符將被關閉兩次 - 第15行的close(2)將只返回一個將被忽略的錯誤。然而在多線程程序中使用這段代碼會讓你陷入麻煩,
一般很討厭。爲何?由於第15行的第二次close(3)可能不會失敗。這裏存在race condition:若是其餘線程在第一次與第二次close(3)之間打開了一個file或者建立了一個socket而且得到了相同的fd,那麼上述線程會關閉它。
要知道,文件描述符在同一進程的線程之間是共享的。關閉其餘線程的fd可能不是最糟糕的可能發生的狀況,試想:若是上述代碼的第二個close()以前嘗試進行了寫操做,這將致使會向其餘線程的文件或者TCP鏈接進行寫操做!
二次關閉是多線程中可能發生的最難發現的bug之一。由於這種race condition不多復現而且結果一般是很奇怪的錯誤。做爲一種解決方法:建議常常檢查每個close(3)的返回值。可是一般在程序中不會去檢查,特別是當fd只是用於
讀文件的狀況,固然,這要首先看讀文件會不會失敗了。若是用日誌記錄每次close(3)失敗的狀況,咱們就能夠在race condition發生以前發現這種bug。在大多數狀況下,第二個close(3)更有可能失敗而不是會去關閉其餘線程的fd。
未捕獲的異常將致使進程退出並顯示錯誤消息。當編寫多進程網絡daemon程序時,這樣的錯誤將終止一個進程,而且正確編寫的程序將從新產生該錯誤。當這樣的守護進程被轉換爲多線程設計時,未捕獲的異常更危險:
由於它將kill整個程序,而不僅是一個線程。因此必須記住這一點而且在最頂層代碼的某處捕獲一切異常,即便是經過下面這種方式:
1 try 2 ... 3 catch(...) 4 { log(「unknown exception」) }
catch(...)而不是從新拋出異常是雖然是一個不太好的作法,但至少程序仍然能夠處理其他的客戶端請求。這多是惟一catch(...)的狀況。
關於多線程的進程與fork()的東西,後面的文章我會進行總結,也能夠先看open(2)以及dup3(2)的O_CLOEXEC標記的使用說明。但基本上:在多線程進程中沒有安全的方式使用fork(),
而且在子進程中作不止是執行execve()的事情。由於你不能知道fork()調用時其餘線程在作什麼,一些mutex可能已經被一些線程持有了,一些線程可能正在修改一些複雜數據的過程當中等等。
這裏是一個性能提示:避免在持有互斥量的同時進行I/O操做。至少要避免I/O操做,最好是在mutex被鎖定的狀況下避免任何系統調用或甚至庫調用。
相信我:你不會但願在一個很是繁忙的網絡daemon進程中每秒至少處理數千個請求的線程等待一些恰巧在持有mutex的狀況下經過syslog(3)系統調用寫一些錯誤消息的線程。使用互斥體只是爲了同步對內存的訪問,
並儘快解鎖它們。看下面這個例子:
1 pthread_mutex_lock (&mutex); 2 if (freeSlots == 0) { 3 syslog (LOG_ERR, "No slots available, rejecting request"); 4 } else { 5 freeSlots--; 6 } 7 pthread_mutex_unlock (&mutex);
在syslog(3)調用時,mutex已經處於被持有狀態。根據syslog守護程序的配置和機器的負載,當在每一個日誌行以後執行fsync()時,這甚至可能須要幾十或幾百毫秒來完成。因此在進行日誌記錄以前,只需解鎖互斥,
這樣其餘線程就能夠運行而不須要等待I/O完成。
若是你使用的是C ++語言,不要直接使用POSIX mutexes函數。建立一個Mutex類會容易不少,這樣就能夠在構造函數中得到鎖,而在析構函數中釋放鎖。這種方式只是建立該類的自動變量,但它會在構造函數中得到鎖,
並在代碼的做用域結束時因析構函數而自動解鎖。這種類的一個示例是Boost庫中的scoped_lock。