《Linux 應用編程》—第13章 Linux 多線程編程

1 多線程概述

1.1 什麼是線程

線程是進程內的順序執行流,一個進程中能夠併發多條線程,每條線程並行執行不一樣的任務。html

1.2 線程與進程的關係

  • 一個線程只能屬於一個進程,一個進程能夠包含多個線程,可是至少有一個主線程
  • 資源分配給進程,同一進程的全部線程共享該進程的全部資源
  • 線程做爲調度和分配的基本單位,進程做爲擁有資源的基本單位(這裏說的是操做系統資源吧)
  • 在建立或撤銷進程時,因爲系統都要爲之分配和回收資源,致使系統的開銷大於建立或撤銷線程時的開銷(也就是說建立或撤銷進程包含建立或撤銷線程,同時還有分配和回收資源)

小聲bb:在FreeRTOS等小型操做系統,或者說是任務調度微內核裏面好像沒有進程的概念.安全

引用"阮一峯"的博客內容bash

CPU:工廠; 假定工廠的電力有限,一次只能供給一個車間使用。也就是說,一個車間開工的時候,其餘車間都必須停工。背後的含義就是,單個CPU一次只能運行一個任務。多線程

進程:車間; 進程就比如工廠的車間,它表明CPU所能處理的單個任務。任一時刻,CPU老是運行一個進程,其餘進程處於非運行狀態。併發

一個車間裏,能夠有不少工人。他們協同完成一個任務。函數

線程:工人; 線程就比如車間裏的工人。一個進程能夠包括多個線程。操作系統

車間的空間是工人們共享的,好比許多房間(內存空間)是每一個工人均可以進出的。這象徵一個進程的內存空間是共享的,每一個線程均可以使用這些共享內存線程

但是,每間房間的大小不一樣,有些房間最多隻能容納一我的,好比廁所。裏面有人的時候,其餘人就不能進去了。這表明一個線程使用某些共享內存時,其餘線程必須等它結束,才能使用這一塊內存。設計

一個防止他人進入的簡單方法,就是門口加一把鎖。先到的人鎖上門,後到的人看到上鎖,就在門口排隊,等鎖打開再進去。這就叫"互斥鎖"(Mutual exclusion,縮寫 Mutex),防止多個線程同時讀寫某一塊內存區域。指針

還有些房間,能夠同時容納n我的,好比廚房。也就是說,若是人數大於n,多出來的人只能在外面等着。這比如某些內存區域,只能供給固定數目的線程使用。

時的解決方法,就是在門口掛n把鑰匙。進去的人就取一把鑰匙,出來時再把鑰匙掛回原處。後到的人發現鑰匙架空了,就知道必須在門口排隊等着了。這種作法叫作"信號量"(Semaphore),用來保證多個線程不會互相沖突。

不難看出,mutex是semaphore的一種特殊狀況(n=1時)。也就是說,徹底能夠用後者替代前者。可是,由於mutex較爲簡單,且效率高,因此在必須保證資源獨佔的狀況下,仍是採用這種設計。

1.3 爲何使用多線程

  • 方便通訊和數據交換

    線程間有方便的通訊和數據交換機制。對不一樣進程來講,它們具備獨立的數據空間,要進行數據的傳遞只能經過通訊的方式進行,這種方式不只費時,並且很不方便。線程則否則,因爲同一進程下的線程之間共享數據空間,因此一個線程的數據能夠直接爲其它線程所用,這不只快捷,並且方便。

  • 更高效的利用CPU

    使用多線程能夠提升應用程序響應(說明多線程也是輪轉的)。這對圖形界面的程序尤爲有意義,當一個操做耗時很長時,整個系統都會等待這個操做,此時程序不會響應鍵盤、鼠標、菜單的操做,而使用多線程技術,將耗時長的操做置於一個新的線程,能夠避免這種尷尬的狀況。

2 POSIX Threads 概述

POSIX Threads(一般簡稱爲 Pthreads)定義了建立和操縱線程的一套 API 接口, 通常用於 Unix-like POSIX 系統中(如 FreeBSD、 GNU/Linux、 OpenBSD、 Mac OS 等系統)。

Pthreads接口根據功能劃分:

  • 線程管理
  • 互斥量
  • 條件變量
  • 同步

寫Pthreads多線程程序的時遠源碼須要包含pthread.h頭文件,LDFLAGS += -pthread,能夠用來指定須要包含的庫。Makefile 選項 CFLAGS 、LDFLAGS 、LIBS能夠了解一下。

3 線程管理

3.1 線程ID

定義:能夠看作是線程的句柄,用來引用一個線程

  • pthread_self函數

    • 做用:獲取線程本身的ID
    • 返回值:pthread_t
    • 形參:void
  • pthread_equal函數

    • 做用:比較兩個線程ID是否相等
    • 返回值:相等返回非0值,不然返回0
    • 形參:pthread_t類型的兩個參數

3.2 建立與終止

1. 建立線程

  • pthread_create()函數
    • 做用:在進程中建立一個新線程
    • 返回值: pthread_create()調用成功,函數返回 0,不然返回一個非 0 的錯誤碼
    • 形參:thread指向新建立的線程ID、attr爲線程屬性對象、start_routine是線程開始時調用的的函數的名字、arg爲start_routine指定的函數的參數。

2. 終止線程

  • 進程的終止

    一、直接調用exit()。任何一個線程調用exit()都會致使進程退出

    二、執行main()函數中的return。 進程的主函數終止了進程也就結束了

    三、經過進程的某個其餘線程調用exit()函數。任何一個線程調用exit()都會致使進程退出

  • 主線程、子線程調用exit, pthread_exit,互相產生的影響。

    一、在主線程中,在main函數中return了或是調用了exit函數,則主線程退出,且整個進程也會終止,此時進程中的全部線程也將終止。所以要避免main函數過早結束。【隱式調用】

    任何線程調用exit()都會致使進程結束。

    主線程的main函數跑到return語句會致使進程結束。

    主線程/進程的結束致使全部線程的結束。

    二、在主線程中調用pthread_exit, 則僅僅是主線程結束,進程不會結束,進程內的其餘線程也不會結束,直到全部線程結束,進程纔會終止。

    調用pthread_exit的線程只會結束本線程,主線程調用也只會結束自身,不會結束進程。

    三、在任何一個線程中調用exit函數都會致使進程結束。進程一旦結束,那麼進程中的全部線程都將結束。

    進程內任何地方調用exit()都會結束進程。

  • 主線程

    若是主線程在建立了其它線程後沒有任務須要處理,那麼它應該阻塞等待全部線程都結束爲止,或者應該調用pthread_exit(NULL)。

    調用pthread_exit(NULL)能夠減小一個線程開銷,看一下線程怎麼阻塞。

  • pthread_exit()函數

    • 做用:使得調用線程終止
    • 返回值:void
    • 形參:retval 是一個 void 類型的指針。
      須要理解「線程的返回值",線程是有返回值的。

    pthread_exit(void *ptr) 函數使線程退出,並返回一個空指針類型的值。

    pthread_join(pthread_t tid,void **rtval)調用此函數的進程/線程等id爲tid的線程返回或被終止,並從它那裏得到返回值。

    注意,退出函數返回的是一個空指針類型,接收函數也必須用一個指針來接收。可是函數給出的參數是接收指針的地址,即,接收到的指針值寫入給出的地址處的指針變量。

3. 線程範例1

#include "pthread.h"
#include "stdio.h"
#include "stdlib.h"
#define NUM_THREADS 5

void *PrintHello(void *threadid)
{
    long tid;
    tid = (long)threadid;
    printf("Hello World!It's me,thread #%ld!\n",tid);
    pthread_exit(NULL);
}

int main(int argc,char* argv[])
{
    pthread_t threads[NUM_THREADS];
    int rc;
    long t;

    for (t=0;t<NUM_THREADS;t++)
    {
        printf("In main:creating thread %ld\n",t);
        rc = pthread_create(&threads[t],NULL,PrintHello,(void*)t);
        if(rc)
        {
            printf("ERROR;return code from pthread_create() is %d\n",rc);
            exit(-1);
        }
    }
    printf("In main:exit!\n");
    pthread_exit(NULL);
    return 0;
}

編譯的時候須要加lpthread的庫:

gcc thread_begin_end.c -lpthread

輸出以下:

In main:creating thread 0 
In main:creating thread 1
Hello World!It's me,thread #0!
In main:creating thread 2
Hello World!It's me,thread #1!
In main:creating thread 3
Hello World!It's me,thread #2!
In main:creating thread 4
Hello World!It's me,thread #3!
In main:exit!
Hello World!It's me,thread #4!

進程內的線程是共享資源的,In main:creating thread In main:exit!是主線程打印出來的;

Hello World!It's me,thread #好像是子線程打印的,須要看一下pthread_create的形參,形參3是線程開始時候調用的函數,因此就是子線程打印的。

3.3 鏈接與分離

線程能夠分爲分離線程(DETACHED)非分離線程(JOINABLE)兩種。

  • 分離線程是指線程退出時線程將釋放它的資源的線程;分離線程退出時不會報告線程狀態
  • 非分離線程退出後不會當即釋放資源,須要另外一個線程爲它調用 pthread_join 函數或者進程退出時纔會釋放資源。

1. 線程分離

  • pthread_detach()函數 int pthread_detach(pthread_t thread);
    • 做用:能夠將非分離線程設置爲分離線程
    • 形參:thread 是要分離的線程的 ID。
    • 返回值:成功返回 0;失敗返回一個非 0 的錯誤碼。

2. 線程鏈接

  • pthread_join()函數 int pthread_join(pthread_t thread, void **retval);
    • 做用:將調用線程掛起,直到第一個參數 thread 指定目標線程終止運行爲止。
    • 形參:retval 爲指向線程的返回值的指針提供一個位置, 這個返回值是目標線程調用pthread_exit()或者 return 所提供的值。
    • 返回值:成功返回 0;失敗返回一個非 0 的錯誤碼。

3. 線程範例2

#include "pthread.h"
#include "stdio.h"
#include "stdlib.h"
#include "math.h"

#define NUM_THREADS 4

void *BusyWork(void* t)
{
    int i;
    long tid;
    double result=0.0;
    tid = (long)t;

    printf("Thread %ld starting...\n",tid);
    for(i=0;i<1000000;i++)
    {
        result = result + sin(i)*tan(i);
    }
    printf("Thread %ld done.Result =%e\n",tid,result);
    pthread_exit((void*)t);
}

int main(int argc,char* argv[])
{
    pthread_t thread[NUM_THREADS];
    int rc;
    long t;
    void *status;

    for(t=0;t<NUM_THREADS;t++)
    {
        printf("Main: creating thread %ld\n", t);
        rc = pthread_create(&thread[t], NULL, BusyWork, (void *)t);
        if(rc)
        {
            printf("ERROR; return code from pthread_create() is %d\n", rc);
            exit(-1);
        }
    }
    for(t=0;t<NUM_THREADS;t++)
    {
        rc = pthread_join(thread[t], &status);
        if(rc)
        {
            printf("ERROR; return code from pthread_join() is %d\n", rc);
            exit(-1);
        }
        printf("Main: completed join with thread %ld having a status of %ld\n",t,(long)status);
    }
    printf("Main: program completed. Exiting.\n");
    pthread_exit(NULL); 
}

編譯須要是用:

gcc thread_join.c  -lpthread -lm

輸出以下:

Main: creating thread 0
Main: creating thread 1
Thread 0 starting...
Main: creating thread 2
Thread 1 starting...
Main: creating thread 3
Thread 2 starting...
Thread 3 starting...
Thread 0 done.Result =-3.153838e+06
Thread 3 done.Result =-3.153838e+06
Thread 1 done.Result =-3.153838e+06
Main: completed join with thread 0 having a status of 0
Main: completed join with thread 1 having a status of 1
Thread 2 done.Result =-3.153838e+06
Main: completed join with thread 2 having a status of 2
Main: completed join with thread 3 having a status of 3
Main: program completed. Exiting.

Main: creating thread是主線程打印出來的,子線程初始化執行的都是BusyWork函數內的內容。

主線程循環調用pthread_join函數,應該是將主線程掛起,執行到4遍for循環裏面的第一個pthread_join函數以後就掛起,等待第一個建立的子線程結束。

Thread x done.Result =打印沒什麼特別的,線程的調度是隨機的,不肯定哪一個先結束,隨意沒有前後順序

Main: completed join with thread x having a status of打印有兩個特徵,一個是順序打印,緣由是在主線程裏順序執行,另外一個就是必須在對應的子線程的後面

掛起阻塞的區別:

理解一:掛起是一種主動行爲,所以恢復也應該要主動完成,而阻塞則是一種被動行爲,是在等待事件或資源時任務的表現,你不知道他何時被阻塞(pend),也就不能確切 的知道他何時恢復阻塞。並且掛起隊列在操做系統裏能夠當作一個,而阻塞隊列則是不一樣的事件或資源(如信號量)就有本身的隊列。

理解二:阻塞(pend)就是任務釋放CPU,其餘任務能夠運行,通常在等待某種資源或信號量的時候出現。掛起(suspend)不釋放CPU,若是任務優先級高就永遠輪不到其餘任務運行,通常掛起用於程序調試中的條件中斷,當出現某個條件的狀況下掛起,而後進行單步調試。

理解三:pend是task主動去等一個事件,或消息.suspend是直接懸掛task,之後這個task和你沒任何關係,任何task間的通訊或者同步都和這個suspended task沒任何關係了,除非你resume task;

理解四:任務調度是操做系統來實現的,任務調度時,直接忽略掛起狀態的任務,可是會顧及處於pend下的任務,當pend下的任務等待的資源就緒後,就能夠轉爲ready了。ready只須要等待CPU時間,固然,任務調度也佔用開銷,可是不大,能夠忽略。能夠這樣理解,只要是掛起狀態,操做系統就不在管理這個任務了。

理解五:掛起是主動的,通常須要用掛起函數進行操做,若沒有resume的動做,則此任務一直不會ready。而阻塞是由於資源被其餘任務搶佔而處於休眠態。二者的表現方式都是從就緒態裏「清掉」,即對應標誌位清零,只不過實現方式不同。

3.4 線程屬性

線程基本屬性包括: 棧大小、 調度策略和線程狀態。

屬性對象

  • 初始化屬性對象

    int pthread_attr_init(pthread_attr_t *attr);

  • 銷燬屬性對象

    int pthread_attr_destroy(pthread_attr_t *attr);

線程狀態

  • 兩種線程狀態

    • PTHREAD_CREATE_JOINABLE——非分離線程
    • PTHREAD_CREATE_DETACHED——分離線程
  • 獲取線程狀態

    int pthread_attr_getdetachstate(pthread_attr_t *attr, int *detachstate);

  • 設置線程狀態

    int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);

線程棧

Linux系統線程的默認棧大小爲 8MB,只有主線程的棧大小會在運行過程當中自動增加。

  • 獲取線程棧

    int pthread_attr_getstacksize(pthread_attr_t *attr, size_t *stacksize);

  • 設置線程棧

    intpthread_attr_setstacksize(pthread_attr_t *attr, size_tstacksize);

線程範例3

#include "pthread.h"
#include "string.h"
#include "stdio.h"
#include "stdlib.h"
#include "unistd.h"
#include "errno.h"
#include "ctype.h"

#define handle_error_en(en,msg)\
    do{errno=en;perror(msg);exit(EXIT_FAILURE);}while(0)

#define handle_error(msg)\
    do{perror(msg);exit(EXIT_FAILURE);}while(0)
//宏定義裏面爲何要使用 do while(0)?由於能夠保證被替換後實現想要的功能
struct thread_info
{
    pthread_t   thread_id;
    int         thread_num;
    char        *argv_string;
};

static void* thread_start(void *arg)//這塊void*代表函數返回的是一個指針
{
    struct thread_info *tinfo  = arg;
    char *uargv,*p;

    printf("Thread %d:top of stack near %p;argv_thing =%s\n",tinfo->thread_num,&p,tinfo->argv_string);
    uargv = strdup(tinfo->argv_string);
    if(uargv == NULL)
        handle_error("strdup");

    for(p = uargv;*p!='\0';p++)
        *p = toupper(*p);

    return uargv;
}

int main(int argc,char *argv[])
{
    int s,tnum,opt,num_threads;
    struct thread_info *tinfo;
    pthread_attr_t attr;
    int stack_size;
    void *res;

    stack_size = -1;
    while((opt = getopt(argc,argv,"s:")) != -1)
    {
        switch(opt)
        {
            case 's':
                stack_size = strtoul(optarg,NULL,0);
                break;
            default:
                fprintf(stderr,"Usage:%s[-s stack-size] arg...\n",argv[0]);
                exit(EXIT_FAILURE);
        }
    }

    num_threads = argc - optind;//optind哪來的?好像是庫裏面的,沒用extern

    s = pthread_attr_init(&attr);
    if(s != 0)
        handle_error_en(s,"pthread_attr_init");
    if(stack_size > 0)
    {
        s = pthread_attr_setstacksize(&attr,stack_size);
        
        if(s != 0)
            handle_error_en(s,"pthread_attr_aetstacksize");
    }
    tinfo = calloc(num_threads,sizeof(struct thread_info));
    if(tinfo == NULL)
        handle_error("calloc");
    for(tnum = 0;tnum < num_threads;tnum++)
    {
        tinfo[tnum].thread_num = tnum +1;
        tinfo[tnum].argv_string = argv[optind + tnum];
        s = pthread_create(&tinfo[tnum].thread_id,&attr,&thread_start,&tinfo[tnum]);
        if(s!=0)
            handle_error_en(s,"pthread_create");
    }
    s = pthread_attr_destroy(&attr);
    if(s!=0)
        handle_error_en(s,"pthread_attr_destory");
    
    for(tnum = 0;tnum < num_threads;tnum++)
    {
        s = pthread_join(tinfo[tnum].thread_id,&res);
        if(s!=0)
            handle_error_en(s,"pthread_join");
        printf("Joined with thread %d;returned value was %s\n",tinfo[tnum].thread_num,(char*)res);
        free(res);//free函數
    }
    free(tinfo);
    exit(EXIT_SUCCESS);
}

編譯:

gcc thread_attr.c -lpthread

執行:

./a.out -s 0x100000 hola salut servus

-s參數須要結合getopt函數進行理解

結果:

Thread 1:top of stack near 0x7fcad561fed0;argv_thing =hola
Thread 3:top of stack near 0x7fcad4cbfed0;argv_thing =servus
Thread 2:top of stack near 0x7fcad4dcfed0;argv_thing =salut
Joined with thread 1;returned value was HOLA
Joined with thread 2;returned value was SALUT
Joined with thread 3;returned value was SERVUS

Tips

getopt函數:

使用man 3 getopt能夠獲取到關於getopt函數的相關信息

用來解析命令的參數,在unidtd.h文件裏面被包含,

須要結合optarg、optind等變量使用。

宏定義使用do while(0),能夠保證被替換後實現想要的功能

#define handle_error(msg)\
   do{perror(msg);exit(EXIT_FAILURE);}while(0)

4 線程安全

待總結

5 互斥量

待總結

6 條件變量

待總結

相關文章
相關標籤/搜索