linux下多線程

linux下多線程

linux系統下的多線程遵循POSIX線程接口,連接時需要使用庫libpghread.a。
這裏選擇一些常用的線程函數進行簡單的分析和練習,以使自己對linux下多線程有一定的認識。
注意:隨着時間的推移,很多東西可能不再適用,當前的版本信息如下。

  • linux內核版本:

    uname -r
    4.4.0-59-generic

  • gcc版本

    gcc –version
    gcc (Ubuntu 5.4.0-6ubuntu1~16.04.4) 5.4.0 20160609

1 多線程依賴頭文件

#include <pthread.h>

2 線程id 線程id用來唯一標識每一個線程,在操作系統級別具有唯一性,類型是pthread_t,其值由創建線程函數pthread_create賦予。

pthread_t的定義在/usr/include/x86_64-linux-gnu/bits/pthreadtypes.h中,具體定義是
typedef unsigned long int pthread_t;

3 線程屬性

對於每一個線程來說,都有線程屬性,屬性包括許多屬性,且屬性不能直接設置,需要通過相關的函數進行設置。
pthread_attr_init用來將每一個屬性設置爲默認值,函數中會申請一些其他資源,需要通過pthread_attr_destroy來釋放。

3.1 作用域

作用域表示的是新創建的線程與其他線程競爭資源的範圍,有兩種取值,一種爲PTHREAD_SCOPE_SYSTEM,表示新創建的線程於操作系統內所有其他線程競爭資源,另外一個取值是PTHREAD_SCOPE_PROCESS,表示的是新創建的線程與該進程創建的其他屬性值爲PTHREAD_SCOPE_PROCESS的線程競爭資源。

int pthread_attr_getscope(const pthread_attr_t *attr, int* scope);
用來獲取屬性結構體attr中作用域的值,執行成功,scope的值會被設置爲作用域的值,返回0,否則返回非0值。

int pthread_attr_setscope(pthread_attr_t *attr, int scope);
用來將屬性結構體attr中作用域的值設置爲指定作用域,即參數scope的值。執行成功,attr中的作用域會被修改爲scope的值,返回0,否則返回非0。以下兩種情況pthread_attr_setscope會執行失敗。
* EINVAL 指定了錯誤的scope值。
* ENOTSUP 指定了PTHREAD_SCOPE_PROCESS,目前linux系統不支持。

下面一個簡單的demo用來介紹兩個函數的使用方法:

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <assert.h>
#include <stdbool.h>
#include <errno.h>

// 報錯處理,將錯誤信息展示給用戶
#define handle_error_eno(eno, msg)          \
        do { errno = eno; perror(msg); exit(EXIT_FAILURE); }while(0)                            

/**
 * @free_resource 釋放資源,避免非正常退出時資源未正常釋放。
 *
 * @param exitno exit執行時的退出號
 * @param resource 釋放的資源,該用例中僅爲線程屬性attr
 */
void free_resource(int exitno, void* resource)
{
    if (exitno != EXIT_SUCCESS)
    {
        printf("非正常退出,準備釋放attr...");
        if (pthread_attr_destroy((pthread_attr_t *)resource) != 0)
        {
            printf("attr釋放失敗,請檢查原因!\n");
        }
        else
            printf("attr釋放成功,退出!\n");
    }
}


int main(void)
{
    pthread_attr_t attr;
    int scope;
    int eno = 0;

    //POSIX中爲pthread_attr_init指定了錯誤ENOMEM,但實際上在linux的實現中,不會失敗,因爲linux中採用數組保存各個屬性的值。
    eno = pthread_attr_init(&attr);
    if (eno != 0)
        handle_error_eno(eno, "多線程屬性值初始化失敗\n");//這裏退出時還沒有註冊退出函數,所以不會有問題。                                    

    //從這裏開始attr已經有值,爲了避免異常退出導致的資源非正常釋放,這裏註冊退出時處理函數
    eno = on_exit(free_resource, (void*)&attr); 
    if (eno != 0)
        handle_error_eno(eno, "註冊異常退出函數失敗"); //這裏退出時退出函數註冊失敗,所以也不會有問題。                                        

    //獲取系統爲該線程初始化的scope的默認值。
    eno = pthread_attr_getscope(&attr, &scope);
    if (eno != 0)
        handle_error_eno(eno, "多線程獲取scope屬性值失敗");

    if (PTHREAD_SCOPE_SYSTEM == scope)
        printf("pthread_attr_init初始化的scope值是PTHREAD_SCOPE_SYSTEM!\n");
    else if (PTHREAD_SCOPE_PROCESS == scope)
        printf("pthread_attr_init初始化的scope值是PTHREAD_SCOPE_PROCESS!\n");
    else
        assert(false);//理論上來說scope應該只有兩個取值。

    //將scope設置爲無效的值99,看實際執行情況。
    eno = pthread_attr_setscope(&attr, 99);
    if (eno != 0)
    {
        errno = eno;
        perror("99不能設置爲scope的值");
    }
    else
    {
        eno = pthread_attr_getscope(&attr, &scope);
        if (eno != 0)
            handle_error_eno(eno, "socpe的值設置爲99後獲取scope值失敗");
        else
            printf("99也是scope的可用值!\n");
    }

    //爲scope設置爲PTHREAD_SCOPE_SYSTEM,看實際執行情況。
    eno = pthread_attr_setscope(&attr, PTHREAD_SCOPE_SYSTEM);
    if (eno != 0)
    {
        errno = eno;
        perror("將scope設置爲PTHREAD_SCOPE_SYSTEM失敗");
    }
    else
    {
        eno = pthread_attr_getscope(&attr, &scope);
        if (eno != 0)
            handle_error_eno(eno, "多線程獲取scope屬性值失敗");
        else
            printf("PTHREAD_SCOPE_SYSTEM是scope的可用值!\n");
    }

    //爲scope設置爲PTHREAD_SCOPE_PROCESS,看實際執行情況。
    eno = pthread_attr_setscope(&attr, PTHREAD_SCOPE_PROCESS);
    if(eno != 0)
    {
        errno = eno;
        perror("將scope設置爲PTHREAD_SCOPE_PROCESS失敗");
    }
    else
    {
        eno = pthread_attr_getscope(&attr, &scope);
        if (eno != 0)
            handle_error_eno(eno, "多線程獲取scope屬性值失敗");
        else
            printf("PTHREAD_SCOPE_PROCESS是scope的可用值\n");
    }

    //POSIX中爲pthread_attr_destroy指定了錯誤ENOMEM,但實際上在linux的實現中,不會失敗,因爲linux中採用數組保存各個屬性的值。
    eno = pthread_attr_destroy(&attr);
    if (eno != 0)
        handle_error_eno(eno, "多線程屬性值銷燬失敗");

    return 0;
}

檢驗方法:
將上面的代碼保存到文件pthread_attr_scope.c中後執行命令
gcc -pthread pthread_attr_scope.c -o pthread_attr_scope.out編譯鏈接。

執行./pthread_attr_scope.out後的輸出爲:
pthread_attr_init初始化的scope值是PTHREAD_SCOPE_SYSTEM!
99不能設置爲scope的值: Invalid argument
PTHREAD_SCOPE_SYSTEM是scope的可用值!
將scope設置爲PTHREAD_SCOPE_PROCESS失敗: Operation not supported

屬性總結:
1 該輸出驗證了linux下不支持將scope屬性設置爲PTHREAD_SCOPE_PROCESS。
2 在linux下,該屬性不需要單獨設置。
3 設置該屬性前,最好寫個小demo測試下該平臺是否支持該屬性的設置,避免無用的代碼。

3.2 CPU親緣性

對於早期的單核單線程操作系統來說,操作系統是通過cpu的輪轉來實現所謂的並行,帶來了響應速度上的提升,但同時相對來說也帶來了實際執行時間的延長。
現代cpu大多具有多個線程,而且部分cpu還有超線程能力,但對於目前操作系統上運行的衆多程序來說,cpu線程數依然不足,所以通常來說,操作系統依然是通過調度算法,選擇某個cpu線程來完成你所需要完成的某個任務,也就是說,在很多情況下,你的程序可能會在不同的cpu線程之間切換。
在大多數情況下,這是完全沒有問題的,甚至可以說這是效率更高的,因爲操作系統更瞭解目前系統的運行情況,會比較智能的選擇較爲空閒的cpu線程來執行你的程序,避免某些cpu線程運行壓力極大而其他cpu線程閒置的問題。
但某些特殊情況,人爲的控制程序在某個或某些cpu線程上運行,會提供不錯的性能提升。

這裏爲了避免我們創建的線程和cpu線程混淆,我們之後都成cpu線程爲cpu,而我們要用代碼創建的線程成爲線程。
注:這裏舉一個簡單的例子來介紹cpu數目/cpu內核數/cpu線程數,某款cpu具有4核8線程,這裏就是說,一個cpu,有4個內核,而且由於cpu具有超線程能力,有8個可用線程。因此,理論上來說這樣的機器我們就有8個cpu可用,但實際上在很多時候並不能達到8個物理cpu的效率,這裏是因爲線程與線程之間/內核與內核之間有些東西是共享的,因此很多時候某個線程/內核工作的時候,其他線程/內核並不能同時工作。例如:cpu緩存,對外連接的通道等等。

例如:
1 將某個線程綁定到某個cpu上,其他的線程綁定到其他cpu上,可以獲取更快的響應速度和更多的執行時間。
2 將某些線程綁定到某個或某幾個cpu上,可以通過共享cpu Cache的方式提升性能。(這裏涉及到一些概念,一級數據緩存,一級指令緩存,二級緩存,三級緩存等,這裏提供一些參考文檔。)
七個例子幫你更好的理解cpu緩存
每個程序員都應該瞭解的CPU高速緩存
3 可以測試一些程序的瓶頸,比如某些程序在使用多個cpu時表現更好,當cpu個數達到一定程度時,反而會導致性能的下降。
4 未完待續…

綜上所述,我們可以簡單的認爲cpu親緣性是指該線程/進程與哪些cpu更爲親近,或者更乾脆的認爲cpu親緣性就是指我們人爲的將線程/進程綁定在某些/某個cpu上,從而讓其避免切換到之外的cpu上執行,使cpu緩存失效。爲了更方便的理解,我們從這裏開始將cpu親緣性稱爲綁定屬性。

那麼,一個線程如果綁定到所有 可用的 cpu上,我們稱之爲一個cpu集合(這個集合中包含了所有可用的cpu),那麼理論上我們可以說,若線程執行時間足夠長,該線程可以切換到任意一個可用的cpu上執行。我們假設這個集合是全集,那麼也就是說,我們可以通過調整這個集合中的cpu,來實現將線程綁定在不同的cpu集合上,更極端的是,若集合內只有一個cpu時,我們就實現了將該線程綁定在某個cpu上的任務。
上面我們講的cpu集合,已經有人爲我們想到了,對應的數據結構是cpu_set_t,這個結構中存儲着我們可用的cpu,對應的,有一系列的宏函數來讓我們操作這個集合,比如:
1 CPU_ZERO來清空集合內所有的cpu。
2 CPU_SET將某個cpu添加到集合中。
3 CPU_ISSET判斷某個cpu是否在集合中。
4 CPU_CLR將某個cpu從集合中剔除。
5 CPU_COUNT獲取集合內cpu的個數。
6 CPU_ALLOC申請一個足夠存放n個cpu的集合。
7 CPU_FREE釋放對應的cpu集合。
等等,對應的還有更爲安全的用法,以_S結尾,這些都可以去man手冊中去查看。
注:如果有心試驗一下的話,會發現當計算機只有8個線程時,9/10甚至1023都是可以添加到集合中的,因此並不能用CPU_ISSET來檢查操作系統具體有多少個cpu,這裏推薦兩個函數來判斷計算機有多少個cpu和有多少個可用的cpu。

#include <sys/sysinfo.h>
int get_nprocs(void);//多少個可用cpu
int get_nprocs_conf(void);//多少個cpu
//僅可在linux下執行,Windows請查看對應的API。

int pthread_attr_getaffinity_np(const pthread_attr_t *attr, size_t cpusetsize, cpu_set_t *cpuset);
用來獲取線程屬性cpu親和性的值,從線程數性結構體attr中獲取,結果存儲在cpuset中,當執行成功返回0,否則返回非0。注意:cpuset必須有空間,例如:

    cpu_set_t *cpusetp = CPU_ALLOC(N);

或者

    cpu_set_t cpuset;
    &cpuset;

注:以上代碼並不能執行,這裏只是爲了解釋cpuset必須有實際空間,避免段錯誤。

int pthread_attr_setaffinity_np(pthread_attr_t *attr, size_t cpusetsize, const cpu_set_t *cpuset);
用來設置線程屬性cpu親和性的值,將線程屬性設置爲綁定cpuset中的cpu,成功返回0,失敗返回非0。

下面一個簡單的demo用來介紹兩個函數的使用方法:

2 pthread_create

2.1 函數功能

創建線程

2.2 函數原型

int pthread_create(pthread_t thread, const pthread_attr_t *attr, void (start_routine) (void ), void *arg);

2.3 參數說明

  • pthread_t *thread
    輸出參數,線程id的地址,函數pthread_create會爲新創建的線程指定對應的線程id。

  • const pthread_attr_t *attr
    pthread_attr_t的定義在/usr/include/x86_64-linux-gnu/bits/pthreadtypes.h中,具體定義是
    union pthread_attr_t
    {
    char __size[__SIZEOF_PTHREAD_ATTR_T];
    long int __align;
    };

    __SIZEOF_PTHREAD_ATTR_T的值也在/usr/include/x86_64-linux-gnu/bits/pthreadtypes.h中定義,具體值由一些宏控制,可能爲56/32/36。
    線程屬性包括許多屬性,屬性不能直接設置,需要通過相關的函數進行設置,pthread_attr_init用來完成對屬性的初始化,初始化後所有屬性爲操作系統實現支持的所有屬性的默認值。pthread_attr_init需要在pthread_create之前調用。
    對於某些值如果不想使用默認值,可以調用該屬性的設置方法來爲該重性重新賦值,一般爲pthread_attr_xxx
    pthread_attr_destroy用來在屬性不再需要的時候釋放屬性內所有資源。

  • void (*start_routine) (void )
    線程起始函數,即新創建的函數從該函數開始執行。

  • void *arg
    線程函數所需參數。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章