一、執行緒介紹
- 執行緒是作業系統能內夠進行運算、執行的最小單位,它被包含在程序之中,是程序中的實際運作單位。一條執行緒指的是程序中一個單一順序的控制流,一個程序中可以併發多個執行緒,每條執行緒並行執行不同的任務。
總結:執行緒是程序的一部分,是程序內負責執行的單位,程序是由資源單位(記憶體資源、訊號處理方案、檔案表)+執行單位組成,預設情況下程序內只有一個執行緒,但程序可以有多個。
執行緒的發展簡史:
60年代,在作業系統中能擁有資源和獨立執行的基本單位是程序。
隨著計算機技術的發展,程序出現了很多弊端:
一是由於程序是資源擁有者,建立、撤消與切換存在較大的時間開銷,因此需要引入輕型程序;
二是由於對稱多處理機出現,可以滿足多個執行單位,而多個程序並行開銷過大。
因此在80年代,出現了能獨立執行的基本單位——執行緒(Threads)。
執行緒的排程策略:
執行緒是獨立排程和分派的基本單位,有三種不同的除錯策略:
- 執行緒可以為作業系統核心排程的核心執行緒,如Win32執行緒;
- 由使用者進行自行排程的使用者執行緒,如Linux、UNIX平臺的POSIX Thread;
- 由核心與使用者程序進行混合排程,如Windows 7的執行緒。
多執行緒適用的範圍:
一個程序可以有很多執行緒,每條執行緒並行執行不同的任務。
在多核或多CPU,或支援Hyper-threading的CPU上使用多執行緒程式設計的好處是顯而易見,即提高了程式的執行吞吐率。
在單CPU單核的計算機上,使用多執行緒技術,可以把程序中負責I/O處理、人機互動而常被阻塞的部分與密集計算的部分分開來執行,原因就是執行緒佔用的資源少,被阻塞時不浪費資源。
執行緒的特點:
1、輕型實體:
執行緒中的實體基本上不擁有系統資源,只是有一點必不可少的、能保證獨立執行的資源。執行緒的實體包括用於指示被執行指令序列的程式計數器、區域性變數、狀態引數和返回地址。
執行緒是動態概念,它的動態特性由執行緒控制塊TCB(Thread Control Block)描述,包括以下資訊:
- 執行緒狀態
- 當執行緒不執行時,被儲存的現場資源。
- 一組執行堆疊
- 存放每個執行緒的區域性變數主存區
- 訪問同一個程序中的主存和其它資源
2、獨立排程和分派的基本單位:在多執行緒OS中,執行緒是能獨立執行的基本單位,因而也是獨立排程和分派的基本單位。由於執行緒很“輕”,故執行緒的切換非常迅速且開銷小(在同一程序中的)。
3、可併發執行:在一個程序中的多個執行緒之間,可以併發執行,甚至允許在一個程序中所有執行緒都能併發執行;同樣,不同程序中的執行緒也能併發執行,充分利用和發揮了CPU與外圍裝置並行工作的能力。
4、共享程序資源:在同一程序中的各個執行緒,都可以訪問該程序的使用者空間,此外,還可以訪問程序所擁有的已開啟檔案、定時器、訊號量等,執行緒可以共享該程序所擁有的資源。所以執行緒之間互相通訊不必呼叫核心。
二、執行緒與程序的區別(多程序與多執行緒)
1、資源:
程序採用虛擬空間+使用者態/核心態機制,所以就導致程序與程序之間是互相獨立的,各自的資源不可見。
在同一程序中的各個執行緒都可以共享該程序所擁有的資源。
多程序之間資源是獨立的,多執行緒之間資源是共享的。
2、通訊:
由於程序之間是互相獨立的,需要使用各種IPC通訊機制,保障多個程序協同工作。
同一程序中的各個執行緒共享該程序所擁有的資源,執行緒間可以直接讀寫程序資料段來進行通訊,但需要執行緒同步和互斥手段的輔助,以保證資料的一致性。
多程序之間資源是獨立的,所以需要通訊,多執行緒之間資源是共享的,所以需要同步和互斥。
3、排程:無論系統採用什麼樣的執行緒除錯策略,執行緒上下文切換都比程序上下文切換要快得多。
4、身份:程序是個資源單位,執行緒是個執行單位,並且執行緒是程序的一部分,執行緒需要程序安身立命,程序也需要執行緒當牛做馬。
三、POSIX執行緒庫
POSIX執行緒庫介紹:
POSIX執行緒(POSIX Threads,常被縮寫為pthread)是POSIX的執行緒標準,定義了建立和操縱執行緒的一套API。
實現POSIX 執行緒標準的庫常被稱作pthread,一般用於Unix-likePOSIX 系統,如Linux、Solaris。但是Microsoft Windows上的實現也存在,例如直接使用Windows API實現的第三方庫pthread-w32。
API具體內容:
pthread定義了一套C語言的型別、函式與常量,它以pthread.h
標頭檔案和一個介面庫libpthread.so
,gcc和g++編譯器沒有預設連結該庫,需要程式設計師使用 -l pthread
引數進行手動連結。
pthread API中大致共有100個函式呼叫,全都以pthread_
開頭,並可以分為四類:
- 執行緒管理,如建立執行緒,等待執行緒,查詢執行緒狀態等。
- 互斥鎖,有建立、摧毀、鎖定、解鎖、設定屬性等操作
- 條件變數,有建立、摧毀、等待、通知、設定與查詢屬性等操作
- 使用了互斥鎖的執行緒間的同步管理。
四、建立執行緒
/**
* @thread: 執行緒ID,輸出型引數。我們目前使用的Linux中pthread_t即unsigned long int
* @attr: 執行緒屬性,NULL表示預設屬性,如果沒有特殊需求,一般寫NULL即可
* @start_routine: 執行緒入口函式指標,引數和返回值的型別都是void*
* 啟動執行緒本質上就是呼叫一個函式,只不過是在一個獨立的執行緒中呼叫的,函式返回即執行緒結束
* @arg: 傳遞給執行緒過程函式的引數
* 返回值: 成功返回0,失敗返回錯誤碼,但不會修改全域性的錯誤變數,也就是無法使用perror獲取錯誤原因。
*/
int pthread_create (pthread_t* thread,
const pthread_attr_t* attr,
void* (*start_routine) (void*),
void* arg);
注意:
restrict
: C99引入的編譯最佳化指示符,提高重複解引用同一個指標的效率。- 應設法保證線上程過程函式執行期間,其引數所指向的目標持久有效。
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
void* run(void* arg)
{
for(;;)
{
printf("#");
fflush(stdout);
sleep(1);
}
}
int main(int argc,const char* argv[])
{
pthread_t tid;
int ret = pthread_create(&tid,NULL,run,NULL);
printf("%d %lu\n",ret,tid);
for(;;)
{
printf("*");
fflush(stdout);
sleep(1);
}
return 0;
}
五、執行緒回收
int pthread_join (pthread_t thread, void** retval);
功能:等待thread引數所標識的執行緒結束,並回收相關資源,如果thread執行緒沒有結束則阻塞
retval:獲得執行緒正常結束時的返回值,是輸出型的引數,用於獲取執行緒入口函式的返回值。
返回值:成功返回0,失敗返回錯誤碼
從執行緒過程函式中返回值的方法:
1、執行緒過程函式將所需返回的內容放在一塊記憶體中,返回該記憶體塊的首地址,保證這塊記憶體在函式返回,即執行緒結束,以後依然有效;
2、若retval引數非NULL,則pthread_join函式將執行緒入口函式所返回的指標,複製到該引數所指向的記憶體中;
3、執行緒入口函式所返回的指標指向text、data、bss記憶體段的資料,如果指向heap記憶體段,則還需保證在用過該記憶體之後釋放之。
六、獲取執行緒ID、判斷執行緒ID
pthread_t pthread_self (void);
成功返回撥用執行緒的ID,不會失敗。
int pthread_equal (pthread_t t1, pthread_t t2);
功能:若引數t1和t2所標識的執行緒ID相等,則返回非零,否則返回0。
注意:某些實現的pthread_t不是unsigned long int型別,可能是結構體型別,無法透過“==”判斷其相等性。
練習:在一個多執行緒的程序中,設計一個函式,該函式只能由主執行緒呼叫,其它執行緒如果呼叫了該函式要立即結束執行。
七、終止執行緒
方法1:從執行緒入口函式中return,主執行緒除外。
方法2:呼叫pthread_exit函式。
void pthread_exit (void* retval);
retval - 和執行緒過程函式的返回值語義相同。
注意:在任何執行緒中呼叫exit函式都將終止整個程序。
問題:主執行緒結束,子執行緒是否會跟著一起結束?
主執行緒結束,並不會導致子執行緒跟著一起結束,它們之間沒有必然聯絡。
但是,主執行緒如果執行到最後一行,會執行return 0或隱藏的return 0,而在main函式中執行return 0就相當於執行exit(0),然後當前程序就會結束,有兩種方法可以避免這種情況:
方法1:
等待所有子執行緒結束,主執行緒再執行return 0;
子執行緒在一定時間內會結束,側使用pthread_join。
方法2:
立即結束主執行緒,不要讓它執行return 0;
當子執行緒的結束時間不確定,則使用pthread_exit。
注意:這種情況會產生新的問題,子執行緒的資源沒有辦法回收。
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
void* run(void* arg)
{
for(int i=0; ;i++)
{
printf("子執行緒:%lu %d\n",pthread_self(),i);
sleep(1);
}
}
int main(void)
{
pthread_t tid;
pthread_create(&tid,NULL,run,NULL);
for(int i=0; i<3; i++)
{
printf("我是主執行緒,我要結束了,倒數計時:%d\n",3-i);
sleep(1);
}
exit
}
八、執行緒分離
同步方式(非分離狀態):建立執行緒之後主執行緒呼叫pthread_join函式等待其終止,並釋放執行緒資源。
非同步方式(分離狀態):無需建立者等待,執行緒終止後自行釋放資源。
int pthread_detach (pthread_t thread);
功能:使thread引數所標識的執行緒進入分離(DETACHED)狀態。
返回值:成功返回0,失敗返回錯誤碼。
注意:如果若干個子執行緒需要長時間執行,不知道什麼時候能結束,為了避免它父執行緒陷入無盡的等待,可提前給子執行緒設定分離狀態。
九、取消執行緒
向傳送取消請求:
int pthread_cancel (pthread_t thread);
功能:該函式只是向執行緒發出取消請求,並不等待執行緒終止。
預設情況下,執行緒在收到取消請求以後,並不會立即終止,而是仍繼續執行,直到其達到某個取消點。
在取消點處,執行緒檢查其自身是否已被取消了,並做出相應動作。
設定可取消狀態:
int pthread_setcancelstate (int state,int* oldstate);
成功返回0,並透過oldstate引數輸出原可取消狀態(若非NULL),失敗返回錯誤碼。
state取值:
PTHREAD_CANCEL_ENABLE - 接受取消請求(預設)。
PTHREAD_CANCEL_DISABLE - 忽略取消請求。
設定可取消型別:
int pthread_setcanceltype (int type, int* oldtype);
成功返回0,並透過oldtype引數輸出原可取消型別
(若非NULL),失敗返回錯誤碼。
type取值:
PTHREAD_CANCEL_DEFERRED - 延遲取消(預設)。
被取消執行緒在接收到取消請求之後並不立即響應,
而是一直等到執行了特定的函式(取消點)之後再響應該請求。
PTHREAD_CANCEL_ASYNCHRONOUS - 非同步取消。
被取消執行緒可以在任意時間取消,不是非得遇到取消點才能被取消。
但是作業系統並不能保證這一點。
十、執行緒屬性
int pthread_create (pthread_t* restrict thread,
const pthread_attr_t* restrict attr,
void* (*start_routine) (void*),
void* restrict arg);
//建立執行緒函式的第二個引數即為執行緒屬性,傳空指標表示使用預設屬性。
typedef struct {
// 分離狀態
int detachstate;
// PTHREAD_CREATE_DETACHED - 分離執行緒。
// PTHREAD_CREATE_JOINABLE(預設) - 可匯合執行緒。
// 競爭範圍
int scope;
// PTHREAD_SCOPE_SYSTEM - 在系統範圍內競爭資源(時間片)。
// PTHREAD_SCOPE_PROCESS(Linux不支援) - 在程序範圍內競爭資源。
// 繼承特性
int inheritsched;
// PTHREAD_INHERIT_SCHED(預設) - 排程屬性自建立者執行緒繼承。
// PTHREAD_EXPLICIT_SCHED - 排程屬性由後面兩個成員確定。
// 排程策略
nt schedpolicy;
// SCHED_FIFO - 先進先出策略。
// 沒有時間片。
// 一個FIFO執行緒會持續執行,直到阻塞或有高優先順序執行緒就緒。
// 當FIFO執行緒阻塞時,系統將其移出就緒佇列,待其恢復時再加到同優先順序就緒佇列的末尾。
// 當FIFO執行緒被高優先順序執行緒搶佔時,它在就緒佇列中的位置不變。
// 因此一旦高優先順序執行緒終止或阻塞,被搶佔的FIFO執行緒將會立即繼續執行。
// SCHED_RR - 輪轉策略。
// 給每個RR執行緒分配一個時間片,一但RR執行緒的時間片耗盡,系統即將移到就緒佇列的末尾。
// SCHED_OTHER(預設) - 普通策略。
// 靜態優先順序為0。任何就緒的FIFO執行緒或RR執行緒,都會搶佔此類執行緒。
// 排程引數
struct sched_param schedparam;
// struct sched_param {
// int sched_priority; /* 靜態優先順序 */
// };
// 棧尾警戒區大小(位元組) 預設一頁(4096位元組)。
size_t guardsize;
// 棧地址
void* stackaddr;
// 棧大小(位元組)
size_t stacksize;
} pthread_attr_t;
注意:不要手動讀寫該結構體,而應呼叫pthread_attr_set/get函式設定/獲取具體屬性項。
設定執行緒屬性:
初始化執行緒屬性結構體:
pthread_attr_t attr = {}; // 不要使用這種方式
int pthread_attr_init (pthread_attr_t* attr);
設定具體執行緒屬性項:
int pthread_attr_setdetachstate (pthread_attr_t* attr,int detachstate);
int pthread_attr_setscope (pthread_attr_t* attr,int scope);
int pthread_attr_setinheritsched (pthread_attr_t* attr,int inheritsched);
int pthread_attr_setschedpolicy (pthread_attr_t* attr,int policy);
int pthread_attr_setschedparam (pthread_attr_t* attr,const struct sched_param* param);
int pthread_attr_setguardsize (pthread_attr_t* attr,size_t guardsize);
int pthread_attr_setstackaddr (pthread_attr_t* attr,void* stackaddr);
int pthread_attr_setstacksize (pthread_attr_t* attr,size_t stacksize);
int pthread_attr_setstack (pthread_attr_t* attr,void* stackaddr, size_t stacksize);
以設定好的執行緒屬性結構體為引數建立執行緒:
int pthread_create (pthread_t* restrict thread,
const pthread_attr_t* testrict attr,
void* (*start_routine) (void*),
void* restrict arg);
銷燬執行緒屬性結構體:
int pthread_attr_destroy (pthread_attr_t* attr);
獲取執行緒屬性:
獲取執行緒屬性結構體:
int pthread_getattr_np (pthread_t thread,pthread_attr_t* attr);
獲取具體執行緒屬性項:
int pthread_attr_getdetachstate (pthread_attr_t* attr,int* detachstate);
int pthread_attr_getscope (pthread_attr_t* attr,int* scope);
int pthread_attr_getinheritsched (pthread_attr_t* attr,int* inheritsched);
int pthread_attr_getschedpolicy (pthread_attr_t* attr,int* policy);
int pthread_attr_getschedparam (pthread_attr_t* attr,struct sched_param* param);
int pthread_attr_getguardsize (pthread_attr_t* attr,size_t* guardsize);
int pthread_attr_getstackaddr (pthread_attr_t* attr,void** stackaddr);
int pthread_attr_getstacksize (pthread_attr_t* attr,size_t* stacksize);
int pthread_attr_getstack (pthread_attr_t* attr,void** stackaddr, size_t* stacksize);
以上所有函式成功返回0,失敗返回錯誤碼。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#define __USE_GNU
#include <pthread.h>
int printattrs (pthread_attr_t* attr)
{
printf("------- 執行緒屬性 -------\n");
int detachstate;
int error = pthread_attr_getdetachstate (attr, &detachstate);
if (error)
{
fprintf (stderr, "pthread_attr_getdetachstate: %s\n",strerror (error));
return -1;
}
printf("分離狀態: %s\n",
(detachstate == PTHREAD_CREATE_DETACHED) ? "分離執行緒" :
(detachstate == PTHREAD_CREATE_JOINABLE) ? "可匯合執行緒" :
"未知");
int scope;
if ((error = pthread_attr_getscope (attr, &scope)) != 0)
{
fprintf (stderr, "pthread_attr_getscope: %s\n",
strerror (error));
return -1;
}
printf ("競爭範圍: %s\n",
(scope == PTHREAD_SCOPE_SYSTEM) ? "系統級競爭" :
(scope == PTHREAD_SCOPE_PROCESS) ? "程序級競爭" : "未知");
int inheritsched;
if ((error = pthread_attr_getinheritsched (attr,
&inheritsched)) != 0)
{
fprintf (stderr, "pthread_attr_getinheritsched: %s\n",
strerror (error));
return -1;
}
printf ("繼承特性: %s\n",
(inheritsched == PTHREAD_INHERIT_SCHED) ? "繼承呼叫屬性" :
(inheritsched == PTHREAD_EXPLICIT_SCHED) ? "顯式呼叫屬性" :
"未知");
int schedpolicy;
if ((error = pthread_attr_getschedpolicy(attr,&schedpolicy)) != 0)
{
fprintf (stderr, "pthread_attr_getschedpolicy: %s\n",strerror (error));
return -1;
}
printf ("排程策略: %s\n",
(schedpolicy == SCHED_OTHER) ? "普通" :
(schedpolicy == SCHED_FIFO) ? "先進先出" :
(schedpolicy == SCHED_RR) ? "輪轉" : "未知");
struct sched_param schedparam;
if ((error = pthread_attr_getschedparam (attr, &schedparam)) != 0)
{
fprintf (stderr, "pthread_attr_getschedparam: %s\n",strerror (error));
return -1;
}
printf ("排程優先順序:%d\n", schedparam.sched_priority);
size_t guardsize;
if ((error = pthread_attr_getguardsize(attr, &guardsize)) != 0)
{
fprintf (stderr, "pthread_attr_getguardsize: %s\n",strerror (error));
return -1;
}
printf ("棧尾警戒區:%u位元組\n", guardsize);
/*
void* stackaddr;
if ((error = pthread_attr_getstackaddr (attr, &stackaddr)) != 0)
{
fprintf (stderr, "pthread_attr_getstackaddr: %s\n",strerror (error));
return -1;
}
printf ("棧地址: %p\n", stackaddr);
size_t stacksize;
if ((error = pthread_attr_getstacksize (attr, &stacksize)) != 0)
{
fprintf (stderr, "pthread_attr_getstacksize: %s\n",strerror (error));
return -1;
}
printf ("棧大小: %u位元組\n", stacksize);
*/
void* stackaddr;
size_t stacksize;
if ((error = pthread_attr_getstack (attr, &stackaddr,&stacksize)) != 0)
{
fprintf (stderr, "pthread_attr_getstack: %s\n",strerror (error));
return -1;
}
printf ("棧地址: %p\n", stackaddr);
printf ("棧大小: %u位元組\n", stacksize);
printf("------------------------\n");
return 0;
}
void* thread_proc (void* arg)
{
pthread_attr_t attr;
int error = pthread_getattr_np (pthread_self (), &attr);
if (error)
{
fprintf (stderr, "pthread_getattr_np: %s\n", strerror (error));
exit (EXIT_FAILURE);
}
if (printattrs (&attr) < 0)
exit (EXIT_FAILURE);
exit (EXIT_SUCCESS);
return NULL;
}
int main (int argc, char* argv[])
{
int error;
pthread_attr_t attr, *pattr = NULL;
if (argc > 1)
{
if (strcmp (argv[1], "-s"))
{
fprintf (stderr, "用法:%s [-s]\n", argv[0]);
return -1;
}
if ((error = pthread_attr_init (&attr)) != 0)
{
fprintf (stderr, "pthread_attr_init: %s\n",strerror (error));
return -1;
}
if ((error = pthread_attr_setdetachstate (&attr,PTHREAD_CREATE_DETACHED)) != 0)
{
fprintf (stderr, "pthread_attr_setdetachstate: %s\n",strerror (error));
return -1;
}
if ((error = pthread_attr_setinheritsched (&attr,PTHREAD_EXPLICIT_SCHED)) != 0)
{
fprintf (stderr, "pthread_attr_setinheritsched: %s\n",strerror (error));
return -1;
}
if ((error = pthread_attr_setstacksize (&attr, 4096*10)) != 0)
{
fprintf (stderr, "pthread_attr_setstack: %s\n",strerror (error));
return -1;
}
pattr = &attr;
}
pthread_t tid;
if ((error = pthread_create (&tid, pattr, thread_proc,NULL)) != 0)
{
fprintf (stderr, "pthread_create: %s\n", strerror (error));
return -1;
}
if (pattr)
{
if ((error = pthread_attr_destroy (pattr)) != 0)
{
fprintf (stderr, "pthread_attr_destroy: %s\n",strerror (error));
return -1;
}
}
pause ();
return 0;
}
注意:如果man手冊查不到執行緒的相關函式,安裝完整版gnu手冊:sudo apt-get install glibc-doc。
練習:實現大檔案的多執行緒cp複製,對比系統cp命令,哪個速度更快,為什麼?
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/stat.h>
#include <sys/types.h>
typedef struct Task
{
char* src;
char* dest;
size_t start;
size_t end;
}Task;
void* run(void* arg)
{
Task* task = arg;
// 開啟原始檔和目標檔案
FILE* src_fp = fopen(task->src,"r");
FILE* dest_fp = fopen(task->dest,"a");
if(NULL == src_fp || NULL == dest_fp)
{
perror("fopen");
return NULL;
}
// 調整檔案的位置指標
fseek(src_fp,task->start,SEEK_SET);
fseek(dest_fp,task->start,SEEK_SET);
// 建立緩衝區
char buf[1024];
size_t buf_size = sizeof(buf);
for(int i=task->start; i<task->end; i+=buf_size)
{
int ret = fread(buf,1,buf_size,src_fp);
if(0 >= ret)
break;
fwrite(buf,1,ret,dest_fp);
}
fclose(src_fp);
fclose(dest_fp);
free(task);
}
int main(int argc,const char* argv[])
{
if(3 != argc)
{
puts("Use:./cp <src> <dest>");
return 0;
}
// 獲取到檔案的大小
struct stat buf;
if(stat(argv[1],&buf))
{
perror("stat");
return -1;
}
// 建立出目標檔案
if(NULL == fopen(argv[2],"w"))
{
perror("fopen");
return -2;
}
// 計算需要的執行緒數量,以100M為單位
size_t pthread_cnt = buf.st_size/(1024*1024*100)+1;
// 分配任務
pthread_t tid;
for(int i=0; i<pthread_cnt; i++)
{
Task* task = malloc(sizeof(Task));
task->src = (char*)argv[1];
task->dest = (char*)argv[2];
task->start = i*1024*1024*100;
task->end = (i+1)*1024*1024*100;
// 建立子執行緒並分配任務
pthread_create(&tid,NULL,run,task);
// 分享子執行緒
pthread_detach(tid);
}
// 結束主執行緒
pthread_exit(NULL);
}
多執行緒並不能提高執行速度,反而可能會降低,所以多執行緒不適合解決運算密集性問題,而是適合解決等待、阻塞的問題,如果使用程序去等待,會浪費大量資源,所以使用更輕量的執行緒去等待,節約資源。
一、執行緒同步
同步就是協同步調,按預定的先後次序進行執行。如:你說完,我再說。“同”字從字面上容易理解為一起動作,其實不是,“同”字應是指協同、協助、互相配合。
如程序、執行緒同步,可理解為程序或執行緒A和B一塊配合,A執行到一定程度時要依靠B的某個結果,於是停下來,示意B執行;B依言執行,再將結果給A,A再繼續操作。
在多執行緒程式設計裡面,一些敏感資料不允許被多個執行緒同時訪問,此時就使用同步訪問技術,保證資料在任何時刻,最多有一個執行緒訪問,以保證資料的完整性。
注意:同一個程序記憶體的多個執行緒之間,除了棧記憶體是獨立的,其他資源全部共享。
#include <stdio.h>
#include <pthread.h>
int num = 0;
void* run(void* arg)
{
for(int i=0; i<1000000; i++)
{
// 加鎖
num++;
// 解鎖
}
}
int main(int argc,const char* argv[])
{
pthread_t tid1,tid2;
pthread_create(&tid1,NULL,run,NULL);
pthread_create(&tid2,NULL,run,NULL);
pthread_join(tid1,NULL);
pthread_join(tid2,NULL);
printf("%d\n",num);
}
執行緒A 執行緒A
讀取
運算 讀取
回寫 運算
回寫
二、互斥鎖
注意:如果man手冊中查不到這系列函式,可以安裝以下內容:
sudo apt-get install glibc-doc
sudo apt-get install manpages-posix-dev
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
功能:定義並初始化互斥鎖
int pthread_mutex_init (pthread_mutex_t* mutex,const pthread_mutexattr_t* mutexattr);
功能:初始化一互斥鎖,會被初始化為非鎖定狀態
int pthread_mutex_lock (pthread_mutex_t* mutex);
功能:加鎖,當互斥鎖已經是鎖定狀態時,呼叫者會阻塞,直到互斥被解開,當前執行緒才會加鎖成功並返回。
int pthread_mutex_unlock (pthread_mutex_t* mutex);
功能:解鎖,解鎖後等待加鎖的執行緒才能加鎖成功。
int pthread_mutex_destroy (pthread_mutex_t* mutex);
功能:銷燬鎖
int pthread_mutex_trylock (pthread_mutex_t *__mutex)
功能:加測試鎖,如果不加鎖剛立即返回
int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex,
const struct timespec *restrict abs_timeout);
功能:倒數計時加鎖,如果超時還不加上則立即返回。
struct timespec{
time_t tv_sec; /* Seconds. */
long int tv_nsec; /* Nanoseconds.*/ 1秒= 1000000000 納秒
};
#include <stdio.h>
#include <pthread.h>
/*
執行流程:
1、互斥鎖被初始化為非鎖定狀態
2、執行緒1呼叫pthread_mutex_lock函式,立即返回,互斥量呈鎖定狀態;
3、執行緒2呼叫pthread_mutex_lock函式,阻塞等待;
4、執行緒1呼叫pthread_mutex_unlock函式,互斥量呈非鎖定狀態;
5、執行緒2被喚醒,從pthread_mutex_lock函式中返回,互斥量呈鎖定狀態
*/
pthread_mutex_t mutex;
//pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
int num = 0;
void* run(void* arg)
{
for(int i=0; i<1000000; i++)
{
pthread_mutex_lock(&mutex);
num++;
pthread_mutex_unlock(&mutex);
}
}
int main(int argc,const char* argv[])
{
pthread_mutex_init(&mutex,NULL);
pthread_t pid1,pid2;
pthread_create(&pid1,NULL,run,NULL);
pthread_create(&pid2,NULL,run,NULL);
pthread_join(pid1,NULL);
pthread_join(pid2,NULL);
pthread_mutex_destroy(&mutex);
printf("%d\n",num);
}
三、讀寫鎖
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;
功能:定義並初始化讀寫鎖
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,
const pthread_rwlockattr_t *restrict attr);
功能:初始化讀寫鎖
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
功能:加讀鎖,如果不能加則阻塞等待
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
功能:加寫鎖,如果不能加則阻塞等待
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
功能:解讀寫鎖。
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
功能:嘗試加讀鎖,如果不能加則立即返回
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
功能:嘗試加寫鎖,如果不能加則立即返回
int pthread_rwlock_timedrdlock(pthread_rwlock_t *restrict rwlock,
const struct timespec *restrict abstime);
功能:帶倒數計時加讀鎖,超時則立即返回
int pthread_rwlock_timedwrlock(pthread_rwlock_t *restrict rwlock,
const struct timespec *restrict abstime);
功能:帶倒數計時加寫鎖,超時則立即返回
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
功能:銷燬讀寫鎖
使用讀寫鎖的執行緒應根據後續的操作進行加鎖,如果只對資料進行讀取則只加讀鎖即可,只有對資料進行修改時才應該加寫鎖,與互斥鎖的區別是,它能讓只讀的執行緒加上鎖,使用原理與檔案鎖一樣。
執行緒A 執行緒B
讀鎖 讀鎖 OK
讀鎖 寫鎖 NO
寫鎖 讀鎖 NO
寫鎖 寫鎖 NO
練習:使用讀寫鎖來解決同步問題。
#include <stdio.h>
#include <pthread.h>
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;
int num = 0;
void* run(void* arg)
{
for(int i=0; i<1000000; i++)
{
pthread_rwlock_wrlock(&rwlock);
num++;
pthread_rwlock_unlock(&rwlock);
}
}
int main(int argc,const char* argv[])
{
pthread_t tid1,tid2;
pthread_create(&tid1,NULL,run,NULL);
pthread_create(&tid2,NULL,run,NULL);
pthread_join(tid1,NULL);
pthread_join(tid2,NULL);
pthread_rwlock_destroy(&rwlock);
printf("%d\n",num);
}
四、死鎖問題
什麼是死鎖:
多個執行緒互相等待對方資源,在得到所需要的資源之前都不會釋放自己的資源,然後造成迴圈等待的現象,稱為死鎖。
死鎖產生四大必要條件:
1、資源互斥
2、佔有且等待
3、資源不可剝奪
4、環路等待
以上四個條件缺一不可,只要有一個不滿足就不能構成死鎖。
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
// 建立三個互斥鎖並初始化
pthread_mutex_t mutex1 = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t mutex2 = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t mutex3 = PTHREAD_MUTEX_INITIALIZER;
void* run1(void* arg)
{
pthread_mutex_lock(&mutex1);
usleep(100);
pthread_mutex_lock(&mutex2);
printf("沒有構成死鎖!!!\n");
pthread_mutex_unlock(&mutex2);
pthread_mutex_unlock(&mutex1);
}
void* run2(void* arg)
{
pthread_mutex_lock(&mutex2);
usleep(100);
pthread_mutex_lock(&mutex3);
printf("沒有構成死鎖!!!\n");
pthread_mutex_unlock(&mutex3);
pthread_mutex_unlock(&mutex2);
}
void* run3(void* arg)
{
pthread_mutex_lock(&mutex3);
usleep(100);
pthread_mutex_lock(&mutex1);
printf("沒有構成死鎖!!!\n");
pthread_mutex_unlock(&mutex1);
pthread_mutex_unlock(&mutex3);
}
int main(int argc,const char* argv[])
{
// 建立三個執行緒
pthread_t tid1,tid2,tid3;
pthread_create(&tid1,NULL,run1,NULL);
pthread_create(&tid2,NULL,run2,NULL);
pthread_create(&tid3,NULL,run3,NULL);
// 主執行緒等待三個子執行緒結束
pthread_join(tid1,NULL);
pthread_join(tid2,NULL);
pthread_join(tid3,NULL);
return 0;
}
如休防止出現死鎖:
構成死鎖的四個條件只有一個不成立,就不會產生死鎖了。
1、破壞互斥條件,讓資源能夠共享使用(準備多份)。
2、破壞佔有且等待的條件,一次申請完成它所有需要的資源(把所有資源進行打包,用一把鎖來代表,拿到這反鎖就相當於拿到的所有資源),資源沒有滿足前不讓它執行,一旦開始執行就一直歸它所有, 缺點是系統資源會被浪費。
3、破壞不可剝奪的條件,當已經佔有了一些資源,請求新的資源而獲取不到,然後就釋放已經獲取到的資源,缺點是實現起來比較複雜,釋放已經獲取到的資源可能會造成前一階段的工作浪費。
4、破壞迴圈等待的條件,採用順序分配資源的方法,在系統中為資源進行編號,規定執行緒必須按照編號遞增的順序獲取資源,缺點是資源必須相對穩定,這樣就限制了資源的增加和減少。
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
// 建立三個互斥鎖並初始化
pthread_mutex_t mutex1 = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t mutex2 = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t mutex3 = PTHREAD_MUTEX_INITIALIZER;
void* run1(void* arg)
{
while(1)
{
pthread_mutex_lock(&mutex1);
usleep(100);
if(0 == pthread_mutex_trylock(&mutex2))
break;
pthread_mutex_unlock(&mutex1);
}
printf("沒有構成死鎖!!!\n");
pthread_mutex_unlock(&mutex2);
pthread_mutex_unlock(&mutex1);
}
void* run2(void* arg)
{
while(1)
{
pthread_mutex_lock(&mutex2);
usleep(100);
if(0 == pthread_mutex_trylock(&mutex3))
break;
pthread_mutex_unlock(&mutex2);
}
printf("沒有構成死鎖!!!\n");
pthread_mutex_unlock(&mutex3);
pthread_mutex_unlock(&mutex2);
}
void* run3(void* arg)
{
while(1)
{
pthread_mutex_lock(&mutex3);
usleep(100);
if(0 == pthread_mutex_trylock(&mutex1))
break;
pthread_mutex_unlock(&mutex3);
}
printf("沒有構成死鎖!!!\n");
pthread_mutex_unlock(&mutex1);
pthread_mutex_unlock(&mutex3);
}
int main(int argc,const char* argv[])
{
// 建立三個執行緒
pthread_t tid1,tid2,tid3;
pthread_create(&tid1,NULL,run1,NULL);
pthread_create(&tid2,NULL,run2,NULL);
pthread_create(&tid3,NULL,run3,NULL);
// 主執行緒等待三個子執行緒結束
pthread_join(tid1,NULL);
pthread_join(tid2,NULL);
pthread_join(tid3,NULL);
return 0;
}
檢測死鎖的方法:
總體思路:觀察+分析
方法1:閱讀程式碼,分析各執行緒的加鎖步驟。
方法2:使用strace追蹤程式的執行流程。
方法3:檢視日誌觀察程式的業務執行過程。
方法4:使用gdb除錯,檢視各執行緒的執行情況。
1、把斷點打線上程建立完畢後
2、run
3、info threads 檢視所有執行緒
4、thread n 程序指定的執行緒
5、bt 檢視執行緒堆疊資訊
6、配合s/n單步除錯
什麼是死鎖?
構成死鎖的4個必要條件?
如何避免死鎖?
如何判斷程式是否陷入死鎖?
五、原子操作
所謂的原子操作就是不可被拆分的操作,對於多執行緒對全域性變數進行操作時,就再也不用再執行緒鎖了,和pthread_mutex_t保護作用是一樣的,也是執行緒安全的,有些編譯器在使用時需要加-march=i686編譯引數。
type __sync_fetch_and_add (type *ptr, type value); // +
type __sync_fetch_and_sub (type *ptr, type value); // -
type __sync_fetch_and_and (type *ptr, type value); // &
type __sync_fetch_and_or (type *ptr, type value); // |
type __sync_fetch_and_nand (type *ptr, type value); // ~
type __sync_fetch_and_xor (type *ptr, type value); // ^
功能:以上操作返回的是*ptr的舊值
type __sync_add_and_fetch (type *ptr, type value); // +
type __sync_sub_and_fetch (type *ptr, type value); // -
type __sync_and_and_fetch (type *ptr, type value); // &
type __sync_or_and_fetch (type *ptr, type value); // |
type __sync_nand_and_fetch (type *ptr, type value); // ~
type __sync_xor_and_fetch (type *ptr, type value); // ^
功能:以上操作返回的是*ptr與value計算後的值
type __sync_lock_test_and_set (type *ptr, type value);
功能:把value賦值給*ptr,並返回*ptr的舊值
__sync_lock_release(type *ptr);
功能:將*ptr賦值為0
#include <stdio.h>
#include <pthread.h>
int num = 0;
void* run(void* arg)
{
for(int i=0; i<100000000; i++)
{
__sync_fetch_and_add(&num,1);
}
}
int main(int argc,const char* argv[])
{
pthread_t pid1,pid2;
pthread_create(&pid1,NULL,run,NULL);
pthread_create(&pid2,NULL,run,NULL);
pthread_join(pid1,NULL);
pthread_join(pid2,NULL);
printf("%d\n",num);
}
原子操作的優點:
1、速度賊快
2、不會產生死鎖
原子操作的缺點:
1、該功能並不通用,有些編譯器不支援。
2、type只能是整數相關的型別,浮點型和自定義型別無法使用。
練習1:
使用讀寫鎖或互斥鎖實現一個執行緒安全佇列。
#include "queue.h"
#include <stdlib.h>
Node* create_node(TYPE data)
{
Node* node = malloc(sizeof(Node));
node->data = data;
node->next = NULL;
return node;
}
Queue* create_queue(void)
{
Queue* queue = malloc(sizeof(Queue));
pthread_rwlock_init(&queue->lock,NULL);
queue->front = NULL;
queue->rear = NULL;
return queue;
}
bool empty_queue(Queue* queue)
{
pthread_rwlock_rdlock(&queue->lock);
bool flag = NULL == queue->front;
pthread_rwlock_unlock(&queue->lock);
return flag;
}
void push_queue(Queue* queue,TYPE data)
{
Node* node = create_node(data);
if(empty_queue(queue))
{
pthread_rwlock_wrlock(&queue->lock);
queue->front = node;
queue->rear = node;
}
else
{
pthread_rwlock_wrlock(&queue->lock);
queue->rear->next = node;
queue->rear = node;
}
pthread_rwlock_unlock(&queue->lock);
}
bool pop_queue(Queue* queue)
{
if(empty_queue(queue))
return false;
pthread_rwlock_wrlock(&queue->lock);
Node* tmp = queue->front;
queue->front = tmp->next;
pthread_rwlock_unlock(&queue->lock);
free(tmp);
return true;
}
TYPE top_queue(Queue* queue)
{
pthread_rwlock_rdlock(&queue->lock);
TYPE data = queue->front->data;
pthread_rwlock_unlock(&queue->lock);
return data;
}
void destroy_queue(Queue* queue)
{
while(!empty_queue(queue))
pop_queue(queue);
pthread_rwlock_destroy(&queue->lock);
free(queue);
}
int main(void)
{
Queue* queue = create_queue();
for(int i=0; i<10; i++)
{
push_queue(queue,i);
printf("push %d\n",i);
}
while(!empty_queue(queue))
{
printf("top %d\n",top_queue(queue));
pop_queue(queue);
}
}
練習2:
使用原子操作實現一個執行緒安全的無鎖佇列。
//queue->rear = (queue->rear+1)%queue->cap;
if(queue->rear == queue->cap)
{
queue->rear = 0;
}
else
{
__sync_fetch_and_add(&queue->rear,1);
}
queue->front = (queue->front+1)%queue->cap;
if(queue->front == queue->cap)
{
queue->front = 0;
}
else
{
__sync_fetch_and_add(&queue->front,1);
}
六、生產者與消費者模型
生產者:生產資料的執行緒,這類的執行緒負責從使用者端、客戶端接收資料,然後把資料Push到儲存中介。
消費者:負責消耗資料的執行緒,對生產者執行緒生產的資料進行(判斷、篩選、使用、響應、儲存)處理。
儲存中介:也叫資料倉儲,是生產者執行緒與消費者執行緒之間的資料緩衝區,用於平衡二者之間的生產速度與消耗速度不均衡的問題,透過緩衝區隔離生產者和消費者,與二者直連相比,避免相互等待,提高執行效率。
問題1:生產快於消費,緩衝區滿,撐死。
解決方法:負責生產的執行緒通知負責消費的執行緒全速消費,然後進入休眠。
問題2:消費快於生產,緩衝區空,餓死。
解決方法:負責消費的執行緒通知負責生產的執行緒全速生產,然後進入休眠。
七、條件變數
條件變數是利用執行緒間共享的"全域性變數"進行同步的一種機制,主要包括兩個動作:
1、執行緒等待"條件變數的條件成立"而休眠;
2、等"條件成立"叫醒休眠的執行緒。
為了防止競爭,條件變數的使用總是和一個互斥鎖結合在一起,一般執行緒睡入條件變數,伴隨著解鎖動作,而執行緒從條件變數醒來時,伴隨著加鎖動作,如果加鎖失敗執行緒進入阻塞狀態,而不是睡眠。
// 定義或建立條件變數
pthread_cond_t cond;
// 初始化條件變數
int pthread_cond_init (pthread_cond_t* cond,const pthread_condattr_t* attr);
//亦可pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
// 使呼叫執行緒睡入條件變數cond,同時釋放互斥鎖mutex
int pthread_cond_wait (pthread_cond_t* cond,pthread_mutex_t* mutex);
// 帶倒數計時的睡眠,時間到了會自動醒來
int pthread_cond_timedwait (pthread_cond_t* cond,
pthread_mutex_t* mutex,
const struct timespec* abstime);
struct timespec {
time_t tv_sec; // Seconds
long tv_nsec; // Nanoseconds [0 - 999999999]
};
// 從條件變數cond中叫醒一個執行緒,令其重新獲得原先的互斥鎖
int pthread_cond_signal (pthread_cond_t* cond);
注意:被喚出的執行緒此刻將從pthread_cond_wait函式中返回,
但如果該執行緒無法獲得原先的鎖,則會繼續阻塞在加鎖上。
// 從條件變數cond中喚醒所有執行緒
int pthread_cond_broadcast (pthread_cond_t* cond);
// 銷燬條件變數
int pthread_cond_destroy (pthread_cond_t* cond);
注意:使用互斥鎖配合條件變數實現的生產者與消費者模型,能夠平衡生產與消費的時間不協調,並且可以最大限度的節約執行資源。
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
void* run(void* arg)
{
int index = 1;
for(;;)
{
pthread_mutex_lock(&mutex);
if(0 == index % 10)
{
printf("任務已完成,即將睡眠!\n");
pthread_cond_wait(&cond,&mutex);
}
printf("index = %d\n",index++);
sleep(1);
pthread_mutex_unlock(&mutex);
}
}
int main(int argc,const char* argv[])
{
pthread_t tid;
pthread_create(&tid,NULL,run,NULL);
printf("是否叫醒睡眠的執行緒?");
for(;;)
{
char cmd = getchar();
if('y' == cmd)
{
pthread_cond_signal(&cond);
}
}
pthread_join(tid,NULL);
return 0;
}
八、訊號量
多執行緒使用的訊號量:
#include <semaphore.h>
sem_t sem;
int sem_init(sem_t *sem, int pshared, unsigned int value);
功能:給訊號量設定初始值
pshared:訊號量的使用範圍
0 執行緒間使用
nonzero 程序之間使用
int sem_wait(sem_t *sem);
功能:訊號量減1操作,如果訊號量已經等於0,則阻塞
int sem_trywait(sem_t *sem);
功能:嘗試對訊號量減1操作,能減返回0成功,不能減返回-1失敗,不會阻塞
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
功能:帶倒數計時的對訊號減1操作,能減返回0成功,不能減超時返回-1失敗,阻塞abs_timeout一段時間
int sem_post(sem_t *sem);
功能:對訊號量執行加1操作
int sem_getvalue(sem_t *sem, int *sval);
功能:獲取訊號量的值
int sem_destroy(sem_t *sem);
功能:銷燬訊號量
多程序使用的訊號量:
sem_t *sem_open(const char *name, int oflag,mode_t mode, unsigned int value);
功能:在核心建立一個訊號量物件
name:訊號量的名字
oflag:
O_CREAT 不存在則建立訊號量,存在則獲取
O_EXCL 如果訊號量已經存在,返回失敗
mode:訊號量的許可權
value:訊號量的初始值
sem_t *sem_open(const char *name, int oflag);
功能:獲取訊號,或相關屬性
int sem_unlink(const char *name);
功能:刪除訊號量