(一):posix線程及線程間互斥
有了進程的概念,為何還要使用線程呢?
首先,回憶一下上一個系列我們講到的IPC,各個進程之間具有獨立的內存空間,要進行數據的傳遞只能通過通信的方式進行,這種方式不僅費時,而且很不方便。而同一個進程下的線程是共享全局內存的,所以一個線程的數據可以在另一個線程中直接使用,及快捷又方便。
其次,在Linux系統下,啟動一個新的進程必須分配給它獨立的地址空間,建立眾多的數據表來維護它的代碼段、堆棧段和數據段,這是一種"昂貴"的多任務工作方式。而運行於一個進程中的多個線程,它們彼此之間使用相同的地址空間,共享大部分數據,啟動一個線程所花費的空間遠遠小於啟動一個進程所花費的空間,而且,線程間彼此切換所需的時間也遠遠小於進程間切換所需要的時間。
但是,伴隨著這些優點,線程卻帶來了同步與互斥的問題。下面先講講線程基本函數:
1. 線程的創建pthread_create
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
一個線程由一個線程ID(參數thread)標識,新的線程創建成功,其值通過指針thread返回。
參數attr為線程屬性(比如:優先級、初始棧大小等),通常我們使用默認設置,設為NULL。
參數start_routine為一個函數指針,指向線程執行的函數,最後參數arg為函數start_routine唯一參數,如果需要傳遞多個參數,需要打包為結構,然後將其地址傳給該函數。
pthread_create成功時返回0,失敗為非0值,這和其他linux系統調用的習慣不一樣。
2. pthread_join函數
#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
通過調用該函數等待一個給定線程終止,類似於線程的waitpid函數。
該函數等待參數thread指定的線程終止,該函數會阻塞,直到線程thread終止,將線程返回的(void *)指針賦值為retval指向的位置,然後回收已經終止線程佔用的所有存儲器資源。
3. pthread_self函數
#include <pthread.h>
pthread_t pthread_self(void);
該函數用於獲取線程自身線程ID。類似於進程的getpid函數。
4. pthread_detach函數
#include <pthread.h>
int pthread_detach(pthread_t thread);
該函數可分離可結合線程,線程可以通過以pthread_self()為參數的pthread_detach調用來分離他們自己。
一個分離線程是不能被其他線程回收或殺死的,他的存儲器資源在他終止時由系統自動釋放。一個可結合線程能夠被其他線程收回其資源和殺死,在被其他線程收回之前,他的存儲器資源是沒有被釋放的。在任何一個時間點上,線程是可結合的或者是可分離的。默認情況下,線程是被創建成可結合的。
為了避免存儲器洩露,每個可結合線程都應該要麼被其他線程現實的收回,要麼通過調用pthread_detach函數被分離。
在現實的程序中,我們一般都使用分離的線程。
5. pthread_exit函數
#include <pthread.h>
void pthread_exit(void *retval);
該函數作用就是終止線程。如果該線程未曾分離,他的線程ID和退出狀態將一直保留到調用進程內某個其他線程對他調用pthread_join。
另外,當線程函數(pthread_create第三個參數)返回時,該線程將終止;當創建該線程的進程main函數返回時,該線程也將終止。
下面給一個簡單的示例
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
void* func_th(void* arg)
{
unsigned int* val = (unsigned int*)arg;
printf("=======%s->%d==thread%d: %u====\n", __func__, __LINE__,
*val, (unsigned int)pthread_self());
return NULL;
}
int main(int argc, const char* argv[])
{
pthread_t tid1, tid2;
int a, b;
a = 1;
if (0 != pthread_create(&tid1, NULL, func_th, &a)) {
printf("pthread_create failed!\n");
return -1;
}
b = 2;
if (0 != pthread_create(&tid2, NULL, func_th, &b)) {
printf("pthread_create failed!\n");
return -1;
}
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
return 0;
}
執行2次輸出為:
# ./target_bin
=======func_th->9==thread1: 3077856064====
=======func_th->9==thread2: 3069463360====
# ./target_bin
=======func_th->9==thread2: 3069315904====
=======func_th->9==thread1: 3077708608====
類似於進程,線程的調度隨機的。
在前面開始我們說到同一個進程內的線程是共享全局內存的,那麼當多個線程同時去修改一個全局變量的時候就會出問題,如果一個線程在修改某個變量時中途被掛起,操作系統去調度另外一個線程執行,那就可能導致錯誤。我們無法保證操作系統對這些操作都是原子的。
在我們在現在的例子中這樣去復現這種問題:一個線程對一個全局變量(100)進行讀-加1-讀操作,另個變量對該全局進行減1操作,我們通過sleep來實現加線程先執行,減線程在加線程的加和讀之間進行,最後來查看加操作是否是我們期望的結果(101)。例子如下:
(該示例只是為了強化運行時出錯,並對這種錯誤有一個宏觀的瞭解而寫)
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
int gval = 100;
void* func_add_th(void* arg)
{
int* val = (int*)arg;
printf("==do %s==thread%d: %u====\n", __func__,
*val, (unsigned int)pthread_self());
printf("before add 1, gval=%d\n", gval);
gval += 1;
sleep(4);//此時add線程掛起,sub線程執行鍵操作
printf("after add 1, gval=%d\n", gval);
return NULL;
}
void* func_sub_th(void* arg)
{
int* val = (int*)arg;
printf("==do %s==thread%d: %u====\n", __func__,
*val, (unsigned int)pthread_self());
gval -= 1;
return NULL;
}
int main(int argc, const char* argv[])
{
pthread_t tid1, tid2;
int a, b;
a = 1;
if (0 != pthread_create(&tid1, NULL, func_add_th, &a)) {
printf("pthread_create failed!\n");
return -1;
}
sleep(1); //保證add線程先被調度
b = 2;
if (0 != pthread_create(&tid2, NULL, func_sub_th, &b)) {
printf("pthread_create failed!\n");
return -1;
}
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
return 0;
}
執行結果如下:
# ./target_bin
==do func_add_th==thread1: 3078355776====
before add 1, gval=100
==do func_sub_th==thread2: 3069963072====
after add 1, gval=100
通過輸出我們可以看到sub操作在加1和讀之間操作,最終讀取出來的值仍然是100,不是我們期望的101。
這就是兩個線程不是互斥帶來的結果,所以我們希望在某某一線程一段代碼執行期間,只有一個線程在運行,當運行完成之後,下一個線程運行該部分代碼,所以我們需要將該部分代碼加鎖。這就是線程編程,也是併發編程需要考慮的問題。
解決多線程共享的問題就是使用互斥鎖(mutex,即mutual exliusion)來保護共享數據。在執行某一段代碼是首先要持有該互斥鎖,執行完成之後再釋放該鎖。互斥鎖是類型為pthread_mutex_t的變量。使用如下方法來加鎖和解鎖操作。
派生到我的代碼片
#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
首先我們需要初始化鎖,初始化方法有兩種,一種是靜態初始化,給鎖變量賦值PTHREAD_MUTEX_INITIALIZER,一種動態初始化,使用函數pthread_mutex_init。
我們使用靜態方法初始化:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
當試圖使用pthread_mutex_lock()獲得一個已經被另外線程加鎖的鎖時,本線程將阻塞,直到互斥鎖被解鎖為止。函數pthread_mutex_trylock為獲取鎖的非阻塞版本,當獲取失敗時會立即返回。
我們修改add和sub線程函數分別如下:
void* func_add_th(void* arg)
{
int* val = (int*)arg;
pthread_mutex_lock(&mutex);//此處加鎖
printf("==do %s==thread%d: %u====\n", __func__,
*val, (unsigned int)pthread_self());
printf("before add 1, gval=%d\n", gval);
gval += 1;
sleep(4);
printf("after add 1, gval=%d\n", gval);
pthread_mutex_unlock(&mutex);//此處釋放鎖
return NULL;
}
void* func_sub_th(void* arg)
{
int* val = (int*)arg;
pthread_mutex_lock(&mutex);
printf("==do %s==thread%d: %u====\n", __func__,
*val, (unsigned int)pthread_self());
gval -= 1;
pthread_mutex_unlock(&mutex);
return NULL;
}
執行結果為:
# ./target_bin
==do func_add_th==thread1: 3077614400====
before add 1, gval=100
after add 1, gval=101
==do func_sub_th==thread2: 3069221696====
通過結果輸出可以看到,sub操作是在add操作執行完成之後才執行的,而add線程輸出結果也是我們預期的,所以我們的加鎖是成功的。但是如果add線程要執行很久的話,sub線程就要阻塞很久,我們可以將sub線程加鎖函數改為非阻塞版本,當加鎖失敗時,立即返回。
修改後的sub線程函數:
void* func_sub_th(void* arg)
{
int* val = (int*)arg;
if (0 != pthread_mutex_trylock(&mutex)) {
printf("failed to lock!\n");
return NULL;
}
printf("==do %s==thread%d: %u====\n", __func__,
*val, (unsigned int)pthread_self());
gval -= 1;
pthread_mutex_unlock(&mutex);
return NULL;
}
運行輸出為:
# ./target_bin
==do func_add_th==thread1: 3077638976====
before add 1, gval=100
failed to lock!
after add 1, gval=101
當多個線程同時需要多個相同鎖時,可能會出現死鎖的情況。比如兩個線程同時需要互斥鎖1和互斥鎖2,線程a先獲得鎖1,線程b獲得鎖2,這是線程a、b分別還需要鎖2和鎖1,但此時兩個鎖都被加鎖了,都阻塞在那裡等待對方釋放鎖,這樣死鎖就出現了。我們來實現一下死鎖的情況,將之前兩個例子的線程函數修改如下:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t mutex_sec = PTHREAD_MUTEX_INITIALIZER;
void* func_add_th(void* arg)
{
int* val = (int*)arg;
pthread_mutex_lock(&mutex);//1. add線程先加第一個鎖mutex
printf("==do %s==thread%d: %u====\n", __func__,
*val, (unsigned int)pthread_self());
sleep(2);//等待2秒,讓sub線程加第二個鎖mutex_sec
pthread_mutex_lock(&mutex_sec);//4. add線程加鎖mutex_sec失敗
printf("before add 1, gval=%d\n", gval);
gval += 1;
sleep(4);
printf("after add 1, gval=%d\n", gval);
pthread_mutex_unlock(&mutex);
pthread_mutex_unlock(&mutex_sec);
return NULL;
}
void* func_sub_th(void* arg)
{
int* val = (int*)arg;
pthread_mutex_lock(&mutex_sec);//2. Sub線程比add線程先加鎖mutex_sec
pthread_mutex_lock(&mutex);//3. Sub線程加鎖mutex失敗
printf("==do %s==thread%d: %u====\n", __func__,
*val, (unsigned int)pthread_self());
gval -= 1;
pthread_mutex_unlock(&mutex);
pthread_mutex_unlock(&mutex_sec);
return NULL;
}
上面兩個線程按照函數註釋中1-2-3-4順序執行,運行時程序就卡在那裡出現了死鎖。
可以使用非阻塞版本的加鎖函數來加鎖,不過也要注意在第二個鎖加鎖不成功情況下,需要釋放第一個鎖再返回,不然其他線程仍然得不到第一個鎖。有時在線程需要多個互斥鎖時,讓線程按照指定的同樣順序進行加鎖也可以避免死鎖。程序死鎖出現時很難定位,所以程序猿在編程(尤其是在設計)時需要注意避免這個問題。
本節示例代碼下載: