1. 程式人生 > >多執行緒程式設計之Linux環境下的多執行緒(一)——好文

多執行緒程式設計之Linux環境下的多執行緒(一)——好文

一、Linux環境下的執行緒

  相對於其他作業系統,Linux系統核心只提供了輕量級程序的支援,並未實現執行緒模型。Linux是一種“多程序單執行緒”的作業系統,Linux本身只有程序的概念,而其所謂的“執行緒”本質上在核心裡仍然是程序

     程序是資源分配的單位,同一程序中的多個執行緒共享該程序的資源(如作為共享記憶體的全域性變數)。Linux中所謂的“執行緒”只是在被建立時clone了父程序的資源,因此clone出來的程序表現為“執行緒”,這一點一定要弄清楚。因此,Linux“執行緒”這個概念只有在打引號的情況下才是最準確的。

  目前Linux中最流行的執行緒機制為LinuxThreads

,所採用的就是執行緒-程序“一對一”模型,排程交給核心,而在使用者級實現一個包括訊號處理在內的執行緒管理機制。LinuxThreads由Xavier Leroy負責開發完成,並已繫結在GLIBC中發行,它實現了一種BiCapitalized面向Linux的Posix 1003.1c “pthread”標準介面。Linuxthread可以支援Intel、Alpha、MIPS等平臺上的多處理器系統。 

  需要注意的是,Linuxthread執行緒模型存在一些缺陷,尤其是在訊號處理、排程和程序間同步原語方面都存在問題。並且,這個執行緒模型也不符合POSIX標準的要求。為了解決LinuxThread的缺陷,RedHat開發了一套符合POSIX標準的新型執行緒模型:NPTL(Native POSIX Thread Library)。關於Linuxthread與NPTL的比較,請參考文章:

Linux 執行緒模型的比較:LinuxThreads 和 NPTL

二、Linux環境下的多執行緒編譯支援

  按照POSIX 1003.1c 標準編寫的程式與Linuxthread 庫相連結即可支援Linux平臺上的多執行緒,在程式中需包含標頭檔案pthread. h,在編譯連結時使用命令:

gcc -D -REENTRANT -lpthread xxx. c

  其中-REENTRANT巨集使得相關庫函式(如stdio.h、errno.h中函式) 是可重入的、執行緒安全的(thread-safe),-lpthread則意味著連結庫目錄下的libpthread.a或libpthread.so檔案。  

  在一個多執行緒程式裡,預設情況下,只有一個errno變數供所有的執行緒共享。在一個執行緒準備獲取剛才的錯誤程式碼時,該變數很容易被另一個執行緒中的函式呼叫所改變。類似的問題還存在於fputs之類的函式中,這些函式通常用一個單獨的全域性性區域來快取輸出資料。

       為解決這個問題,需要使用可重入的例程。可重入程式碼可以被多次呼叫而仍然工作正常。編寫的多執行緒程式,通過定義巨集_REENTRANT來告訴編譯器我們需要可重入功能,這個巨集的定義必須出現於程式中的任何#include語句之前。

       _REENTRANT為我們做三件事情,並且做的非常優雅:

(1)它會對部分函式重新定義它們的可安全重入的版本,這些函式名字一般不會發生改變,只是會在函式名後面新增_r字串,如函式名gethostbyname變成gethostbyname_r。

(2)stdio.h中原來以巨集的形式實現的一些函式將變成可安全重入函式。

(3)在error.h中定義的變數error現在將成為一個函式呼叫,它能夠以一種安全的多執行緒方式來獲取真正的errno的值。

三、Linux環境下的多執行緒函式

 3.1 執行緒建立

  在程序被建立時,系統會為其建立一個主執行緒,而要在程序中建立新的執行緒,則可以呼叫pthread_create函式:

#include <pthread.h>
int pthread_create(pthread_t *thread, pthread_attr_t *attr, void *(*start_routine)(void *), void *arg);

  引數說明:

  • thread:指向pthread_create型別的指標,用於引用新建立的執行緒。
  • attr:用於設定執行緒的屬性,一般不需要特殊的屬性,所以可以簡單地設定為NULL。
  • start_routine:傳遞新執行緒所要執行的函式地址。
  • arg:新執行緒所要執行的函式的引數。

  返回值:

  呼叫如果成功,則返回值是0;如果失敗則返回錯誤程式碼。

  每個執行緒都有自己的執行緒ID,以便在程序內區分。執行緒ID在pthread_create呼叫時回返給建立執行緒的呼叫者;一個執行緒也可以在建立後使用pthread_self()呼叫獲取自己的執行緒ID: 

pthread_self (void);

3.2 執行緒退出

  執行緒的退出方式有三種:

(1)執行完成後隱式退出;

(2)由執行緒本身顯示呼叫pthread_exit 函式退出;

pthread_exit (void * retval);

(3)被其他執行緒用pthread_cance函式終止:

pthread_cancel (pthread_t thread);

  如果一個執行緒要等待另一個執行緒的終止,可以使用pthread_join函式,該函式的作用是呼叫pthread_join的執行緒將被掛起直到執行緒ID為引數thread的執行緒終止:

pthread_join (pthread_t thread, void** threadreturn);

3.3 簡單的多執行緒示例

  一個簡單的Linux多執行緒示例如下:

複製程式碼
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void *thread_function(void *arg);

char message[] = "Hello World";

int main()
{
    int res;
    pthread_t a_thread;
    void *thread_result;

    res = pthread_create(&a_thread, NULL, thread_function, (void *)message);
    if (res != 0)
    {
        perror("Thread creation failed!");
        exit(EXIT_FAILURE);
    }

    printf("Waiting for thread to finish.../n");
    
    res = pthread_join(a_thread, &thread_result);
    if (res != 0)
    {
        perror("Thread join failed!/n");
        exit(EXIT_FAILURE);
    }

    printf("Thread joined, it returned %s/n", (char *)thread_result);
    printf("Message is now %s/n", message);

    exit(EXIT_FAILURE);
}

void *thread_function(void *arg)
{
    printf("thread_function is running. Argument was %s/n", (char *)arg);
    sleep(3);
    strcpy(message, "Bye!");
    pthread_exit("Thank you for your CPU time!");
}
複製程式碼

  編譯語句如下:

gcc -D_REENTRANT thread1.c -o thread1 –lpthread

  輸出結果是:

$./thread1[輸出]:
thread_function is running. Argument was Hello World
Waiting for thread to finish...
Thread joined, it returned Thank you for your CPU time!
Message is now Bye!

  在這個例子中,pthread_exit(void *retval)本身返回的就是指向某個物件的指標,因此,pthread_join(pthread_t th, void **thread_return);中的thread_return是二級指標,指向執行緒返回值的指標。可以看到,我們建立的新執行緒修改的陣列message的值,而原先的執行緒也可以訪問該陣列。如果我們呼叫的是fork而不是pthread_create,就不會有這樣的效果了。因為fork建立子程序之後,子程序會拷貝父程序,兩者分離,相互不干擾,而執行緒之間則是共享程序的相關資源。

小結:

  本文主要講了Linux環境下的多執行緒基本概念,包括多執行緒的實現方式、函式介面、功能特性等。