以 Windows 為例,一個拓展名為 .exe 的檔案就是一個應用程式,應用程式是能夠雙擊執行的。
應用程式執行起來就建立了一個程序,即程序就是執行起來的應用程式;如電腦上執行的 Edge、Typora、PotPlayer 等。
程序的特點:
我們知道,一個程序指的是一個正在執行的應用程式;而執行緒則是執行程序中的某個具體任務,比如一段程式、一個函數等。
程序想要執行任務就需要依賴執行緒;換句話說,執行緒就是程序中的最小執行單位,並且一個程序中至少有一個執行緒(主執行緒)。
執行緒和程序之間的關係,類似於工廠和工人之間的關係:
程序僅負責為各個執行緒提供所需的資源,真正執行任務的是執行緒,而不是程序。
提到多執行緒這裡要說兩個概念,就是序列和並行,搞清楚這個,我們才能更好地理解多執行緒。
所謂序列,其實是相對於單條執行緒來執行多個任務來說的,我們就拿下載檔案來舉個例子:當我們下載多個檔案時,在序列中它是按照一定的順序去進行下載的,也就是說,必須等下載完 A 之後才能開始下載 B;它們在時間上是不可能發生重疊的。
下載多個檔案,多個檔案同時進行下載;這裡是嚴格意義上的,在同一時刻發生的,並行在時間上是重疊的。
瞭解了序列和並行這兩個概念之後,我們再來說說什麼是多執行緒。舉個例子,我們開啟騰訊管家,騰訊管家本身就是一個應用程式,也就是說它就是一個程序,它裡面有很多的功能,我們可以看下圖,能查殺病毒、清理垃圾、電腦加速等眾多功能:
按照單執行緒來說,無論你想要清理垃圾、還是要病毒查殺,那麼你必須先做完其中的一件事,才能做下一件事,這裡面是有一個執行順序的。如果是多執行緒的話,我們其實在清理垃圾的時候,還可以進行查殺病毒、電腦加速等等其他的操作,這個是嚴格意義上的同一時刻發生的,沒有執行上的先後順序。
所謂多執行緒,即一個程序中擁有多個執行緒(≥2,主執行緒+若干子執行緒),執行緒之間相互共同作業、共同執行一個應用程式。
我們通常將以「多執行緒」方式編寫的程式稱為「多執行緒程式」,將編寫多執行緒程式的過程稱為「多執行緒程式設計」,將擁有多個執行緒的程序稱為「多執行緒程序」。
PS:以下程式碼是在 Linux 下執行的。
定義:typedef unsigned long int pthread_t;
。
功能:用於宣告執行緒ID,是一個執行緒識別符號。
函數原型:int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
頭 文 件:#include <pthread.h>
功能介紹:用來建立一個執行緒
引數介紹:
返 回 值:
下面我們通過程式碼來深入理解如何建立一個子執行緒。
#include <stdio.h>
#include <pthread.h>
#include <errno.h>
/* 執行緒執行函數 */
void *func(void *arg)
{
int i;
for (i = 1; i <= 10; i++) //該函數執行動作:列印 10 次 func
{
printf("func[%d]\n", i);
}
return NULL;
}
int main()
{
printf("主執行緒開始執行\n");
pthread_t th; //定義一個執行緒識別符號
/* 使用預設屬性建立執行緒,該執行緒執行 func 函數 */
if (0 != pthread_create(&th, NULL, func, NULL))
{
/* 輸出錯誤紀錄檔並列印相應的錯誤碼 */
printf("fail, errno[%d, %s], \n", errno, strerror(errno));
}
/* 阻塞主執行緒,不然,主執行緒馬上結束,從而使建立的執行緒沒有機會開始執行就結束了 */
sleep(2);
printf("主執行緒結束執行\n");
return 0;
}
注意:編寫 Linux 下的多執行緒程式時,需要使用標頭檔案pthread.h,連線時需要使用靜態庫 libpthread.a。
執行結果如下:
上述程式碼中,我們通過在 main 中新增 sleep 來阻塞主執行緒,以此保證子執行緒可以正常執行並終止;但通過 sleep 的方式阻塞主執行緒多少會影響程式效率,所以我們需要換一種方式來阻塞主執行緒。
在講解 pthread_join 前,我們先來通過一個小栗子初步體驗一下為何需要 pthread_join。
場景 1
在簡單的程式中一般只需要一個執行緒就可以搞定,也就是主執行緒:
int main()
{
printf("主執行緒開始執行\n");
return 0;
}
現在假設我要做一個比較耗時的工作,從一個伺服器下載一個視訊並進行處理,那麼我的程式碼會變成:
int main()
{
printf("主執行緒開始執行\n");
download(); // 下載視訊到本地
process(); // 視訊處理
return 0;
}
場景 2
如果我需要下載兩個視訊素材,一起在本地進行處理,也很簡單:
int main()
{
printf("主執行緒開始執行\n");
download1(); //下載視訊 1
download2(); //下載視訊 2
process(); //處理視訊 1、2
return 0;
}
本身這麼做完全沒有問題,可是就是有點浪費時間,如果兩個視訊能夠同時下載就好了,這時候執行緒就派上了用場。
#include <stdio.h>
#include <pthread.h>
void *download1(void *arg)
{
puts("子執行緒開始下載第一個視訊...");
sleep(6); // 耗時 6s
puts("第一個視訊下載完成");
}
void *download2(void *arg)
{
puts("主執行緒開始下載第二個視訊...");
sleep(10); // 耗時 10s
puts("第二個視訊下載完成");
return NULL;
}
void process()
{
puts("開始處理兩個視訊...");
sleep(3); // 耗時 3s
puts("處理完成");
}
int main()
{
printf("主執行緒開始執行\n");
pthread_t th;
pthread_create(&th, NULL, download1, NULL); // 子執行緒下載視訊 1
download2(NULL); // 主執行緒下載視訊 2
process();//處理視訊1、2
return 0;
}
主執行緒叫來了 th 這個執行緒去下載「視訊 1」,自己去下載「視訊 2」;減輕了自己的工作量也縮短了時間。
通過download
函數的對比,可以發現,兩個視訊同時下載肯定是「視訊 1」先下載完,這樣在主執行緒下載完「視訊 2」的時候,「視訊 1」已經準備好了,後面就可以一起進行處理,這沒什麼問題。
但是萬一「視訊 1」的下載時間比「視訊 2」的時間長呢(比如下載「視訊 2」僅需要耗費 3s 的時間)?當「視訊 2」下載完成了,但此時子執行緒 th 還沒幹完活,本地還沒有「視訊 1」,那麼接下來處理的時候肯定會有問題,或者說接下來不能直接進行處理,要等 th 幹完活後,主執行緒中的process
函數才能去處理這兩個視訊。
在這種場景下就用到了pthread_join()
這個函數。
函數原型:int pthread_join(pthread_t thread, void **retval);
頭 文 件:#include <pthread.h>
功能介紹:用來等待一個執行緒的結束
引數介紹:
返 回 值:
這個函數是一個執行緒阻塞的函數,呼叫它的函數將一直等待到被等待的執行緒結束為止,當函數返回時,被等待執行緒的資源被收回。
下面,我們通過 pthread_join 修改一下「場景 2」的程式碼:
void *download1(void *arg)
{
puts("子執行緒開始下載第一個視訊...");
sleep(6); // 耗時 6s
puts("第一個視訊下載完成");
}
void *download2(void *arg)
{
puts("主執行緒開始下載第二個視訊...");
sleep(3); // 耗時 3s
puts("第二個視訊下載完成");
return NULL;
}
int main()
{
printf("主執行緒開始執行\n");
pthread_t th;
pthread_create(&th, NULL, download1, NULL); // 子執行緒下載視訊 1
download2(NULL); // 主執行緒下載視訊 2
pthread_join(th, NULL); // 阻塞主執行緒,直到「視訊 1」下載完成
process();//處理視訊1、2
return 0;
}
現在下載「視訊 1」需要 6s,下載「視訊 2」需要 3s;當「視訊 2」下載完成後要等待「視訊 1」下載完成方可一起進行處理,為了實現這個目的,我們在第 24 行加入了pthread_join()
。
在這個場景下,我們明確兩個事情:
Q1:誰呼叫了pthread_join
函數?
pthread_join
函數,因此必須等待 th 的下載任務結束了,pthread_join()
才能返回。Q2:在哪個執行緒環境下呼叫了pthread_join
函數?
pthread_join
函數的,因此主執行緒要等待 th 的工作做完,否則主執行緒將一直處於阻塞狀態。這裡不要搞混的是子執行緒 th 真正做的任務(下載「視訊 1」)是在另一個執行緒中做的;但是 th 呼叫
pthread_join
函數的動作是在主執行緒環境下做的。
子執行緒執行的函數在結束後可能會有返回值:
#define STRING_LEN_24 24
/* 執行緒執行函數 */
void *func(void *arg)
{
int i;
for (i = 1; i <= 10; i++) //該函數執行動作:列印 10 次 func
{
printf("func[%d]\n", i);
}
char *buf = (char *)malloc(STRING_LEN_24);
strncpy(buf, "The child thread ends", STRING_LEN_24 - 1);
return buf;
}
這種情況下,該如何處理呢?還記得pthread_join()
函數的第二個引數嗎?這個引數就是用來儲存執行緒函數的返回值的:
int main()
{
printf("主執行緒開始執行\n");
pthread_t th;
if (0 != pthread_create(&th, NULL, func, NULL))
{
printf("fail, errno[%d, %s], \n", errno, strerror(errno));
}
char *buf;
pthread_join(th, (void **)&buf);
printf("子執行緒返回值[%s]\n", buf);
printf("主執行緒結束執行\n");
return 0;
}
執行結果:
引入一個新的概念:執行緒分離(detach)和非分離(join)狀態。執行緒的分離狀態決定一個執行緒以什麼樣的方式來終止自己。
在預設情況下執行緒是非分離狀態的,這種情況下,原有的執行緒等待建立的執行緒結束。只有當pthread_join()
函數返回時,建立的執行緒才算終止,才能釋放自己佔用的系統資源。也就是說,通過預設屬性建立的執行緒必須要通過呼叫pthread_join()
函數來釋放執行緒資源,換句話說,非分離狀態的執行緒一定要呼叫pthread_join()
函數。
對於非分離狀態的執行緒,如果不及時呼叫pthread_join()
函數,則會導致資源洩露。下面就通過建立大量非分離狀態的執行緒,但不呼叫pthread_join()
函數來觀察會出現什麼情況。
#include <stdio.h>
#include <pthread.h>
#include <errno.h>
#include <string.h>
/* 執行緒函數,不作任何操作 */
void *func(void *arg)
{
return NULL;
}
int main()
{
int i;
for (i = 1; ; i++)
{
pthread_t th;
if (0 != pthread_create(&th, NULL, func, NULL))
{
printf("fail, errno[%d, %s]\n", errno, strerror(errno));
break;
}
printf("pthread create succeed[%d]\n", i);
}
return 0;
}
執行結果如下:
通過執行結果可以看出,未及時釋放執行緒導致記憶體資源耗盡,進而導致執行緒建立失敗。
但如果在「第 23 行」新增pthread_join(th, NULL);
程式碼,則可以避免這種情況的發生。
函數原型:int pthread_detach(pthread_t thread);
頭 文 件:#include <pthread.h>
功能介紹:從狀態上實現執行緒分離
引數介紹:執行緒識別符號
返 回 值:
在「3.3.5 及時釋放資源」時提到了兩個概念:執行緒分離狀態和執行緒非分離狀態。預設建立的執行緒為非分離狀態,那麼如何設定執行緒為分離狀態呢?有兩種方式:
pthread_detach()
函數。pthread_create()
函數的第二個引數來設定執行緒分離。一般情況下,執行緒終止後,其終止狀態一直保留到其它執行緒呼叫pthread_join()
獲取它的狀態為止(或者程序終止被回收了)。但是執行緒也可以被置為 detach 狀態;如果執行緒被設定為了分離狀態,那麼該執行緒主動與主控執行緒斷開關係。執行緒結束後(不會產生殭屍執行緒),其退出狀態不由其他執行緒獲取,而直接自己自動釋放。
#include <stdio.h>
#include <pthread.h>
#include <errno.h>
#include <string.h>
/* 執行緒函數,不作任何操作 */
void *func(void *arg)
{
return NULL;
}
int main()
{
int i;
for (i = 1; ; i++)
{
pthread_t th;
if (0 != pthread_create(&th, NULL, func, NULL))
{
printf("fail, errno[%d, %s]\n", errno, strerror(errno));
break;
}
pthread_detach(th);//使用 pthread_detach 函數實現執行緒分離
printf("pthread create succeed[%d]\n", i);
}
return 0;
}
注意:不能對一個已經處於 detach 狀態的執行緒呼叫 pthread_join(),這樣的呼叫將返回 EINVAL 錯誤。