線程狀态
在一個線程的生存期内,可以在多種狀态之間轉換。不同作業系統可以實作不同的線程模型,定義許多不同的線程狀态,每個狀
态還可以包含多個子狀态。但大體說來,如下幾種狀态是通用的:
就緒:參與排程,等待被執行。一旦被排程選中,立即開始執行。
運作:占用CPU,正在運作中。
休眠:暫不參與排程,等待特定事件發生。
中止:已經運作完畢,等待回收線程資源(要注意,這個很容易誤解,後面解釋)。
線程環境
線程存在于程序之中。程序内所有全局資源對于内部每個線程均是可見的。
程序内典型全局資源有如下幾種:
代碼區。這意味着目前程序空間内所有可見的函數代碼,對于每個線程來說也是可見的。
靜态存儲區。全局變量。靜态變量。
動态存儲區。也就是堆空間。
線程内典型的局部資源有:
本地棧空間。存放本線程的函數調用棧,函數内部的局部變量等。
部分寄存器變量。例如本線程下一步要執行代碼的指針偏移量。
一個程序發起之後,會首先生成一個預設的線程,通常稱這個線程為主線程。C/C++程式中主線程就是通過main函數進入的線程
。由主線程衍生的線程稱為從線程,從線程也可以有自己的入口函數,作用相當于主線程的main函數。
這個函數由使用者指定。Pthread和winapi中都是通過傳入函數指針實作。在指定線程入口函數時,也可以指定入口函數的參數。
就像main函數有固定的格式要求一樣,線程的入口函數一般也有固定的格式要求,參數通常都是void *類型,傳回類型在
pthread中是void *, winapi中是unsigned int,而且都需要是全局函數。
最常見的線程模型中,除主線程較為特殊之外,其他線程一旦被建立,互相之間就是對等關系 (peer to peer), 不存在隐含的
層次關系。每個程序可以建立的最大線程數由具體實作決定。
為了更好的了解上述概念,下面通過具體代碼來詳細說明。
線程類接口定義
一個線程類無論具體執行什麼任務,其基本的共性無非就是
建立并啟動線程
停止線程
另外還有就是能睡,能等,能分離執行(有點拗口,後面再解釋)。
還有其他的可以繼續加…
将線程的概念加以抽象,可以為其定義如下的類:
檔案 thread.h
#ifndef __THREAD__H_
#define __THREAD__H_
class Thread
{
public:
Thread();
virtual ~Thread();
int start (void * = NULL);
void stop();
void sleep (int);
void detach();
void * wait();
protected:
virtual void * run(void *) = 0;
private:
//這部分win和unix略有不同,先不定義,後面再分别實作。
//順便提一下,我很不習慣寫中文注釋,這裡為了更明白一
//點還是選用中文。
…
};
#endif
Thread::start()函數是線程啟動函數,其輸入參數是無類型指針。
Thread::stop()函數中止目前線程。
Thread::sleep()函數讓目前線程休眠給定時間,機關為秒。
Thread::run()函數是用于實作線程類的線程函數調用。
Thread::detach()和thread::wait()函數涉及的概念略複雜一些。在稍後再做解釋。
Thread類是一個虛基類,派生類可以重載自己的線程函數。下面是一個例子。
示例程式
代碼寫的都不夠精緻,暴力類型轉換比較多,歡迎有閑階級美化,謝過了先。
檔案create.h
#ifndef __CREATOR__H_
#define __CREATOR__H_
#include
#include "thread.h"
class Create: public Thread
{
protected:
void * run(void * param)
{
char * msg = (char*) param;
printf ("%s\n", msg);
//sleep(100); 可以試着取消這行注釋,看看結果有什麼不同。
printf("One day past.\n");
return NULL;
}
};
#endif
然後,實作一個main函數,來看看具體效果:
檔案Genesis.cpp
#include
#include "create.h"
int main(int argc, char** argv)
{
Create monday;
Create tuesday;
printf("At the first God made the heaven and the earth.\n");
monday.start("Naming the light, Day, and the dark, Night, the first day.");
tuesday.start("Gave the arch the name of Heaven, the second day.");
printf("These are the generations of the heaven and the earth.\n");
return 0;
}
編譯運作,程式輸出如下:
At the first God made the heaven and the earth.
These are the generations of the heaven and the earth.
令人驚奇的是,由周一和周二對象建立的子線程似乎并沒有執行!這是為什麼呢?别急,在最後的printf語句之前加上如下語句
:
monday.wait();
tuesday.wait();
重新編譯運作,新的輸出如下:
At the first God made the heaven and the earth.
Naming the light, Day, and the dark, Night, the first day.
One day past.
Gave the arch the name of Heaven, the second day.
One day past.
These are the generations of the heaven and the earth.
為了說明這個問題,需要了解前面沒有解釋的Thread::detach()和Thread::wait()兩個函數的含義。
無論在windows中,還是Posix中,主線程和子線程的預設關系是:
無論子線程執行完畢與否,一旦主線程執行完畢退出,所有子線程執行都會終止。這時整個程序結束或僵死(部分線程保持一種
終止執行但還未銷毀的狀态,而程序必須在其所有線程銷毀後銷毀,這時程序處于僵死狀态),在第一個例子的輸出中,可以看
到子線程還來不及執行完畢,主線程的main()函數就已經執行完畢,進而所有子線程終止。
需要強調的是,線程函數執行完畢退出,或以其他非常方式終止,線程進入終止态(請回顧上面說的線程狀态),但千萬要記住
的是,進入終止态後,為線程配置設定的系統資源并不一定已經釋放,而且可能在系統重新開機之前,一直都不能釋放。終止态的線程,
仍舊作為一個線程實體存在與作業系統中。(這點在win和unix中是一緻的。)而什麼時候銷毀線程,取決于線程屬性。
通常,這種終止方式并非我們所期望的結果,而且一個潛在的問題是未執行完就終止的子線程,除了作為線程實體占用系統資源
之外,其線程函數所擁有的資源(申請的動态記憶體,打開的檔案,打開的網絡端口等)也不一定能釋放。是以,針對這個問題,
主線程和子線程之間通常定義兩種關系:
可會合(joinable)。這種關系下,主線程需要明确執行等待操作。在子線程結束後,主線程的等待操作執行完畢,子線程
和主線程會合。這時主線程繼續執行等待操作之後的下一步操作。主線程必須會合可會合的子線程,Thread類中,這個操作通過
在主線程的線程函數内部調用子線程對象的wait()函數實作。這也就是上面加上三個wait()調用後顯示正确的原因。必須強調的
是,即使子線程能夠在主線程之前執行完畢,進入終止态,也必需顯示執行會合操作,否則,系統永遠不會主動銷毀線程,配置設定
給該線程的系統資源(線程id或句柄,線程管理相關的系統資源)也永遠不會釋放。
相分離(detached)。顧名思義,這表示子線程無需和主線程會合,也就是相分離的。這種情況下,子線程一旦進入終止态
,系統立即銷毀線程,回收資源。無需在主線程内調用wait()實作會合。Thread類中,調用detach()使線程進入detached狀态。
這種方式常用線上程數較多的情況,有時讓主線程逐個等待子線程結束,或者讓主線程安排每個子線程結束的等待順序,是很困
難或者不可能的。是以在并發子線程較多的情況下,這種方式也會經常使用。
預設情況下,建立的線程都是可會合的。可會合的線程可以通過調用detach()方法變成相分離的線程。但反向則不行。
UNIX實作
檔案 thread.h
#ifndef __THREAD__H_
#define __THREAD__H_
class Thread
{
public:
Thread();
virtual ~Thread();
int start (void * = NULL);
void stop();
void sleep (int);
void detach();
void * wait();
protected:
virtual void * run(void *) = 0;
private:
pthread_t handle;
bool started;
bool detached;
void * threadFuncParam;
friend void * threadFunc(void *);
};
//pthread中線程函數必須是一個全局函數,為了解決這個問題
//将其聲明為靜态,以防止此檔案之外的代碼直接調用這個函數。
//此處實作采用了稱為Virtual friend function idiom 的方法。
Static void * threadFunc(void *);
#endif
檔案thread.cpp
#include
#include
#include “thread.h”
static void * threadFunc (void * threadObject)
{
Thread * thread = (Thread *) threadObject;
return thread->run(thread->threadFuncParam);
}
Thread::Thread()
{
started = detached = false;
}
Thread::~Thread()
{
stop();
}
bool Thread::start(void * param)
{
pthread_attr_t attributes;
pthread_attr_init(&attributes);
if (detached)
{
pthread_attr_setdetachstate(&attributes, PTHREAD_CREATE_DETACHED);
}
threadFuncParam = param;
if (pthread_create(&handle, &attributes, threadFunc, this) == 0)
{
started = true;
}
pthread_attr_destroy(&attribute);
}
void Thread::detach()
{
if (started && !detached)
{
pthread_detach(handle);
}
detached = true;
}
void * Thread::wait()
{
void * status = NULL;
if (started && !detached)
{
pthread_join(handle, &status);
}
return status;
}
void Thread::stop()
{
if (started && !detached)
{
pthread_cancel(handle);
pthread_detach(handle);
detached = true;
}
}
void Thread::sleep(unsigned int milliSeconds)
{
timeval timeout = { milliSeconds/1000, millisecond%1000};
select(0, NULL, NULL, NULL, &timeout);
}
Windows實作
檔案thread.h
#ifndef _THREAD_SPECIFICAL_H__
#define _THREAD_SPECIFICAL_H__
#include
static unsigned int __stdcall threadFunction(void *);
class Thread {
friend unsigned int __stdcall threadFunction(void *);
public:
Thread();
virtual ~Thread();
int start(void * = NULL);
void * wait();
void stop();
void detach();
static void sleep(unsigned int);
protected:
virtual void * run(void *) = 0;
private:
HANDLE threadHandle;
bool started;
bool detached;
void * param;
unsigned int threadID;
};
#endif
檔案thread.cpp
#include "stdafx.h"
#include
#include "thread.h"
unsigned int __stdcall threadFunction(void * object)
{
Thread * thread = (Thread *) object;
return (unsigned int ) thread->run(thread->param);
}
Thread::Thread()
{
started = false;
detached = false;
}
Thread::~Thread()
{
stop();
}
int Thread::start(void* pra)
{
if (!started)
{
param = pra;
if (threadHandle = (HANDLE)_beginthreadex(NULL, 0, threadFunction, this, 0, &threadID))
{
if (detached)
{
CloseHandle(threadHandle);
}
started = true;
}
}
return started;
}
//wait for current thread to end.
void * Thread::wait()
{
DWORD status = (DWORD) NULL;
if (started && !detached)
{
WaitForSingleObject(threadHandle, INFINITE);
GetExitCodeThread(threadHandle, &status);
CloseHandle(threadHandle);
detached = true;
}
return (void *)status;
}
void Thread::detach()
{
if (started && !detached)
{
CloseHandle(threadHandle);
}
detached = true;
}
void Thread::stop()
{
if (started && !detached)
{
TerminateThread(threadHandle, 0);
//Closing a thread handle does not terminate
//the associated thread.
//To remove a thread object, you must terminate the thread,
//then close all handles to the thread.
//The thread object remains in the system until
//the thread has terminated and all handles to it have been
//closed through a call to CloseHandle
CloseHandle(threadHandle);
detached = true;
}
}
void Thread::sleep(unsigned int delay)
{
::Sleep(delay);
}
小結
本節的主要目的是幫助入門者建立基本的線程概念,以此為基礎,抽象出一個最小接口的通用線程類。在示例程式部分,初學者
可以體會到并行和串行程式執行的差異。有興趣的話,大家可以在現有線程類的基礎上,做進一步的擴充和嘗試。如果覺得對線
程的概念需要進一步細化,大家可以進一步擴充和完善現有Thread類。
想更進一步了解的話,一個建議是,可以去看看其他語言,其他平台的線程庫中,線程類抽象了哪些概念。比如Java, perl等跨
平台語言中是如何定義的,微軟從winapi到dotnet中是如何支援多線程的,其線程類是如何定義的。這樣有助于更好的了解線程
的模型和基礎概念。
另外,也鼓勵大家多動手寫寫代碼,在此基礎上嘗試寫一些代碼,也會有助于更好的了解多線程程式的特點。比如,先開始的線
程不一定先結束。線程的執行可能會交替進行。把printf替換為cout可能會有新的發現,等等。
每個子線程一旦被建立,就被賦予了自己的生命。管理不好的話,一隻特例獨行的豬是非常讓人頭痛的。
對于初學者而言,編寫多線程程式可能會遇到很多令人手足無措的bug。往往還沒到考慮效率,避免死鎖等階段就問題百出,而
且很難了解和調試。這是非常正常的,請不要氣餒,後續文章會盡量解釋各種常見問題的原因,引導大家避免常見錯誤。目前能
想到入門階段常遇到的問題是:
記憶體洩漏,系統資源洩漏。
程式執行結果混亂,但是在某些點插入sleep語句後結果又正确了。
程式crash, 但移除或添加部分無關語句後,整個程式正常運作(假相)。
多線程程式執行結果完全不合邏輯,出于預期。
本文至此,如果自己動手改改,試一些例子,對多線程程式應該多少有一些感性認識了。剛開始隻要把基本概念弄懂了,後面可
以一步一步搭建出很複雜的類。不過剛開始不要貪多,否則會欲速則不達,越弄越糊塗。