多線程編程(Linux C)

多線程編程能夠說每一個程序員的基本功,同時也是開發中的難點之一,本文以Linux C爲例,講述了線程的建立及經常使用的幾種線程同步的方式,最後對多線程編程進行了總結與思考並給出代碼示例。html

1、建立線程

多線程編程的第一步,建立線程。建立線程實際上是增長了一個控制流程,使得同一進程中存在多個控制流程併發或者並行執行。程序員

線程建立函數,其餘函數這裏再也不列出,能夠參考pthread.hshell

#include<pthread.h>

int pthread_create(
    pthread_t *restrict thread,  /*線程id*/
	const pthread_attr_t *restrict attr,    /*線程屬性,默承認置爲NULL,表示線程屬性取缺省值*/
	void *(*start_routine)(void*),  /*線程入口函數*/ 
	void *restrict arg  /*線程入口函數的參數*/
	);
複製代碼

代碼示例:編程

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

char* thread_func1(void* arg) {
    pid_t pid = getpid();
    pthread_t tid = pthread_self();
    printf("%s pid: %u, tid: %u (0x%x)\n", (char*)arg, (unsigned int)pid, (unsigned int)tid, (unsigned int)tid);

    char* msg = "thread_func1";
    return msg;
}

void* thread_func2(void* arg) {
    pid_t pid = getpid();
    pthread_t tid = pthread_self();
    printf("%s pid: %u, tid: %u (0x%x)\n", (char*)arg, (unsigned int)pid, (unsigned int)tid, (unsigned int)tid);
    char* msg = "thread_func2 ";
    while(1) {
        printf("%s running\n", msg);
        sleep(1);
    }
    return NULL;
}

int main() {
    pthread_t tid1, tid2;
    if (pthread_create(&tid1, NULL, (void*)thread_func1, "new thread:") != 0) {
        printf("pthread_create error.");
        exit(EXIT_FAILURE);
    }

    if (pthread_create(&tid2, NULL, (void*)thread_func2, "new thread:") != 0) {
        printf("pthread_create error.");
        exit(EXIT_FAILURE);
    }
    pthread_detach(tid2);

    char* rev = NULL;
    pthread_join(tid1, (void *)&rev);
    printf("%s return.\n", rev);
    pthread_cancel(tid2);

    printf("main thread end.\n");
    return 0;
}
複製代碼

2、線程同步

有時候咱們須要多個線程相互協做來執行,這時須要線程間同步。線程間同步的經常使用方法有:微信

  • 互斥
  • 信號量
  • 條件變量

咱們先看一個未進行線程同步的示例:多線程

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

#define LEN 100000
int num = 0;

void* thread_func(void* arg) {
    for (int i = 0; i< LEN; ++i) {
        num += 1;
    }
    
    return NULL;
}

int main() {
    pthread_t tid1, tid2;
    pthread_create(&tid1, NULL, (void*)thread_func, NULL);
    pthread_create(&tid2, NULL, (void*)thread_func, NULL);

    char* rev = NULL;
    pthread_join(tid1, (void *)&rev);
    pthread_join(tid2, (void *)&rev);

    printf("correct result=%d, wrong result=%d.\n", 2*LEN, num);
    return 0;
}
複製代碼

運行結果:correct result=200000, wrong result=106860.併發

【1】互斥

這個是最容易理解的,在訪問臨界資源時,經過互斥,限制同一時刻最多隻能有一個線程能夠獲取臨界資源。函數

其實互斥的邏輯就是:若是訪問臨街資源發現沒有其餘線程上鎖,就上鎖,獲取臨界資源,期間若是其餘線程執行到互斥鎖發現已鎖住,則線程掛起等待解鎖,當前線程訪問完臨界資源後,解鎖並喚醒其餘被該互斥鎖掛起的線程,等待再次被調度執行。post

「掛起等待」和「喚醒等待線程」的操做如何實現?每一個Mutex有一個等待隊列,一個線程要在Mutex上掛起等待,首先在把本身加入等待隊列中,而後置線程狀態爲睡眠,而後調用調度器函數切換到別的線程。一個線程要喚醒等待隊列中的其它線程,只需從等待隊列中取出一項,把它的狀態從睡眠改成就緒,加入就緒隊列,那麼下次調度器函數執行時就有可能切換到被喚醒的線程。ui

主要函數以下:

#include <pthread.h>

int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);       /*初始化互斥量*/
int pthread_mutex_destroy(pthread_mutex_t *mutex);      /*銷燬互斥量*/
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
複製代碼

用互斥解決上面計算結果錯誤的問題,示例以下:

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

#define LEN 100000
int num = 0;

void* thread_func(void* arg) {
    pthread_mutex_t* p_mutex = (pthread_mutex_t*)arg;
    for (int i = 0; i< LEN; ++i) {
        pthread_mutex_lock(p_mutex);
        num += 1;
        pthread_mutex_unlock(p_mutex);
    }
    
    return NULL;
}

int main() {
    pthread_mutex_t m_mutex;
    pthread_mutex_init(&m_mutex, NULL);

    pthread_t tid1, tid2;
    pthread_create(&tid1, NULL, (void*)thread_func, (void*)&m_mutex);
    pthread_create(&tid2, NULL, (void*)thread_func, (void*)&m_mutex);

    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);

    pthread_mutex_destroy(&m_mutex);

    printf("correct result=%d, result=%d.\n", 2*LEN, num);
    return 0;
}
複製代碼

運行結果:correct result=200000, result=200000.

若是在互斥中還嵌套有其餘互斥代碼,須要注意死鎖問題。

產生死鎖的兩種狀況:

  • 一種狀況是:若是同一個線程前後兩次調用lock,在第二次調用時,因爲鎖已經被佔用,該線程會掛起等待別的線程釋放鎖,然而鎖正是被本身佔用着的,該線程又被掛起而沒有機會釋放鎖,所以就永遠處於掛起等待狀態了,產生死鎖。
  • 另外一種典型的死鎖情形是:線程A得到了鎖1,線程B得到了鎖2,這時線程A調用lock試圖得到鎖2,結果是須要掛起等待線程B釋放鎖2,而這時線程B也調用lock試圖得到鎖1,結果是須要掛起等待線程A釋放鎖1,因而線程A和B都永遠處於掛起狀態了。

如何避免死鎖:

  1. 不用互斥鎖(這個不少時候很難辦到)
  2. 寫程序時應該儘可能避免同時得到多個鎖。
  3. 若是必定有必要這麼作,則有一個原則:若是全部線程在須要多個鎖時都按相同的前後順序(常見的是按Mutex變量的地址順序)得到鎖,則不會出現死鎖。好比一個程序中用到鎖一、鎖二、鎖3,它們所對應的Mutex變量的地址是鎖1<鎖2<鎖3,那麼全部線程在須要同時得到2個或3個鎖時都應該按鎖一、鎖二、鎖3的順序得到。若是要爲全部的鎖肯定一個前後順序比較困難,則應該儘可能使用pthread_mutex_trylock調用代替pthread_mutex_lock調用,以免死鎖。
【2】條件變量

條件變量歸納起來就是:一個線程須要等某個條件成立(而這個條件是由其餘線程決定的)才能繼續往下執行,如今這個條件不成立,線程就阻塞等待,等到其餘線程在執行過程當中使這個條件成立了,就喚醒線程繼續執行。

相關函數以下:

#include <pthread.h>

int pthread_cond_destroy(pthread_cond_t *cond);
int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);
複製代碼

舉個最容易理解條件變量的例子,「生產者-消費者」模式中,生產者線程向隊列中發送數據,消費者線程從隊列中取數據,當消費者線程的處理速度大於生產者線程時,會產生隊列中沒有數據了,一種處理辦法是等待一段時間再次「輪詢」,但這種處理方式不太好,你不知道應該等多久,這時候條件變量能夠很好的解決這個問題。下面是代碼:

#include<sys/types.h>
#include<unistd.h>
#include<stdlib.h>
#include<stdio.h>
#include<pthread.h>
#include<errno.h>
#include<string.h>

#define LIMIT 1000

struct data {
    int n;
    struct data* next;
};

pthread_cond_t condv = PTHREAD_COND_INITIALIZER;
pthread_mutex_t mlock = PTHREAD_MUTEX_INITIALIZER; 
struct data* phead = NULL;

void producer(void* arg) {
    printf("producer thread running.\n");
    int count = 0;
    for (;;) {
        int n = rand() % 100;
        struct data* nd = (struct data*)malloc(sizeof(struct data));
        nd->n = n;

        pthread_mutex_lock(&mlock);
        struct data* tmp = phead;
        phead = nd;
        nd->next = tmp;
        pthread_mutex_unlock(&mlock);
        pthread_cond_signal(&condv);

        count += n;

        if(count > LIMIT) {
            break;
        }
        sleep(rand()%5);
    }
    printf("producer count=%d\n", count);
}

void consumer(void* arg) {
    printf("consumer thread running.\n");
    int count = 0;
    for(;;) {
        pthread_mutex_lock(&mlock);
        if (NULL == phead) {
            pthread_cond_wait(&condv, &mlock);
        } else {
            while(phead != NULL) {
                count += phead->n;
                struct data* tmp = phead;
                phead = phead->next;
                free(tmp);
            }
        }
        pthread_mutex_unlock(&mlock);
        if (count > LIMIT)
            break;
    }
    printf("consumer count=%d\n", count);
}

int main() {
    pthread_t tid1, tid2;
    pthread_create(&tid1, NULL, (void*)producer, NULL);
    pthread_create(&tid2, NULL, (void*)consumer, NULL);
    
    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);

    return 0;
}
複製代碼

條件變量中的執行邏輯:

關鍵是理解執行到int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex) 這裏時發生了什麼,其餘的都比較容易理解。執行這條函數前須要先獲取互斥鎖,判斷條件是否知足,若是知足執行條件,則繼續向下執行後釋放鎖;若是判斷不知足執行條件,則釋放鎖,線程阻塞在這裏,一直等到其餘線程通知執行條件知足,喚醒線程,再次加鎖,向下執行後釋放鎖。(簡而言之就是:釋放鎖-->阻塞等待-->喚醒後加鎖返回

實現細節可看源碼pthread_cond_wait.cpthread_cond_signal.c

上面的例子可能有些繁瑣,下面的這個代碼示例則更爲簡潔:

#include<sys/types.h>
#include<unistd.h>
#include<stdlib.h>
#include<stdio.h>
#include<pthread.h>
#include<errno.h>
#include<string.h>

#define NUM 3
pthread_cond_t condv = PTHREAD_COND_INITIALIZER;
pthread_mutex_t mlock = PTHREAD_MUTEX_INITIALIZER; 

void producer(void* arg) {
    int n = NUM;
    while(n--) {
        sleep(1);
        pthread_cond_signal(&condv);
        printf("producer thread send notify signal. %d\t", NUM-n);
    }
}

void consumer(void* arg) {
    int n = 0;
    while (1) {
        pthread_cond_wait(&condv, &mlock);
        printf("recv producer thread notify signal. %d\n", ++n);
        if (NUM == n) {
            break;
        }
    }
}

int main() {
    pthread_t tid1, tid2;
    pthread_create(&tid1, NULL, (void*)producer, NULL);
    pthread_create(&tid2, NULL, (void*)consumer, NULL);
    
    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);

    return 0;
}
複製代碼

運行結果:

producer thread send notify signal. 1   recv producer thread notify signal. 1
producer thread send notify signal. 2   recv producer thread notify signal. 2
producer thread send notify signal. 3   recv producer thread notify signal. 3
複製代碼

【3】信號量

信號量適用於控制一個僅支持有限個用戶的共享資源。用於保持在0至指定最大值之間的一個計數值。當線程完成一次對該semaphore對象的等待時,該計數值減一;當線程完成一次對semaphore對象的釋放時,計數值加一。當計數值爲0時,線程掛起等待,直到計數值超過0.

主要函數以下:

#include <semaphore.h>

int sem_init(sem_t *sem, int pshared, unsigned int value);
int sem_wait(sem_t *sem);
int sem_trywait(sem_t *sem);
int sem_post(sem_t * sem);
int sem_destroy(sem_t * sem);
複製代碼

代碼示例以下:

#include<sys/types.h>
#include<unistd.h>
#include<stdlib.h>
#include<stdio.h>
#include<pthread.h>
#include<errno.h>
#include<string.h>
#include<semaphore.h>

#define NUM 5

int queue[NUM];
sem_t psem, csem; 

void producer(void* arg) {
    int pos = 0;
    int num, count = 0;
    for (int i=0; i<12; ++i) {
        num = rand() % 100;
        count += num;
        sem_wait(&psem);
        queue[pos] = num;
        sem_post(&csem);
        printf("producer: %d\n", num); 
        pos = (pos+1) % NUM;
        sleep(rand()%2);
    }
    printf("producer count=%d\n", count);
}

void consumer(void* arg){
    int pos = 0;
    int num, count = 0;
    for (int i=0; i<12; ++i) {
        sem_wait(&csem);
        num = queue[pos];
        sem_post(&psem);
        printf("consumer: %d\n", num);
        count += num;
        pos = (pos+1) % NUM;
        sleep(rand()%3);
    }
    printf("consumer count=%d\n", count);    
} 

int main() {
    sem_init(&psem, 0, NUM);
    sem_init(&csem, 0, 0);

    pthread_t tid[2];
    pthread_create(&tid[0], NULL, (void*)producer, NULL);
    pthread_create(&tid[1], NULL, (void*)consumer, NULL);
    pthread_join(tid[0], NULL);
    pthread_join(tid[1], NULL);
    sem_destroy(&psem);
    sem_destroy(&csem);

    return 0;
}
複製代碼

信號量的執行邏輯:

當須要獲取共享資源時,先檢查信號量,若是值大於0,則值減1,訪問共享資源,訪問結束後,值加1,若是發現有被該信號量掛起的線程,則喚醒其中一個線程;若是檢查到信號量爲0,則掛起等待。

可參考源碼sem_post.c

3、多線程編程總結與思考

最後,咱們對多線程編程進行總結與思考。

  • 第一點就是在進行多線程編程時必定注意考慮同步的問題,由於多數狀況下咱們建立多線程的目的是讓他們協同工做,若是不進行同步,可能會出現問題。
  • 第二點,死鎖的問題。在多個線程訪問多個臨界資源時,處理不當會發生死鎖。若是遇到編譯經過,運行時卡住了,有多是發生死鎖了,能夠先思考一下是那些線程會訪問多個臨界資源,這樣查找問題會快一些。
  • 第三點,臨界資源的處理,多線程出現問題,很大緣由是多個線程訪問臨界資源時的問題,一種處理方式是將對臨界資源的訪問與處理所有放到一個線程中,用這個線程服務其餘線程的請求,這樣只有一個線程訪問臨界資源就會解決不少問題。
  • 第四點,線程池,在處理大量短任務時,咱們能夠先建立好一個線程池,線程池中的線程不斷從任務隊列中取任務執行,這樣就不用大量建立線程與銷燬線程,這裏再也不細述。

參考文檔:pthread.h - threads

歡迎關注我的微信公衆號,Let's go!

在這裏插入圖片描述
相關文章
相關標籤/搜索