引言
本文所有內容均摘抄自《LinuxC編程直通車》,僅作爲學習交流使用。
進程是系統中程序執行和資源分配的基本單位,在進程調度時涉及較複雜的上下文切換。
線程是進程內獨立的一條運行路線, 是處理機調度的最小單元,也稱“輕量級進程”。
線程可對進程的內存空間和資源進行訪問,並與統一進程中的其他線程共享這些資源。
1. 線程概述
線程是一個進程內的基本調度單位,線程是在進程的共享內存中併發的多道執行路徑,它們共享一個進程的資源, 如文件描述和信號處理, 因此,進程可以有一個或多個線程, 即有一個或多個線程控制表及堆棧寄存器, 但卻共享一個用戶地址空間。同時, 線程也有其私有的數據信息, 包括線程號、寄存器、堆棧、信號掩碼等。
爲什麼有了進程的概念後, 還要引入線程的概念呢?
速度快
在Linux系統下, 啓動一個新的進程必須分配給它獨立的地址空間, 建立衆多的數據表來維護它的代碼段、堆棧段和數據段, 是一種“昂貴” 的多任務工作方式。而線程公用相同的地址空間,共享大部分數據, 啓動一個線程所花費的空間遠遠小於啓動一個進程所花費的時間, 而且線程間彼此切換所需的時間也遠遠小於進程間切換所需的時間。 據統計, 總的來說, 一個進程的開銷大約是線程開銷的30倍左右(在不同的系統上,這個數據可能會有較大的區別)。
通信便捷
對於不同的進程來說, 它們具有獨立的數據空間, 要進行數據的傳遞只能通過通信的方式進行, 這種方式不僅費時,而且很不方便。線程則不然,由於同一線程之間共享數據空間, 所以一個線程的數據可以直接爲其他線程所用,這不僅快捷而且方便。但是有的變量不能同時被兩個線程所修改,有的子程序中聲明爲static 的數據改有可能給多線程程序帶來災難性的打擊。
此外線程還有以下優點:
- 提高應用程序響應速度
- 使多CPU系統更加有效
- 改善程序結構
- … …
目前Linux中最流行的線程機制是POSIX 1003.1c "pthread"標準藉口。在程序中需包含頭文件“pthread.h” , 在編譯鏈接時需要使用“-lpthread” 選項,-lpthread意味着鏈接庫目錄下的libpthread.a 或 libpthread.so 文件。
2. 線程的基本操作
線程的基本操作包括創建線程、 線程等待、 線程終止, 這些過程很像進程的常見操作。這些函數接口在/usr/include/pthread.h頭文件中都進行了引用聲明。
2.1 創建線程
線程的創建通過函數pthread_create來完成,函數原型如下
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine)(void *), void *arg);
第一個參數爲只想線程標識符的指針。pthread_t是一個無符號長整型數值,在pthread線程庫中的定義如下
typedef unsigned long int pthread_t;
第二個參數用來設置線程的屬性,常被設爲空指針,這樣將生成默認屬性的線程
第三個參數是線程運行函數的起始地址,最後一個參數是傳遞給運行函數的參數,若不需要則將該參數設爲空指針。
當創建線程成功時, 函數返回0。常見的錯誤返回代碼爲EAGAIN(系統限制創建新的線程)和EINVA(線程屬性值非法)。
每個線程都有自己的線程ID, 線程ID 在pthread_create調用時返回給創建線程的調用者;一個線程也可以在創建後使用pthread_self函數調用來獲取自己的線程ID。
創建新線程示例代碼1如下
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<pthread.h>
int *thread_function(void *args)
{
pthread_t newthid;
newthid = pthread_self();
printf("New thread, thread ID = %u \n", newthid);
return NULL;
}
int main(void)
{
int ret;
pthread_t thid;
printf("Main thread, thread ID = %u \n", pthread_self());
ret = pthread_create(&thid, NULL, (void *)thread_function, NULL);
if(ret != 0){
printf("Create thread error ! \n");
exit(1);
}
printf("Return new thread ID is %u\n", thid);
sleep(1);
return 0;
}
2.2 線程等待
在示例1中,主線程調用sleep()函數而特意使新的線程能夠搶佔到CPU, 事實上POSIX pthread標準提供了專門使一個線程等待另一個線程返回的函數調用pthread_join, 其函數原型如下
int pthread_join(pthread_t thread, void **thread_return);
該函數的作用是將調用pthread_join的線程掛起, 直到參數thread所代表的線程終止。pthread_join相當於進程用來等待子進程的wait函數。
第一個參數指定了將要等待的進程——pthread_create返回的線程標識符;
第二個參數是一個指針, 它指向另一個指針,之後在指向線程的返回值,等待線程的資源被回收。
進程等待代碼示例2如下
#include<stdio.h>
#include<stdlib.h>
#include<pthread.h>
#include<unistd.h>
void thread_function(void)
{
int i;
printf("New thread, thred ID = %u \n", pthread_self());
for(i = 0; i < 500; i++)
printf("This is the new thread.\n");
}
int main(void)
{
pthread_t thread_id;
int i, ret;
printf("Main thread, thread ID = %u\n", pthread_self());
ret = pthread_create(&thread_id, NULL, (void *)thread_function, NULL);
if(ret != 0){
printf("Create thread error! \n");
exit(1);
}
for(i = 0; i < 500; i++)
printf("This is the main thread.\n");
pthread_join(thread_id, NULL);
return 0;
}
從運行結果可以看出:
- 主線程在執行完語句後並沒有直接執行"return 0", 而是等待創建線程的返回。
- 兩個線程的順序是不一定的, 更有可能是交替運行
注意,一個線程不能被多個線程等待,否則第一個接到信號的線程成功返回, 其餘調用pthread_join的線程則返回錯誤代碼ESRCH
2.3 線程終止
一個進程的終止一般有三種途徑,
- 線程執行完畢後,通過return() 或exit() 從線程函數主動返回;
- 通過調用pthread_exit()函數使線程退出;
- 被其他線程調用pthread_cancel()函數終止。
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<pthread.h>
#include<string.h>
char buffer[] = "Hello World";
void thread_function(void *arg)
{
int i;
printf("New thread is running, argument is %s\n", (char *)arg);
strcpy(buffer, "Bye!");
pthread_exit("I like Linux C program!");
}
int main(void)
{
pthread_t thread_id;
int ret;
void *thread_result;
ret = pthread_create(&thread_id, NULL, (void *)thread_function, (void *)buffer);
if(ret != 0){
printf("Create thread error! \n");
exit(1);
}
printf("Main thread is running. \n");
printf("Before new thread running, the buffer content is %s \n", buffer);
ret = pthread_join(thread_id, &thread_result);
if(ret != 0){
printf("Threat join error! \n");
exit(1);
}
printf("Main waiting new thread, it returns %s \n", (char*)thread_result);
printf("New thread returned, now the buffer is %s\n",buffer);
return 0;
}
示例代碼中pthread_exit函數唯一的參數是函數的返回代碼,返回給pthread_join的第二個參數(當第二個參數不爲NULL時)。
2.4. 線程屬性
咳咳,這些內容先不寫…
3. 線程同步
線程最大的特點就是資源共享性,但資源共享中的同步問題是多線程編程的難點。
所謂同步,即線程等待某個時間的發生,只有當等待的事件發生後線程才能繼續執行,否則線程被掛起並放棄處理器。
Linux下提供了多種方式來處理線程的同步, 最常見的機制是互斥鎖、條件變量和信號量。
互斥鎖(mutex) 通過鎖機制實現線程間同步,同一時刻只允許一個線程執行一個關鍵部分代碼;
條件變量(condition variable) 是利用線程間共享的全局變量進行同步的一種機制;
3.1 互斥鎖(mutex)
互斥鎖用來保證一段時間內只有一個線程在執行一段代碼,它只有“上鎖”、“解鎖”兩種狀態。
同一時刻只能有一個線程掌握某個互斥鎖,擁有上鎖狀態的線程能夠對共享資源進行操作,若其他線程希望上鎖一個已經被上鎖的互斥鎖,則該線程就會掛起,知道上鎖的線程釋放互斥鎖爲止。
無論是軟件資源還是硬件資源,多個線程必須互斥地對它進行訪問。每個線程中方位臨界資源的代碼稱爲臨界區(critical section)(臨界資源是一次僅允許一個線程使用的共享資源)。每次只允許一個線程進入臨界區,進入後不允許其他線程進入。
3.3.1 創建和銷燬
在Linux Threads中有兩種方式創建互斥鎖:靜態方式和動態方式。POSIX定義了一個宏PTHREAD_MUTEX_INITIALIZER來靜態初始化互斥鎖,該方法很簡單:
pthread_mutex_t = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_init()函數動態地初始化一個互斥鎖,函數原型如下:
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr);
銷燬操作函數原型如下
int pthread_mutex_destroy(pthread_mutex_t *mutex);
3.3.2 互斥鎖屬性
3.3.3 鎖操作
鎖操作主要包括加鎖、解鎖和測試三個,函數原型如下
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
3.2 條件變量
3.3 信號量
信號量可以分爲二值信號量和多值信號量。前者只有兩種狀態,0爲燈滅,資源不可用,1爲燈亮,資源可用。多值信號量本質是一個非負的整數計數器,當共享資源被釋放時信號量+1,當資源被佔用時信號量-1.
3.3.1創建和註銷
信號量的數據類型爲sem_t,實際上是一個長整型的數。函數sem_init()用來創建一個信號量,並初始化信號量的值,其函數原型爲:
int sem_init(sem_t *sem, int pshared, unsigned int value);
指針sem表示要創建的信號量, pshared表示是否再多進程間共享信號量而不是僅在一個進程中的所有線程間共享,該參數爲0時表示只能在當前進程的所有線程間共享,否則此信號量在多個進程間共享,value爲信號量的初始值。
函數sem_destroy()用來釋放信號量sem, 函數原型如下
int sem_destroy(sem_t *sem);
3.3.2類P操作
函數sem_wait()用來阻塞當前線程直到信號量sem的值>0,解除阻塞後將sem的值-1,表明公共資源經佔用後數目減少,與P操作有着異曲同工之妙,函數原型如下
int sem_wait(sem_t *sem);
3.3.3類V操作
函數sem_post()用來增加信號量的值,當有線程阻塞在這個信號量上時,調用該函數會使其中的一個線程不在阻塞,而選擇哪個線程獲得資源則是由線程的調度策略決定的,函數原型如下:
int sem_post(sem_t *sem);