爲何引入線程編程
爲了實現服務端併發處理客戶端請求,咱們介紹了多進程模型、select和epoll,這三種辦法各有優缺點。建立(複製)進程的工做自己會給操做系統帶來至關沉重的負擔。並且,每一個進程有獨立的內存空間,因此進程間通訊的實現難度也會隨之提升。且進程的切換一樣也是不菲的開銷。什麼是進程切換?咱們都知道計算機即使只有一個CPU也能夠同時運行多個進程,這是由於系統將CPU時間分紅多個微小的塊後分配給多個進程,比方進程B在進程A以後執行,當進程A所分配的CPU時間到點以後,要開始執行進程B,此時須要將進程A的數據移出內存保存到磁盤,並讀入進程B的數據,因此上下文切換須要比較長的時間,即便經過優化加快速度,也會存在侷限安全
爲了保持多進程的優勢,同時在必定程度上克服其缺點,人們引入了線程。這是爲了將進程的各類劣勢降至最低限度而設計的一種「輕量級進程」,線程相比進程有以下優勢:bash
線程和進程的差別併發
每一個進程的內存空間都由保存全局變量的「數據區」、向malloc等函數動態分配提供空間的堆(Heap)、函數運行時使用的棧(Stack)構成。每一個進程都擁有這種獨立的空間,多個進程結構如圖1-1所示函數
圖1-1 進程間獨立的內存優化
但若是以得到多個代碼執行流爲主要目的,則不該像圖1-1那樣徹底分離內存結構,而只需分離棧區域,經過這種方式能夠得到以下優點:spa
實際上這就是線程,線程爲了保持多條代碼執行流而隔開了棧區域,所以具備如圖1-2所示的內存結構操作系統
圖1-2 線程的內存結構線程
如圖1-2所示,多個線程將共享數據區和堆,爲了保持這種結構,線程將在進程內建立並運行。也就是說,進程和線程能夠定義爲以下形式:設計
若是說進程在操做系統內部生成多個執行流,那麼線程就在同一進程內部建立多條執行流。所以,操做系統、進程、線程之間的關係能夠經過圖1-3表示
圖1-3 操做系統、進程、線程之間的關係
線程的建立及運行
線程具備單獨的執行流,所以須要單獨定義線程的main函數,還須要請求操做系統在單獨的執行流中執行該函數,完成該功能的函數以下:
#include<pthread.h> int pthread_create(pthread_t * restrict thread, const pthread_attr_t * restrict attr, void* (* start_routine)(void *), void * restrict arg);//成功時返回0,失敗時返回其餘值
下面,咱們來看一個示例
thread1.c
#include <stdio.h> #include <pthread.h> void *thread_main(void *arg); int main(int argc, char *argv[]) { pthread_t t_id; int thread_param = 5; if (pthread_create(&t_id, NULL, thread_main, (void *)&thread_param) != 0) { puts("pthread_create() error"); return -1; }; sleep(10); puts("end of main"); return 0; } void *thread_main(void *arg) { int i; int cnt = *((int *)arg); for (i = 0; i < cnt; i++) { sleep(1); puts("running thread"); } return NULL; }
編譯thread1.c並運行
# gcc thread1.c -o thread1 -lpthread # ./thread1 running thread running thread running thread running thread running thread end of main
從上述運行結果能夠看到,線程相關代碼在編譯時需添加-lpthread選項聲明須要鏈接線程庫,只有這樣才能調用頭文件pthread.h中聲明的函數,上述程序的執行流程如圖1-4所示
圖1-4 示例thread1.c的執行流程
圖1-4中的虛線表明執行流程,向下的箭頭指的是執行流,橫向箭頭是函數調用。
接下來,能夠嘗試將上述示例的第15行sleep函數的調用語句改成sleep(2)。運行以後你們會發現不會再像以前那樣打印5次"running thread"字符串。由於main函數返回後整個進程將被銷燬,如圖1-5所示
圖1-5 終止進程和線程
正因如此,咱們以前的示例中經過調用sleep函數向線程提供了充足的時間
那麼,若是咱們但願等線程執行完畢,再結束程序,是否是必定要調用sleep函數?若是是,那麼又牽扯出一個問題了,線程是在什麼時候執行完畢呢?並不是全部的程序都像thread1.c同樣可預測線程的執行時間。那麼,爲了等待線程執行完畢,難道咱們要用一個很是大的數做爲sleep的參數嗎?那這樣就算線程能夠執行完,程序依然在休眠,形成計算機資源的浪費是必定的。那麼,針對這一困境,是否有解決方案呢?固然是有的,那就是pthread_join函數
#include <pthread.h> int pthread_join(pthread_t thread, void ** status);//成功時返回0,失敗時返回其餘值
thread2.c
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <pthread.h> void *thread_main(void *arg); int main(int argc, char *argv[]) { pthread_t t_id; int thread_param = 5; void *thr_ret; if (pthread_create(&t_id, NULL, thread_main, (void *)&thread_param) != 0) { puts("pthread_create() error"); return -1; }; if (pthread_join(t_id, &thr_ret) != 0) { puts("pthread_join() error"); return -1; }; printf("Thread return message: %s \n", (char *)thr_ret); free(thr_ret); return 0; } void *thread_main(void *arg) { int i; int cnt = *((int *)arg); char *msg = (char *)malloc(sizeof(char) * 50); strcpy(msg, "Hello, I'am thread~ \n"); for (i = 0; i < cnt; i++) { sleep(1); puts("running thread"); } return (void *)msg; }
編譯thread2.c並運行
# gcc thread2.c -o thread2 -lpthread # ./thread2 running thread running thread running thread running thread running thread Thread return message: Hello, I'am thread~
接下來咱們來看thread2.c的執行流程圖,如圖1-6所示
圖1-6 調用pthread_join函數
可在臨界區內調用的函數
以前的示例只建立一個線程,接下來的示例將建立多個線程。固然,不管建立多少個線程,其建立方法沒有區別。但關於線程的運行須要考慮「多個線程同時調用函數時(執行時)可能產生的問題」。這類函數內部存在臨界區,也就是說,多個線程同時執行這部分代碼時,可能引發問題。根據臨界區是否引發問題,函數可分爲兩類:
線程安全函數被多個線程同時調用不會發生問題,反之,非線程安全函數被調用時就會出現問題。
下面咱們介紹一個示例,將計算1到10的和,但並非在main函數中計算,而是建立兩個線程,其中一個線程計算1到5的和,另外一個線程計算6到10的和,main函數只負責輸出結果。這種方式的編程模型稱爲「工做線程模型」。計算1到5之和與計算6到10之和的線程將成爲main線程管理的工做。最後,在給出示例代碼以前先給出程序執行流程圖,如圖1-7所示
圖1-7 示例thread3.c的執行流程
thread3.c
#include <stdio.h> #include <pthread.h> void *thread_summation(void *arg); int sum = 0; int main(int argc, char *argv[]) { pthread_t id_t1, id_t2; int range1[] = {1, 5}; int range2[] = {6, 10}; pthread_create(&id_t1, NULL, thread_summation, (void *)range1); pthread_create(&id_t2, NULL, thread_summation, (void *)range2); pthread_join(id_t1, NULL); pthread_join(id_t2, NULL); printf("result: %d \n", sum); return 0; } void *thread_summation(void *arg) { int start = ((int *)arg)[0]; int end = ((int *)arg)[1]; while (start <= end) { sum += start; start++; } return NULL; }
這裏要注意一下,兩個線程都訪問全局變量sum
編譯thread3.c 並運行
# gcc thread3.c -o thread3 -lpthread # ./thread3 result: 55
運行結果是55,雖然正確,但示例自己存在問題。此處存在臨界區相關問題,所以再介紹另外一示例,該示例與上述示例類似,只是增長了發生臨界區相關錯誤的可能性
thread4.c
#include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <pthread.h> #define NUM_THREAD 100 void *thread_inc(void *arg); void *thread_des(void *arg); long long num = 0; int main(int argc, char *argv[]) { pthread_t thread_id[NUM_THREAD]; int i; printf("sizeof long long: %d \n", sizeof(long long)); for (i = 0; i < NUM_THREAD; i++) { if (i % 2) pthread_create(&(thread_id[i]), NULL, thread_inc, NULL); else pthread_create(&(thread_id[i]), NULL, thread_des, NULL); } for (i = 0; i < NUM_THREAD; i++) pthread_join(thread_id[i], NULL); printf("result: %lld \n", num); return 0; } void *thread_inc(void *arg) { int i; for (i = 0; i < 50000000; i++) num += 1; return NULL; } void *thread_des(void *arg) { int i; for (i = 0; i < 50000000; i++) num -= 1; return NULL; }
上述示例共建立100個線程,其中一半執行thread_inc函數中的代碼,另外一半則執行thread_des函數中的代碼,全局變量sum通過增減後的值應仍是0,可是,咱們在編譯執行下程序
# gcc thread4.c -o thread4 -lpthread # ./thread4 sizeof long long: 8 result: 10862532
能夠看到,結果並不是咱們預想的那樣。雖然暫時不清楚緣由,但能夠確定,冒然使用線程對變量進行操做,是有可能發生問題的。那麼,這是什麼問題?如何解決,咱們會在後面的一章介紹