文章目錄
在介紹多線程編程前,還是一如既往的閒扯一會,
昨晚我終於意識到思維導圖以及流程圖的重要性,但是對於學生黨的我應該是沒有閒錢買付費軟件,經過小黑在Internet的遨遊,終於找到了兩款又免費又好用的軟件,推薦給大家,接下來的博客我儘量都用思維導圖以及流程圖呈現知識體系以及代碼流程,一款是愛莫腦圖(畫思維導圖,也可以畫流程圖,但是流程圖需要付費),還有一款就是著名的yed(流程圖), OK接下來進入今天的正題。
1, 多線程簡單介紹
1.1 線程回顧
徐小黑之前有一篇博客就是關於進程和線程的區別,有興趣的可以看一下。
https://blog.csdn.net/weixin_46027505/article/details/104812719
在操作系統原理的術語中,線程是進程的一條執行路徑。線程在Unix系統下,通常被稱爲輕量級的進程,
-
所有的線程都是在同一進程空間運行,這也意味着多條線程將共享該進程中的全部系統資源,如虛擬地址空間,文件描述符和信號處理等等。
-
但同一進程中的多個線程有各自的調用棧(call stack),自己的寄存器環境 (register context),自己的線程本地存儲(thread-local storage)。 一個進程可以有很多線程,每條線程並行執行不同的任務。
-
典型的UNIX進程可以看成只有一個主線程: 一個進程在某一時刻只能做一件事。
有了多線程之後,我們可以讓一個進程同一時刻做不止一件事,每個線程處理各自獨立的任務。
- 一個進程創建後,會首先生成一個缺省的線程,通常稱這個線程爲主線程(或稱控制線程),C/C++程序中,主線程就是通過 main函數進入的線程,由主線程調用pthread_create()創建的線程稱爲子線程,子線程也可以有自己的入口函數,該函數由用戶 在創建的時候指定。每個線程都有自己的線程ID,可以通過pthread_self()函數獲取。最常見的線程模型中,除主線程較爲特殊之外,其他線程一旦被創建,相互之間就是對等關係,不存在隱含的層次關係。每個進程可創建的最大線程數由具體實現決定。
1.2 多線程的優點
線程可以提高應用程序在多核環境下處理諸如文件I/O或者socket I/O等會產生堵塞的情況的表現性能。在Unix系統中,一個 進程包含很多東西,包括可執行程序以及一大堆的諸如文件描述符地址空間等資源。在很多情況下,完成相關任務的不同代碼間 需要交換數據。如果採用多進程的方式,進程的創建所花的時間片要比線程大些,另外進程間的通信比較麻煩,需要在用戶空間 和內核空間進行頻繁的切換,開銷很大。但是如果使用多線程的方式,因爲可以使用共享的全局變量,所以線程間的通信(數據交換)變得非常高效。
2, 主線程和子線程的關係
在介紹多線程編程之前, 必須瞭解主線程和子線程的關係。
無論在windows中還是Posix中,主線程和子線程的默認關係是:無論子線程執行完畢與否,一旦主線程執行完畢退出,所有 子線程執行都會終止。
如果不設置過,整個進程結束或僵死,部分線程保持一種終止執行但還未銷燬的狀態,而進程必須在其所有線程銷燬 後銷燬,這時進程處於僵死狀態。線程函數執行完畢退出,或以其他非常方式終止,線程進入終止態,但是爲線程分配的系統資 源不一定釋放,可能在系統重啓之前,一直都不能釋放,終止態的線程仍舊作爲一個線程實體存在於操作系統中,什麼時候銷燬,取決於線程屬性。在這種情況下,主線程和子線程通常定義以下兩種關係:
-
可會合(joinable):這種關係下,主線程需要明確執行等待操作,在子線程結束後,主線程的等待操作執行完畢,子線 程和主線程會合,這時主線程繼續執行等待操作之後的下一步操作。主線程必須會合可會合的子線程。在主線程的線程函 數內部調用子線程對象的wait函數實現,即使子線程能夠在主線程之前執行完畢,進入終止態,也必須執行會合操作,否 則,系統永遠不會主動銷燬線程,分配給該線程的系統資源也永遠不會釋放。
-
相分離(detached):表示子線程無需和主線程會合,也就是相分離的,這種情況下,子線程一旦進入終止狀態,這種 方式常用在線程數較多的情況下,有時讓主線程逐個等待子線程結束,或者讓主線程安排每個子線程結束的等待順序,是 很困難或不可能的,所以在併發子線程較多的情況下,這種方式也會經常使用。
線程的分離狀態決定一個線程以什麼樣的方式來終止自己,在默認的情況下,線程是非分離狀態的,這種情況下,原有的線程 等待創建的線程結束,只有當pthread_join函數返回時,創建的線程纔算終止,釋放自己佔用的系統資源,而分離線程沒有被其他的線程所等待,自己運行結束了,線程也就終止了,馬上釋放系統資源。
- 至於如何設置線程爲分離態,以及如果線程是會合態,主線程如何設置 接下來很快就會介紹到。
3, 多線程編程
3.1 線程API函數知識體系
接下來我們先介紹前三個分支,第四個互斥鎖的API函數在介紹完鎖的概念後在簡述
3.2 基本函數
3.2.1 pthread_creat()
這個函數是多進程編程的關鍵,所以先介紹這個星號函數。
創建一個線程,該線程將執行start_routine執行的函數,arg爲傳給該函數的參數。新創建的線程ID通過指針tidp傳給主線程,attr指定新創建線程的屬性。
#include <pthread.h>
int pthread_create
(
pthread_t *thread,
const pthread_attr_t *attr,
void *(*start_routine) (void *),
void *arg
);
//編譯鏈接時加上-pthread
說明: pthreand_create()用來創建一個線程,並執行第三個參數start_routine所指向的函數。
- 第一個參數thread是一個pthread_t類型的指針,他用來返回該線程的線程ID。每個線程都能夠通過pthread_self()來獲取自己的線程ID(pthread_t類型)。
- 第二個參數是線程的屬性,其類型是pthread_attr_t類型,其定義如下:
typedef struct {
int detachstate; 線程的分離狀態
int schedpolicy; 線程調度策略
struct sched_param schedparam; 線程的調度參數
int inheritsched; 線程的繼承性
int scope; 線程的作用域
size_t guardsize; 線程棧末尾的警戒緩衝區大小
int stackaddr_set;
void * stackaddr; 線程棧的位置
size_t stacksize; 線程棧的大小
}pthread_attr_t;
//對於這些屬性,我們需要設定的是線程的分離狀態,如果有必要也需要修改每個線程的棧大小。相關函數下面會介紹到
- 第三個參數start_routine是一個函數指針,它指向的函數原型是 void *func(void *),這是所創建的子線程要執行的任務 (函數);
- 第四個參數arg就是傳給了所調用的函數的參數,如果有多個參數需要傳遞給子線程則需要封裝到一個結構體裏傳進去;
例如: pthread_create(&tid, &thread_attr, thread_worker, &value); 其中第四是傳給thread_worker函數的參數
3.2.2 其他三個函數
因爲剩下的幾個函數較簡單,就直接在接下來的示例代碼介紹,具體功能上面的知識體系大圖中已經提到。
主要功能就是設置creat()函數的第二個參數線程屬性。
3.3 默認會合態處理API函數
每個線程創建後默認是joinable 狀態,該狀態需要主線程調用 pthread_join() 等待它退出,否則子線程在結束時,內存資源不能得到釋放造成內存泄漏。
pthread_join(tid, NULL);
//其中pthread_t tid;是pthread_t類型的線程號
3.4 設置線程分離態API函數
我們創建線程時一般會將線程設置爲分離狀態,具體有兩種方法:
-
- 線程裏面調用 pthread_detach(pthread_self()) 這個方法最簡單
-
- 將線程屬性變量設置成PTHREAD_CREATE_DETACHED
pthread_attr_setdetachstate(&thread_attr, PTHREAD_CREATE_DETACHED);
//其中 thread_attr是定義的pthread_attr_t 類型的線程屬性變量。
- 將線程屬性變量設置成PTHREAD_CREATE_DETACHED
4, 多線程鎖概念及鎖API函數
4.1 多線程鎖概念
因爲關於線程互斥鎖這塊的概念相對多,也適用於作爲單個知識點記憶,所以我另外寫一篇博客講述。
https://blog.csdn.net/weixin_46027505/article/details/104991860
4.2 互斥鎖API函數
-
互斥鎖在使用之前,需要先調用 pthread_mutex_init() 函數來初始化互斥鎖;
-
互斥鎖在使用完之後,我們應該調用pthread_mutex_destroy() 將他摧毀釋放;
-
調用pthread_mutex_lock() 來申請鎖,這裏是阻塞鎖,如果鎖被別的線程持有則該函數不會返回;
-
在訪問臨界資源(shared_var)完成退出臨界區時,我們調用**pthread_mutex_unlock()**來釋放鎖,這樣其他線程才能 再次訪問;
-
pthread_mutex_trylock() 來申請非阻塞鎖;如果鎖現在被別的線程佔用則返回非0值,如果沒有被佔用則返回0;
5, 無鎖的線程示例代碼
5.1 默認會合態代碼(單線程)
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
void *thread_worker1(void *args);
int main(int argc, char *argv[])
{
int value = 100;
pthread_t tid;
pthread_create(&tid, NULL, thread_worker1, &value);
pthread_join(tid, NULL); //主線程會阻塞在這裏,直到子線程退出,纔會運行後面的。
while (1)
{
printf("MAIN THREAD START RUN\n");
printf("main thread control %d:\n", value);
sleep(2);
if(value == 110)
{
break;
}
}
printf("main thread start exit\n");
return 0;
}
void *thread_worker1(void *args)
{
int *ptr = (int *)args;
printf("son thread [%ld] start run\n",pthread_self());
while (1)
{
printf("1111111 %s before, value++: %d\n", __FUNCTION__, *ptr);
*ptr += 1;
sleep(2);
printf("1111111 %s after, value++: %d\n", __FUNCTION__, *ptr);
if( *ptr == 105)
{
break;
}
}
printf("son pthread start exit\n");
return NULL;
}
子線程將value的值加1,當加到105,子線程就會退出。然後阻塞在join()等待子線程退出的主線程就會開始運行,但是因爲子進程退出了,value值就不會變了,主進程就永遠不會退出,就會不斷打印105.
5.2 設置成分離態代碼(單線程)
- 下面完成上面的代碼一樣的功能,但是設置了線程的屬性,還有設置了分離態。
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
//申明子線程內調用的函數
void *thread_worker2(void *args);
int main(int argc, char *argv[])
{
int value = 100; //定義一個共享變量
pthread_t tid; //定義線程號
pthread_attr_t thread_attr; //定義create()第2個參數:線程屬性變量
pthread_attr_init(&thread_attr);
pthread_attr_setstacksize(&thread_attr, 120*1024);
pthread_attr_setdetachstate(&thread_attr, PTHREAD_CREATE_DETACHED); //將線程屬性設置成分離態
pthread_create(&tid, &thread_attr, thread_worker2, &value);
pthread_attr_destroy(&thread_attr);
while (1)
{
printf("MAIN THREAD START RUN\n");
printf("main thread control %d:\n", value);
sleep(2);
if(value == 110)
{
break;
}
}
printf("main thread start exit\n");
return 0;
}
void *thread_worker2(void *args)
{
int *ptr = (int *)args;
printf("son thread [%ld] start run\n",pthread_self());
while (1)
{
printf("2222222 %s before, value++: %d\n", __FUNCTION__, *ptr);
*ptr += 1;
sleep(2);
printf("2222222 %s after, value++: %d\n", __FUNCTION__, *ptr);
if( *ptr == 105)
{
break;
}
}
printf("son pthread start exit\n");
return NULL;
}
接下來編譯運行
- 因爲主線程和子線程是分離的,所以主線程不用等待子線程退出在運行。
- 主線程創建子線程後究竟是子線程還是主線程先執行,或究竟哪個子線程先運行系統並沒有規定,這個依賴操作系統的進程 調度策略。
5.3 兩個線程訪問共享變量(不帶鎖)
接下來我們將上面兩段代碼結合一下,但是不加鎖
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
void *thread_worker1(void *args);
void *thread_worker2(void *args);
int main(int argc, char **argv)
{
int shared_var = 100;
pthread_t tid;
pthread_attr_t thread_attr;
if( pthread_attr_init(&thread_attr) )
{
printf("pthread_attr_init() failure: %s\n", strerror(errno));
return -1;
}
if( pthread_attr_setstacksize(&thread_attr, 120*1024) )
{
printf("pthread_attr_setstacksize() failure: %s\n", strerror(errno));
return -1;
}
if( pthread_attr_setdetachstate(&thread_attr, PTHREAD_CREATE_DETACHED) )
{
printf("pthread_attr_setdetachstate() failure: %s\n", strerror(errno));
return -1;
}
pthread_create(&tid, &thread_attr, thread_worker1, &shared_var);
printf("Thread worker1 tid[%ld] created ok\n", tid);
pthread_create(&tid, NULL, thread_worker2, &shared_var);
printf("Thread worker2 tid[%ld] created ok\n", tid);
pthread_attr_destroy(&thread_attr);
pthread_join(tid, NULL);
while(1)
{
printf("Main/Control thread shared_var: %d\n", shared_var);
sleep(10);
}
}
void *thread_worker1(void *args)
{
int *ptr = (int *)args;
if( !args )
{
printf("%s() get invalid arguments\n", __FUNCTION__);
pthread_exit(NULL);
}
printf("Thread workder 1 [%ld] start running...\n", pthread_self());
while(1)
{
printf("111: %s before shared_var++: %d\n", __FUNCTION__, *ptr);
*ptr += 1;
sleep(2);
printf("111: %s after sleep shared_var: %d\n", __FUNCTION__, *ptr);
}
printf("Thread workder 1 exit...\n");
return NULL;
}
void *thread_worker2(void *args)
{
int *ptr = (int *)args;
if( !args )
{
printf("%s() get invalid arguments\n", __FUNCTION__);
pthread_exit(NULL);
}
printf("Thread workder 2 [%ld] start running...\n", pthread_self());
while(1)
{
printf("222: %s before shared_var++: %d\n", __FUNCTION__, *ptr);
*ptr += 1;
sleep(2);
printf("222: %s after sleep shared_var: %d\n", __FUNCTION__, *ptr);
}
printf("Thread workder 2 exit...\n");
return NULL;
}
我們觀察運行結果發現當線程1 操作加1的運算後,就是111before和111after輸出value的值, 相差了兩個數,就是101直接變成了103,而我們進程1想要的效果是101,102.線程1加1的操作被打斷了, 線程2同樣是被打斷了, 爲什麼會這樣? 就是因爲沒有加鎖的緣故,線程2也對共享變量share_value進行了修改,那怎麼才能讓兩個線程自己幹自己的事呢,那肯定是加鎖啊。
6, 帶鎖的線程示例代碼
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
void *thread_worker1(void *args);
void *thread_worker2(void *args);
typedef struct worker_ctx_s
{
int shared_var;
pthread_mutex_t lock;
} worker_ctx_t;
int main(int argc, char **argv)
{
worker_ctx_t worker_ctx;
pthread_t tid;
pthread_attr_t thread_attr;
worker_ctx.shared_var = 1000;
pthread_mutex_init(&worker_ctx.lock, NULL);
if( pthread_attr_init(&thread_attr) )
{
printf("pthread_attr_init() failure: %s\n", strerror(errno));
return -1;
}
if( pthread_attr_setstacksize(&thread_attr, 120*1024) )
{
printf("pthread_attr_setstacksize() failure: %s\n", strerror(errno));
return -1;
}
if( pthread_attr_setdetachstate(&thread_attr, PTHREAD_CREATE_DETACHED) )
{
printf("pthread_attr_setdetachstate() failure: %s\n", strerror(errno));
return -1;
}
pthread_create(&tid, &thread_attr, thread_worker1, &worker_ctx);
printf("Thread worker1 tid[%ld] created ok\n", tid);
pthread_create(&tid, &thread_attr, thread_worker2, &worker_ctx);
printf("Thread worker2 tid[%ld] created ok\n", tid);
while(1)
{
printf("Main/Control thread shared_var: %d\n", worker_ctx.shared_var);
sleep(10);
}
pthread_mutex_destroy(&worker_ctx.lock);
}
void *thread_worker1(void *args)
{
worker_ctx_t *ctx = (worker_ctx_t *)args;
if( !args )
{
printf("%s() get invalid arguments\n", __FUNCTION__);
pthread_exit(NULL);
}
printf("Thread workder 1 [%ld] start running...\n", pthread_self());
while(1)
{
pthread_mutex_lock(&ctx->lock);
printf("1111: %s before shared_var++: %d\n", __FUNCTION__, ctx->shared_var);
ctx->shared_var ++;
sleep(2);
printf("1111: %s after sleep shared_var: %d\n", __FUNCTION__, ctx->shared_var);
pthread_mutex_unlock(&ctx->lock);
sleep(1);
}
printf("Thread workder 1 exit...\n");
return NULL;
}
void *thread_worker2(void *args)
{
worker_ctx_t *ctx = (worker_ctx_t *)args;
if( !args )
{
printf("%s() get invalid arguments\n", __FUNCTION__);
pthread_exit(NULL);
}
printf("Thread workder 2 [%ld] start running...\n", pthread_self());
while(1)
{
if(0 != pthread_mutex_trylock(&ctx->lock) )
{
continue;
}
printf("2222: %s before shared_var++: %d\n", __FUNCTION__, ctx->shared_var);
ctx->shared_var ++;
sleep(2);
printf("2222: %s after sleep shared_var: %d\n", __FUNCTION__, ctx->shared_var);
pthread_mutex_unlock(&ctx->lock);
sleep(1);
}
printf("Thread worker 2 exit...\n");
return NULL;
}
我們觀察到1111before和1111after是正常加1的,沒有被打斷。這就是原子操作,在加鎖和釋放鎖這段代碼是不能被打斷的,是打包的操作。