作業系統-執行緒

sleeeeeping發表於2024-08-22

一、執行緒介紹

  • 執行緒是作業系統能內夠進行運算、執行的最小單位,它被包含在程序之中,是程序中的實際運作單位。一條執行緒指的是程序中一個單一順序的控制流,一個程序中可以併發多個執行緒,每條執行緒並行執行不同的任務。
    總結:執行緒是程序的一部分,是程序內負責執行的單位,程序是由資源單位(記憶體資源、訊號處理方案、檔案表)+執行單位組成,預設情況下程序內只有一個執行緒,但程序可以有多個。

執行緒的發展簡史:

​ 60年代,在作業系統中能擁有資源和獨立執行的基本單位是程序。
​ 隨著計算機技術的發展,程序出現了很多弊端:
​ 一是由於程序是資源擁有者,建立、撤消與切換存在較大的時間開銷,因此需要引入輕型程序;
​ 二是由於對稱多處理機出現,可以滿足多個執行單位,而多個程序並行開銷過大。
​ 因此在80年代,出現了能獨立執行的基本單位——執行緒(Threads)。

執行緒的排程策略:

​ 執行緒是獨立排程和分派的基本單位,有三種不同的除錯策略:

  1. 執行緒可以為作業系統核心排程的核心執行緒,如Win32執行緒;
  2. 由使用者進行自行排程的使用者執行緒,如Linux、UNIX平臺的POSIX Thread;
  3. 由核心與使用者程序進行混合排程,如Windows 7的執行緒。

多執行緒適用的範圍:

​ 一個程序可以有很多執行緒,每條執行緒並行執行不同的任務。
​ 在多核或多CPU,或支援Hyper-threading的CPU上使用多執行緒程式設計的好處是顯而易見,即提高了程式的執行吞吐率。
​ 在單CPU單核的計算機上,使用多執行緒技術,可以把程序中負責I/O處理、人機互動而常被阻塞的部分與密集計算的部分分開來執行,原因就是執行緒佔用的資源少,被阻塞時不浪費資源。

執行緒的特點:

1、輕型實體
​ 執行緒中的實體基本上不擁有系統資源,只是有一點必不可少的、能保證獨立執行的資源。執行緒的實體包括用於指示被執行指令序列的程式計數器、區域性變數、狀態引數和返回地址。
​ 執行緒是動態概念,它的動態特性由執行緒控制塊TCB(Thread Control Block)描述,包括以下資訊:

  1. 執行緒狀態
  2. 當執行緒不執行時,被儲存的現場資源。
  3. 一組執行堆疊
  4. 存放每個執行緒的區域性變數主存區
  5. 訪問同一個程序中的主存和其它資源

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_開頭,並可以分為四類

  1. 執行緒管理,如建立執行緒,等待執行緒,查詢執行緒狀態等。
  2. 互斥鎖,有建立、摧毀、鎖定、解鎖、設定屬性等操作
  3. 條件變數,有建立、摧毀、等待、通知、設定與查詢屬性等操作
  4. 使用了互斥鎖的執行緒間的同步管理。

四、建立執行緒

/**
 * @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); 

注意

  1. restrict: C99引入的編譯最佳化指示符,提高重複解引用同一個指標的效率。
  2. 應設法保證線上程過程函式執行期間,其引數所指向的目標持久有效。
#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);
}

六、生產者與消費者模型

img

生產者:生產資料的執行緒,這類的執行緒負責從使用者端、客戶端接收資料,然後把資料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);
功能:刪除訊號量

相關文章