1. 程式人生 > >執行緒概述、執行緒控制和執行緒私有資料

執行緒概述、執行緒控制和執行緒私有資料

一、執行緒概述

  在許多經典的作業系統教科書中,總是把程序定義為程式的執行例項,它並不執行什麼, 只是維護應用程式所需的各種資源,而執行緒則是真正的執行實體。在一個程序中的多個執行路線叫做執行緒。為了讓程序完成一定的工作,程序必須至少包含一個執行緒。   執行緒又叫輕量級程序(LWP)。 在這裡插入圖片描述

執行緒會共享程序的一些資源,也有一些資源是執行緒獨立擁有的。但是不同程序的執行緒是不共享資源的

【共享】:   程式碼區、資料區、堆區(注意沒有棧區)、環境變數和命令列引數、檔案描述符、訊號處理函式、當前目錄、使用者 ID 和組 ID 等。

【非共享】:    ID、暫存器值、棧記憶體、排程策略和優先順序、訊號掩碼、errno變數以及執行緒私有資料等。

  也可以說執行緒是包含在程序中的一種實體。它有自己的執行線索,可完成特定任務。可與其他執行緒共享程序中的共享變數及部分環境。可通過相互之間協同來完成程序所要完成的任務

二、執行緒控制

1、執行緒標識

  就像每個程序都有一個程序號一樣,每個執行緒也有一個執行緒號。程序號在整個系統中是唯一的,但執行緒號不同,執行緒號只在它所屬的程序環境中有效。程序號用 pid_t 資料型別表示,是一個非負整數。執行緒號則用 pthread_t 資料型別來表示,Linux 使用無符號長整數表示。有的系統在實現 pthread_t 的時候,用一個結構體來表示,所以在可移植的作業系統實現不能把它做為整數處理。

(1)獲取執行緒號

#include <pthread.h>
pthread_t pthread_self(void);
  • 功能: 獲取執行緒號。
  • 引數: 無。
  • 返回值: 呼叫執行緒的執行緒 ID 。

(2)執行緒號的比較

#include <pthread.h>
int pthread_equal(pthread_t t1, pthread_t t2);
  • 功能: 判斷執行緒號 t1 和 t2 是否相等。為了方便移植,儘量使用函式來比較執行緒 ID。
  • 引數: t1,t2:待判斷的執行緒號。
  • 返回值: 相等:非 0;不相等:0。
#include <stdio.h>
#include <stdlib.h> #include <pthread.h> int main() { pthread_t thread_id; thread_id = pthread_self(); // 獲取執行緒號 printf("thread id = %lu\n", thread_id); if (pthread_equal(thread_id, pthread_self())) // 執行緒號比較 printf("Equal\n"); else printf("not Equal\n"); return 0; } // 執行緒函式的程式在 pthread 庫中,故連結時要加上引數 -lpthread。

2、執行緒建立

#include <pthread.h>
int pthread_create( pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine)(void *), void *arg );
  • 功能: 建立一個執行緒。
  • 引數:
    • thread:執行緒識別符號地址;
    • attr:執行緒屬性結構體地址,通常設定為 NULL;
    • start_routine:執行緒函式的入口地址;
    • arg:傳給執行緒函式的引數。
  • 返回值: 成功:0;失敗:非0。
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>

void *fun(void *arg)
{
    int *p = (int *)arg;
    printf("child thread pid = %d\n", getpid());
    printf("child thread id = %lu\n", pthread_self());
    printf("child thread *arg = %d\n", *p);
    sleep(1);
}

int main()
{
    pthread_t tid;
    int n = 10;
    // 建立一個執行緒,fun是執行緒函式,n是傳給執行緒函式的引數,
    // 強制轉換為void *型別。
    int err = pthread_create(&tid, NULL, fun, (void *)&n);
    if (err != 0)
    {
    	// 如果建立失敗,打印出錯資訊。
        fprintf(stderr, "can't create thread:%s\n", strerror(err)); 
        exit(1);
    }
    printf("main pid = %d\n", getpid());
    printf("main thread id = %lu\n", pthread_self());
    printf("main child thread id = %lu\n", tid);
    // 執行緒建立時並不能保證哪個執行緒先執行:是新建的執行緒還是呼叫執行緒。
    sleep(2); 
    return 0;
}

3、執行緒終止

(1)執行緒退出

#include <pthread.h>
void pthread_exit(void *retval);
  • 功能: 主要用於終止正在執行的執行緒,但並不釋放資源。
  • 引數:
    • retval:來帶出執行緒的退出狀態資訊。
  • 返回值: 無。

  線上程過程函式或者被執行緒過程函式直接或間接呼叫的函式中,呼叫 pthread_exit 函式,其效果都與線上程過程函式中執行 return 語句效果一樣 – 終止呼叫執行緒。注意,在任何執行緒中呼叫 exit 函式,被終止的都是程序。當然隨著程序的終止,隸屬於該程序的包括呼叫執行緒在內的所有執行緒也都一併終止。

【Note】:   如果thread執行緒函式從return返回,則retval存放的是thread執行緒函式的返回值;如果從pthread_exit返回,則retval存放的是pthread_exit的引數。

(2)執行緒取消

#include <pthread.h>
int pthread_cancel(pthread_t thread);
  • 功能: 對引數指定的執行緒傳送取消的請求(必須是同一程序中的其他執行緒)。
  • 引數:
    • thread:執行緒號。
  • 返回值: 成功:0;失敗:錯誤碼。

  該函式只是向執行緒發出取消請求,並不等於執行緒終止。預設情況下,執行緒在收到取消請求以後,並不會立即終止,而是仍繼續執行,直到其達到某個取消點。在取消點處,執行緒檢查其自身是否已被取消,若是則立即終止。當執行緒呼叫一些特定函式時,取消點會出現。

(3)執行緒回收(阻塞)

#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
  • 功能: 等待執行緒結束(此函式會阻塞),並回收執行緒資源,類似程序的 wait() 函式。如果執行緒已經結束,那麼該函式會立即返回。
  • 引數:
    • thread:被等待的執行緒號;
    • retval:用來儲存執行緒退出狀態的指標的地址。
  • 返回值: 成功:0;失敗:非0。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>

void *thrfun1(void *p)
{
    int sum = 0;
    for (int i = 1; i <= 10; ++i)
        sum += i;
    printf("子執行緒1退出\n");
    return (void *)sum; // 為了保持型別匹配,強制轉換。
}

void *thrfun2(void *p)
{
    printf("子執行緒2退出\n");
    pthread_exit((void *)2); // 本執行緒退出,不影響其他執行緒。
}

void *thrfun3(void *p)
{
    while (1)
    {
        printf("子執行緒3執行\n");
        sleep(1);
    }
}

int main()
{
    pthread_t tid;
    void *it;
    pthread_create(&tid, NULL, thrfun1, NULL);
    pthread_join(tid, &it);
    printf("子執行緒1返回的資料是:%ld\n", (long)it);
    
    pthread_create(&tid, NULL, thrfun2, NULL);
    pthread_join(tid, &it);
    printf("子執行緒2返回的資料是:%ld\n", (long)it);
    
    pthread_create(&tid, NULL, thrfun3, NULL);
    sleep(3); // 主控執行緒執行三秒後取消子執行緒3
    pthread_cancel(tid); // 執行緒被取消
    // 回收由pthread_cancel終止的程序,返回值是一個巨集(-1)
    pthread_join(tid, &it); 
    printf("子執行緒3返回的資料是:%ld\n", (long)it);
    
    return 0;
}

(4)執行緒回收(非阻塞)

#include <pthread.h>
int pthread_detach(pthread_t thread);
  • 功能: 主要用於將引數指定的執行緒標記為分離狀態。
  • 引數:
    • thread:執行緒號;
  • 返回值: 成功:0;失敗:錯誤碼。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>

void *thrfun(void *p)
{
    int n = 3;
    while (n--)
    {
        printf("thread count %d\n", n);
        sleep(1);
    }
    return (void *)1;
}

int main()
{
    pthread_t tid;
    void *it;
    int err;
    pthread_create(&tid, NULL, thrfun, NULL);
    // 如果沒有pthread_detach,主控執行緒會阻塞回收子執行緒,
    // 並且列印返回狀態和出錯碼。
    // 如果設定了pthread_detach,主控執行緒非阻塞回收子執行緒,
    // 這種情況下再呼叫pthread_join會出錯。
    pthread_detach(tid);
    while (1)
    {
        err = pthread_join(tid, &it);
        if (err != 0)
            fprintf(stderr, "thread %s\n", strerror(err));
        else
            fprintf(stderr, "thread exit code %ld\n", (long)it);
        sleep(1);
    }
    return 0;
}

  對於分離狀態的執行緒來說:當該執行緒終止後,會自動將資源釋放給系統,不需要其他執行緒的加入/等待(即:立即回收資源),也就是說分離的執行緒無法被其他執行緒使用 pthread_join 進行等待。建議:對於新啟動的執行緒來說,要麼使用 pthread_detach 設定為分離狀態,要麼使用 pthread_join 設定為可加狀態(即:pthread_join 和 pthread_detach 是互斥的)。

三、執行緒私有資料

  有時應用程式設計中必要提供執行緒私有的全域性變數,這個變數僅線上程中有效,但卻可以跨過多個函式訪問。比如在程式裡可能需要每個執行緒維護一個連結串列,而會使用相同的函式來操作這個連結串列,最簡單的方法就是使用同名而不同變數地址的執行緒相關資料結構。這樣的資料結構可以由 Posix 執行緒庫維護,成為執行緒私有資料 (Thread-specific Data,或稱為TSD)。

1、建立執行緒私有資料

#include <pthread.h> 
int pthread_key_create(pthread_key_t *key, void (*destructor)(void*));
  • 功能: 建立一個型別為 pthread_key_t 型別的私有資料變數( key )。
  • 引數:
    • key:在分配( malloc )執行緒私有資料之前,需要建立和執行緒私有資料相關聯的鍵( key ),這個鍵的功能是獲得對執行緒私有資料的訪問權;
    • destructor:清理函式名字( 如:fun )。當執行緒退出時,如果執行緒私有資料地址不是非 NULL,此函式會自動被呼叫。該函式指標可以設成 NULL ,這樣系統將呼叫預設的清理函式。
  • 返回值: 成功:0;失敗:非0。

  不論哪個執行緒呼叫 pthread_key_create(),所建立的 key 都是所有執行緒可訪問,但各個執行緒可根據自己的需要往 key 中填入不同的值,相當於提供了一個同名不同值的變數。

2、登出執行緒私有資料

#include <pthread.h> 
int pthread_key_delete(pthread_key_t key);
  • 功能: 登出執行緒私有資料。這個函式並不會檢查當前是否有執行緒正使用執行緒私有資料( key ),也不會呼叫清理函式 destructor() ,而只是將執行緒私有資料( key )釋放以供下一次呼叫 pthread_key_create() 使用。
  • 引數:
    • key:待登出的私有資料。
  • 返回值: 成功:0;失敗:非0。

3、設定執行緒私有資料的關聯

#include <pthread.h> 
int pthread_setspecific(pthread_key_t key, const void *value);
  • 功能: 設定執行緒私有資料( key ) 和 value 關聯,注意,是 value 的值(不是所指的內容)和 key 相關聯。
  • 引數:
    • key:執行緒私有資料;
    • value:和 key 相關聯的指標。
  • 返回值: 成功:0;失敗:非0。

4、讀取執行緒私有資料所關聯的值

#include <pthread.h> 
void *pthread_getspecific(pthread_key_t key);
  • 功能: 讀取執行緒私有資料( key )所關聯的值。
  • 引數:
    • key:執行緒私有資料;
  • 返回值: 成功:執行緒私有資料( key )所關聯的值;失敗:NULL。
#include <stdio.h> 
#include <pthread.h> 
#include <unistd.h>

pthread_key_t key;  // 私有資料,全域性變數
 
void echomsg(void *t) 
{ 
    printf("[destructor] thread_id = %lu, param = %p\n", pthread_self(), t); 
} 
 
void *child1(void *arg) 
{ 
    int i = 10;
    
    pthread_t tid = pthread_self(); //執行緒號
    printf("\nset key value %d in thread %lu\n", i, tid); 
    
    pthread_setspecific(key, &i); // 設定私有資料
    
    printf("thread one sleep 2 until thread two finish\n\n");
    sleep(2); 
    printf("\nthread %lu returns %d, add is %p\n", tid, *((int *)pthread_getspecific(key)), pthread_getspecific(key)); 
} 
 
void *child2(void *arg) 
{ 
    int temp = 20;
    
    pthread_t tid = pthread_self();  //執行緒號
    printf("\nset key value %d in thread %lu\n", temp, tid); 
    
    pthread_setspecific(key, &temp); //設定私有資料
    
    sleep(1); 
    printf("thread %lu returns %d, add is %p\n", tid, *((int *)pthread_getspecific(key)), pthread_getspecific(key)); 
} 
 
int main(void) 
{ 
    pthread_t tid1,tid2; 
    pthread_key_create(&key, echomsg); // 建立
    
    pthread_create(&tid1, NULL, child1, NULL); 
    pthread_create(&tid2, NULL, child2, NULL); 
    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);
    
    pthread_key_delete(key); // 登出
    
    return 0; 
}