1. 程式人生 > >跟著iMX28x開發套件學linux-10

跟著iMX28x開發套件學linux-10

十、linux應用程式設計之八:執行緒

執行緒是包含在程序內部的順序執行流,是程序中的實際運作單位,也是作業系統能夠進行排程的最小單位。一個程序中可以併發多條執行緒,每條執行緒並行執行不同的任務。

簡單來說,程序是由執行緒組成的,執行緒是系統排程的最小單位,程序是擁有資源的基本單位而執行緒共享程序的資源。

執行緒的內容有點複雜,分執行緒建立與終止,連線與分離,執行緒屬性,互斥量,條件變數五部分做筆記。

 1.執行緒建立與終止

1) 函式原型:int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg);

 輸入引數:thread,執行緒ID,實際上是個結構體。attr,屬性物件,暫時先置為NULLstart_routine, 函式指標,指向執行緒執行函式。arg,執行緒執行函式的輸入引數。

 返 值:成功返回0,失敗返回一個非零錯誤碼。

 示    例:rc = pthread_create(&threadID, NULL, PrintHello, (void *)t);

 

2) 函式原型:void pthread_exit(void *retval);

 輸入引數:retval,執行緒終止時向主執行緒返回的引數。

 返

值:無

 示    例:pthread_exit(0);

 

3) 虛擬碼:void* 執行函式{... pthread_exit(NULL);}

main{

  pthread_create(&執行緒ID, NULL, 執行函式, (void*)引數);

  pthread_exit(NULL); //一定不要忘了這裡要退出主執行緒

}

4) 程式碼示例:

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

#define
NUM_THREAD 5 void* PrintHello(void* threadid){ long id; id = (long)threadid; printf("Thread#%ld : Hello World! It's me, thread #%ld\n", id, id); pthread_exit(NULL); } int main(){ pthread_t threads[NUM_THREAD]; int rc; long i; for(i=0; i<NUM_THREAD; i++){ printf("main : creating thread %ld\n", i); rc = pthread_create(&threads[i], NULL, PrintHello, (void*)i); if(rc){ fprintf(stderr,"create thread #%ld error\n",i); exit(-1); } } printf("main : main exit!\n"); pthread_exit(NULL); return 0; }

 

執行結果:

 

 

 2.連線與分離

執行緒分為分離執行緒和非分離執行緒,分離執行緒在退出時會立即釋放系統資源並返回,而非分離執行緒在退出時不會立即釋放資源,需要另一個執行緒為它呼叫pthread_join()函式。這樣的意義是:當執行緒A退出時,為其呼叫join函式的執行緒B可以獲得執行緒A的返回值。但是這樣會導致一個問題,如果沒有一個執行緒A為執行緒B呼叫join函式,那執行緒B就一直無法退出,也就無法釋放資源,只能等到程序終止時,執行緒B才能退出。對於長時間運作的程式來說,完成工作的執行緒長期不退出會導致佔用資源過多的現象。因此,對於長時間執行的程式,最好將執行緒設定為分離執行緒或者為執行緒呼叫join函式,當然,設定為分離執行緒的執行緒將無法返回值。

1) 函式原型:int pthread_detach(pthread_t thread);

 輸入引數:thread,執行緒ID,由變數定義以及create函式得到。

 返 值:int,成功返回0,失敗返回非零錯誤碼。

 示    例:pthread_detach(pthread_self());

 

2) 函式原型:int pthread_join(pthread_t thread, void **retval);

 輸入引數:thread,執行緒IDretval,返回值存放地址。

 返 值:int,成功返回0,失敗返回非零錯誤碼。

 示    例:void *statusrc = pthread_join(threadID, &status);

 說    明:viod **retval作為要被終止執行緒返回值的存放地址,這裡用了&(void*)status,但是實 際上要檢查或者引用這個返回值時,可以直接用(long)status來引用,保留疑問,待解 決。因為執行函式的返回值類    型為void*,也就是說void*型別才是有效資料,又因為 C語言中沒有引用型別,因而要通過引數獲得函式返回值只能通過指標,所以出現了 void** retval。

 

3) 虛擬碼:void* 執行函式(void *引數){...pthread_exit(要返回的引數);}

main(){

void* status;

rc = pthread_create(&執行緒ID, NULL, 執行函式, (void*)要輸入的引數);

rc = pthread_join(執行緒ID, &status); //status時,用(long)status即可。

}

4) 程式碼示例:

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

void* PrintHello(void* threadid){
    long id = (long)threadid;
    printf("thread #%ld : it is #%ld ,hello!!!\n", id, id);
    pthread_exit((void*)5);
}

int main(){
    pthread_t threadID;
    int rc;
    void* status;
    
    rc = pthread_create(&threadID, NULL, PrintHello, (void*)1);
    if(rc){
        perror("create thread error\n");
        exit(-1);
    }
    
    rc = pthread_join(threadID, &status);
    if(rc){
        perror("join thread error\n");
        exit(-1);
    }
    printf("main thread : return value = %ld\n", (long)status);
    pthread_exit(NULL);
    return 0;    
}

 

執行結果:

 

 

 3.執行緒屬性

之前使用create()函式時,第二個引數屬性物件att總是使用NULL,其實是有實際作用的。執行緒是有屬性的,上文說到的分離執行緒和非分離執行緒就是執行緒的屬性之一,稱為執行緒狀態。執行緒的基本屬性包括:執行緒狀態,調整策略和棧大小。

1) 屬性物件arr

初始化屬性物件:int pthread_attr_init(pthread_attr_t *attr);

        arrt是屬性物件,成功返回0,失敗返回非零錯誤碼。

銷燬屬性物件:int pthread_attr_destroy(pthread_attr_t *attr);

        arrt是屬性物件,成功返回0,失敗返回非零錯誤碼。

2) 執行緒狀態

獲取執行緒狀態:int pthread_attr_getdetachstate(pthread_attr_t *attr, int *detachstate);

        arrt為屬性物件,detachstate是所獲取狀態值的指標。

設定執行緒狀態:int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);

        arrt為屬性物件,detachstate是要設定的狀態值。

3) 執行緒棧

獲取執行緒棧:int pthread_attr_getstacksize(pthread_attr_t *attr, size_t *stacksize);

        arrt為屬性物件,stacksize 是儲存所獲取棧大小的指標。

設定執行緒棧:int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize);

        arrt為屬性物件,stacksize 是要設定的棧大小。

4) 屬性物件使用虛擬碼:

int main(){

pthread_attr_t attr;

pthread_attr_init(&attr);

pthread_attr_getstacksize(&attr, stacksize);

pthread_create(&thread, &attr, 執行函式, (void *)arg);

pthread_attr_destroy(&attr);

}

5) 程式碼例項:程式碼中涉及到getopt(),calloc(),strdup()等函式需要注意。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <string.h>
#include <ctype.h>

//extern char* optarg;
//extern int optind;
//extern int opterr;
//extern int optopt;
struct thread_info{
    pthread_t pthreadID;
    int number_id;
    char* argv_string;
};

void* fun(void* var){
    char* p;
    struct thread_info* thread_point = var;
    static char* uargv;
    printf("Thread #%d : top of stack near %p; argv_string = %s\n", 
           thread_point->number_id, &p, thread_point->argv_string);
    uargv = strdup(thread_point->argv_string);
    p = uargv;
    while(*p != '\0'){    
        *p = toupper(*p);
        p++;
    }
    return(uargv);
}

int main(int argc, char* argv[]){
    
    int ch, thread_num;
    int stack_size = -1;
    int ret;
    pthread_attr_t attr;
    void* status;
    struct thread_info* threads = NULL;
    
    while((ch=getopt(argc, argv, "s:")) != -1){
        switch(ch){
            case 's':
                printf("HAVE option : -s\n");
                stack_size = strtoul(optarg, NULL, 0);
                printf("thread's stack size = %d\n", stack_size);
                break;
            default:
                fprintf(stderr, "Usage:%s [-s stack-size] arg...\n", argv[0]);
                exit(-1);
                break;
        }
    }
    thread_num = argc - optind;
    printf("thread_num = %d\n", thread_num);
    
    
    ret = pthread_attr_init(&attr);
    if(ret){
        perror("init attr error\n");
        exit(-1);
    }
    
    
    if(stack_size > 0){
        ret = pthread_attr_setstacksize(&attr, stack_size);
        if(ret){
            perror("set stack size error\n");
            exit(-1);
        }
    }
    else{
        perror("stack size < 0\n");
        exit(-1);
    }
    
    
    threads = calloc(thread_num, sizeof(struct thread_info));
    if(threads == NULL){
        perror("calloc error\n");
        exit(-1);
    }
    
    for(int i=0; i<thread_num; i++){
        threads[i].number_id = i;
        threads[i].argv_string = argv[optind + i];
        ret = pthread_create(&threads[i].pthreadID, &attr, fun, (void*)&threads[i]);
        if(ret){
            perror("create thread error\n");
            exit(-1);
        }
    }
    pthread_attr_destroy(&attr);
    
    for(int i=0; i<thread_num; i++){
        ret = pthread_join(threads[i].pthreadID, &status);
        if(ret){
            perror("join thread error\n");
            exit(-1);
        }
        printf("Join with thread #%d; return value was %s \n", 
               threads[i].number_id, (char*)status);
    }
    
    pthread_exit(NULL);
    free(threads);
    return 0;
}

程式碼稍微有點長,實際上是有一條脈絡下來的。首先程式碼的目的是:在執行程式時用-s選項輸入執行緒堆疊的大小,接著輸入其他字串引數。然後輸出結果是字串引數的大寫形式。要實現這個目標首先一點就是要建立執行緒create(),建立執行緒需要執行緒ID,執行緒屬性物件,執行緒執行函式,執行函式輸入引數。執行緒ID通過變數宣告得到,執行緒屬性物件也是變數宣告+初始化得到,但是要修改棧大小。棧大小從-s選項來,因而需要getopt()函式解析選項。得到棧大小之後需要呼叫pthread_attr_setstacksize()函式設定棧大小。屬性物件得到之後,接下來就是編寫執行函式,獲取執行函式的輸入引數。這樣整個程式碼就寫完了。

需要注意的是,在呼叫pthread_attr_setstacksize()函式設定棧大小之前,必須先初始化屬性物件。-s輸入棧大小時,棧大小不能小於16K

執行結果:

 

 

 4.互斥量

程式中有一些程式碼一次只能由一個執行緒訪問,因此要對這些程式碼進行保護,這些程式碼叫臨界區程式碼,臨界區程式碼必須以互斥的方式訪問。互斥量(也叫互斥鎖),是一種用來保護臨界區的特殊變數,有鎖定和解鎖兩種狀態,當互斥量處於鎖定狀態時,說明某個執行緒正在持有這個互斥量,其他執行緒想要對這個互斥量加鎖的時候,將會阻塞,直到持有互斥量的執行緒解鎖這個互斥量。互斥量常用來做臨界區保護和程式碼同步。

1) 互斥量建立與銷燬

建立方式①:pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;(常用)

建立方式②:pthread_mutex_t mutex

      int pthread_mutex_init(pthread_mutex_t *restrict mutex, constpthread_mutexattr_t *restrict attr);

銷燬互斥量:int pthread_mutex_destroy(pthread_mutex_t *mutex);

 

2) 互斥量加鎖和解鎖

加鎖:int pthread_mutex_lock(pthread_mutex_t *mutex);

   int pthread_mutex_trylock(pthread_mutex_t *mutex);(嘗試加鎖,無法加鎖也不會阻塞)

解鎖:int pthread_mutex_unlock(pthread_mutex_t *mutex);

 

3) 互斥量使用虛擬碼:

pthread_mutex_t mylock = PTHREAD_MUTEX_INITIALIZER;

pthread_mutex_lock(&mylock);

  臨界區程式碼

pthread_mutex_unlock(&mylock);

 

4) 程式碼示例:

 1 #include <stdio.h>
 2 #include <stdlib.h>
 3 #include <unistd.h>
 4 #include <pthread.h>
 5 
 6 pthread_mutex_t mylock = PTHREAD_MUTEX_INITIALIZER;
 7 
 8 void* doPrint(void* var){
 9     
10     long id = (long)var;
11     
12     pthread_mutex_lock(&mylock);
13     for(int i=0; i<5; i++){
14         printf("thread #%ld : it's me #%ld!!\n", id, id);
15         usleep(100);
16     }
17     pthread_mutex_unlock(&mylock);
18     pthread_exit(NULL);
19 }
20 
21 int main(){
22     
23     pthread_t pthreadID[3];
24     int ret;
25     for(long i=0; i<3; i++){
26         ret = pthread_create(&pthreadID[i], NULL, doPrint, (void*)i);
27         if(ret){
28             perror("create thread error\n");
29             exit(-1);
30         }
31     }
32     pthread_exit(NULL);
33     return 0;
34 }

 

 

使用了互斥鎖後,每個執行緒都執行結束之後再執行下一個執行緒,輸出是整齊的,如下圖:

 

假設將程式碼的第12行和第17行註釋掉,輸出變得參差不齊了,如下:

 

5) 死鎖現象以及避免死鎖

死鎖現象指的是:兩個或兩個以上的執行緒在執行過程中,因爭奪資源而造成互相等待的情況。例如,執行緒A已經對互斥鎖a加鎖,而且想要對互斥鎖b加鎖。執行緒B已經對互斥鎖b加鎖,而且想要對互斥鎖a加鎖。那就會導致,執行緒A阻塞等待執行緒B解鎖互斥鎖b,執行緒B阻塞等待執行緒A解鎖互斥鎖a,但是兩個執行緒都已經阻塞,無法解鎖任何一個互斥鎖,所以執行緒A和執行緒B就死鎖了。

避免死鎖:加鎖應按照一定的順序進行加鎖。比如上面的例子中,加鎖順序為互斥鎖a->互斥鎖b。執行緒A已經對互斥鎖a加鎖,想要對互斥鎖b加鎖,這符合加鎖順序。而執行緒B此時不應該處於已經對互斥鎖b加鎖,而想要對互斥鎖a加鎖的狀態,這是不符合加鎖順序的。正確的加鎖順序應該是先對互斥鎖a加鎖,再對互斥鎖b加鎖。

5.條件變數

條件變數也是程式同步的一種。與互斥鎖不同,條件變數是通過 等待-通知 的方式進行同步的,同步方式比較高效。實際上使用條件變數要有互斥鎖的前提。

1) 建立和銷燬

靜態初始化:pthread_cond_t cond = PTHREAD_COND_INITIALIZER;(常用)

動態初始化:pthread_cond_t cond;

      int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);

銷燬:int pthread_cond_destroy(pthread_cond_t *cond);

 

2) 等待與通知

等待:int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);

   int pthread_cond_timedwait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);(阻塞一段時間後繼續執行)

通知:int pthread_cond_signal(pthread_cond_t *cond);(喚醒一個等待執行緒)

    int pthread_cond_broadcast(pthread_cond_t *cond);(喚醒所有等待執行緒)

 

3) 條件變數使用虛擬碼:

執行緒A(等待)

執行緒B(通知)

pthread_mutex_lock(&mutex)

while(a < b)

pthread_cond_wait(&cond, &mutex)

pthread_mutex_unlock(&mutex)

if(a<b)

pthread_cond_signal(&cond)

 

4) 程式碼示例:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>

pthread_mutex_t mylock = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t mycond = PTHREAD_COND_INITIALIZER;
long sum = 0;

void* fun12(void* var){
    long id = (long)var;
    
    for(int i=0; i<60; i++){
        usleep(1);
        pthread_mutex_lock(&mylock);
        sum++;
        printf("thread #%ld : sum + 1, \t sum = %ld\n", id, sum);
        pthread_mutex_unlock(&mylock);
        if(sum>100){
            pthread_cond_signal(&mycond);
        }
    }
    pthread_exit(NULL);
}

void* fun3(void* var){
    long id = (long)var;
    
    pthread_mutex_lock(&mylock);
    while(sum < 100)
        pthread_cond_wait(&mycond, &mylock);
    sum = 0;
    printf("thread #%ld : sum alread > 100, \t clear sum\n", id);
    pthread_mutex_unlock(&mylock);
    pthread_exit(NULL);
}

int main(){

    pthread_t pthreadID[3];
    int ret;
    for(long i=0;i<3;i++){
        if(i<2)
            ret = pthread_create(&pthreadID[i], NULL, fun12, (void*)i);
        else
            ret = pthread_create(&pthreadID[i], NULL, fun3, (void*)i);
        
        if(ret){
            perror("create thread error!!\n");
            exit(-1);
        }
    }

    pthread_exit(NULL);
    return 0;
}

執行緒0和執行緒1sum進行+1,並且檢測sum是否超過100,如果超過100即通知執行緒3繼續執行。執行結果如下:

 

這樣做的好處是,執行緒3只需要執行1次,而不用反覆執行執行緒3檢測sum是否超過100