線程
多線程(1)
線程的基本概念
線程是進程的進一步細化,進程是系統分配資源的基本單位,而線程cpu調度的基本單位。
進程就像是包工頭,獲取到系統資源,然後再分配給它裏面線程。
操作系統用於管理線程有個數據結構tcb,用來描述和管理線程。
而在Linux下稱之爲輕量級進程。和進程用同一塊pcb來表示。
那麼一個進程至少有一個線程。
線程共享進程一些數據,也有自己的一些數據。
線程自己擁有:
- 線程ID
- 一組寄存器
- 棧
- errno
- 信號屏蔽字
- 調度優先級
一個進程下多個線程共享:
- 同一地址空間,因此Text Segment 、Data Segment都是共享的,如果定義一個函數,在進程內的各個線程都可以調用,如果定義一個全局變量,在各個線程都可以訪問到。
- 文件描述符
- 每種信號的處理方式(SIG_IGN、SIG_DFL或者自定義捕捉函數)
- 當前目錄
- 用戶ID和組ID
線程的優缺點:
- 線程的優點
- 創建一個新線程的代價要比創建一個新進程小得多
- 與進程之間的切換相比,線程之間的切換需要操作系統做的工作要少很多
- 線程佔用的資源要比進程少很多
- 能充分利用多處理器的可並行數量
- 在等待慢速I/O操作結束的同時,程序可執行其他的計算任務
- 計算密集型應用,爲了能在多處理器系統上運行,將計算分解到多個線程中實現
- I/O密集型應用,爲了提高性能,將I/O操作重疊。線程可以同時等待不同的I/O操作。
- 線程的缺點
- 性能損失
一個很少被外部事件阻塞的計算密集型線程往往無法與共它線程共享同一個處理器。如果計算密集型線程的數量比可用的處理器多,那麼可能會有較大的性能損失,這裏的性能損失指的是增加了額外的同步和調度開銷,而可用的資源不變。 - 健壯性降低
編寫多線程需要更全面更深入的考慮,在一個多線程程序裏,因時間分配上的細微偏差或者因共享了不該共享的變量而造成不良影響的可能性是很大的,換句話說線程之間是缺乏保護的。 - 缺乏訪問控制
進程是訪問控制的基本粒度,在一個線程中調用某些OS函數會對整個進程造成影響。 - 編程難度提高編寫與調試一個多線程程序比單線程程序困難得多
- 性能損失
線程控制
POSIX線程庫
與線程有關的函數構成了一個完整的系列,絕大多數函數的名字都是以“pthread_”打頭的
要使用這些函數庫,要通過引入頭文<pthread.h>
鏈接這些線程函數庫時要使用編譯器命令的“-lpthread”選項
POSIX在用戶空間內
創建線程:
功能:創建一個新的線程
原型
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void*), void *arg);
參數
thread:返回線程ID
attr:設置線程的屬性,attr爲NULL表示使用默認屬性
start_routine:是個函數地址,線程啓動後要執行的函數
arg:傳給線程啓動函數的參數
返回值:成功返回0;失敗返回錯誤碼
錯誤檢查:
傳統的一些函數是,成功返回0,失敗返回-1,並且對全局變量errno賦值以指示錯誤。
pthreads函數出錯時不會設置全局變量errno(而大部分其他POSIX函數會這樣做)。而是將錯誤代碼通過返回值返回。
pthreads同樣也提供了線程內的errno變量,以支持其它使用errno的代碼。對於pthreads函數的錯誤,建議通過返回值業判定,因爲讀取返回值要比讀取線程內的errno變量的開銷更小。
#include <stdio.h>
#include <pthread.h>
void* Fun(void* arg)
{
while(1)
{
printf("I am pthread! id = %lu\n", pthread_self());//獲取用戶線程ID
sleep(1);
}
}
int main()
{
pthread_t pid;
int ret;
if((ret = pthread_create(&pid, NULL, Fun, NULL)) < 0)
{
perror("pthread_create");
return 1;
}
while(1)
{
printf("I am main pthread!\n");
sleep(2);
}
return 0;
}
進程ID和線程ID
在Linux中,目前的線程實現是Native POSIX Thread Libaray,簡稱NPTL。在這種實現下,線程又被稱爲輕量級進程(Light Weighted Process),每一個用戶態的線程,在內核中都對應一個調度實體,也擁有自己的進程描述符(task_struct結構體)。
沒有線程之前,一個進程對應內核裏的一個進程描述符,對應一個進程ID。但是引入線程概念之後,情況發生了變化,一個用戶進程下管轄N個用戶態線程,每個線程作爲一個獨立的調度實體在內核態都有自己的進程描述符,進程和內核的描述符一下子就變成了1:N關係,POSIX標準又要求進程內的所有線程調用getpid函數時返回相同的進程ID。
Linux內核引入了線程組的概念。
struct task_struct {
...
pid_t pid;
pid_t tgid;
...
struct task_struct *group_leader;
...
struct list_head thread_group;
...
};
多線程的進程,又被稱爲線程組,線程組內的每一個線程在內核之中都存在一個進程描述符(task_struct)與之對應。進程描述符結構體中的pid,表面上看對應的是進程ID,其實不然,它對應的是線程ID;進程描述符中的tgid,含義是Thread Group ID,該值對應的是用戶層面的進ID。
ps命令中的-L選項,會顯示如下信息:
- LWP:線程ID,既gettid()系統調用的返回值。
- NLWP:線程組內線程的個數
上面的a.out是多線程的,進程ID : 2540 一個進程擁有兩個線程,ID分別爲2540 2541
Linux提供了gettid系統調用來返回其線程ID,可是glibc並沒有將該系統調用封裝起來,在開放接口來共程序員使用。如果確實需要獲得線程ID,可以採用如下方法:
#include <sys/syscall.h>
pid_t tid;
tid = syscall(SYS_gettid);
a.out進程的ID爲2540,下面有一個線程的ID也是2540,這不是巧合。線程組內的第一個線程,在用戶態被稱爲主線程(mainthread),在內核中被稱爲group leader,內核在創建第一個線程時,會將線程組的ID的值設置成第一個線程的線程ID,group_leader指針則指向自身,既主線程的進程描述符。所以線程組內存在一個線程ID等於進程ID,而該線程即爲線程組的主線程。
/* 線程組ID等於線程ID,group_leader指向自身 */
p->tgid = p->pid;
p->group_leader = p;
INIT_LIST_HEAD(&p->thread_group);
至於線程組其他線程的ID則有內核負責分配,其線程組ID總是和主線程的線程組ID一致,無論是主線程直接創建線程,還是創建出來的線程再次創建線程,都是這樣。
if ( clone_flags & CLONE_THREAD )
p->tgid = current->tgid;
if ( clone_flags & CLONE_THREAD ) {
P->group_lead = current->group_leader;
list_add_tail_rcu(&p->thread_group, &p->group_leader->thread_group);
}
強調一點,線程和進程不一樣,進程有父進程的概念,但在線程組裏面,所有的線程都是對等關係。
線程ID及進程地址空間佈局
pthread_ create函數會產生一個線程ID,存放在第一個參數指向的地址中。該線程ID和前面說的線程ID不是一回事。
前面講的線程ID屬於進程調度的範疇。因爲線程是輕量級進程,是操作系統調度器的最小單位,所以需要一個數值來唯一表示該線程。
pthread_ create函數產生並標記在第一個參數指向的地址中的線程ID中,屬於NPTL線程庫
的範疇。線程庫的後續操作,就是根據該線程ID來操作線程的。線程庫NPTL提供了
pthread_ self函數,可以獲得線程自身的ID:
pthread_t pthread_self(void);
pthread_t到底是什麼類型呢?取決於實現。對於Linux目前實現的NPTL實現而言,pthread_t類型的線程ID,本質就是一個進程地址空間上的一個地址。
線程終止
如果需要只終止某個線程而不終止整個進程,可以有三種方法:
1. 從線程函數return。這種方法對主線程不適用,從main函數return相當於調用exit。
2. 線程可以調用pthread_ exit終止自己。
3. 一個線程可以調用pthread_ cancel終止同一進程中的另一個線程。
pthread_exit函數
功能:線程終止
原型
void pthread_exit(void *value_ptr);
參數
value_ptr:value_ptr不要指向一個局部變量。
返回值:無返回值,跟進程一樣,線程結束的時候無法返回到它的調用者(自身)
需要注意,pthread_exit或者return返回的指針所指向的內存單元必須是全局的或者是用malloc分配的,不能在線程函數的棧上分配,因爲當其它線程得到這個返回指針時線程函數已經退出了。
pthread_cancel函數
功能:取消一個執行中的線程
原型
int pthread_cancel(pthread_t thread);
參數
thread:線程ID
返回值:成功返回0;失敗返回錯誤碼
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
void* prun(void* arg)
{
int i = 3;
while(i--)
{
printf("I am pthread\n");
sleep(1);
}
*(int*)arg = 1;
//pthread_exit(NULL);
//return arg;
pthread_cancel(pthread_self());
}
int main()
{
pthread_t t;
int ret = 0;
if(pthread_create(&t, NULL, prun,(void*)&ret) < 0)
{
perror("pthread");
return 1;
}
while(1)
{
printf("I am main thread! ret = %d\n", ret);
sleep(1);
}
return 0;
}
線程等待與分離
線程等待
爲什麼需要線程等待?
已經退出的線程,其空間沒有被釋放,仍然在進程的地址空間內。
創建新的線程不會複用剛纔退出線程的地址空間。
功能:等待線程結束
原型
int pthread_join(pthread_t thread, void **value_ptr);
參數
thread:線程ID
value_ptr:它指向一個指針,後者指向線程的返回值
返回值:成功返回0;失敗返回錯誤碼
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
void* run1(void* arg)
{
int *p =(int*)malloc(sizeof(int));
printf("I am run1, return 1!\n");
*p = 1;
return (void*)p;
}
void* run2(void* arg)
{
int *p =(int*)malloc(sizeof(int));
printf("I am run2, pthread_exit(2)!\n");
*p = 2;
pthread_exit((void *)p);
}
void* run3(void* arg)
{
while(1)
printf("I am run3, pthread_cancel()!\n");
//pthread_cancel(pthread_self());
}
int main()
{
pthread_t tid;
void * ret;
if(!pthread_create(&tid, NULL, run1, NULL))
{
pthread_join(tid, &ret);
printf("I am main pthread ret = %d\n", *(int*)ret);
free(ret);
}
if(!pthread_create(&tid, NULL, run2, NULL))
{
pthread_join(tid, &ret);
printf("run2 return %d\n", *(int*)ret);
free(ret);
}
if(!pthread_create(&tid, NULL, run3, NULL))
{
pthread_cancel(tid);
pthread_join(tid, &ret);
if((ret == PTHREAD_CANCELED) )
printf("run3 return cancel\n");
else
printf("errno\n");
// if(ret == (void*)0)
// {
// printf("(void*)0\n");
// }
}
return 0;
}
調用該函數的線程將掛起等待,直到id爲thread的線程終止。thread線程以不同的方法終止,通過pthread_join得到的終止狀態是不同的,總結如下:
- 如果thread線程通過return返回,value_ ptr所指向的單元裏存放的是thread線程函數的返回值。
- 如果thread線程被別的線程調用pthread_ cancel異常終掉,value_ ptr所指向的單元裏存放的是常數PTHREAD_ CANCELED。
- 如果thread線程是自己調用pthreadexit終止的,valueptr所指向的單元存放的是傳給pthread_exit的參數。
- 如果對thread線程的終止狀態不感興趣,可以傳NULL給value_ ptr參數。
分離線程
默認情況下,新創建的線程是joinable的,線程退出後,需要對其進行pthread_join操作,否則無法釋放資源,從而造成系統泄漏。
如果不關心線程的返回值,join是一種負擔,這個時候,我們可以告訴系統,當線程退出時,自動釋放線程資源。
int pthread_detach(pthread_t thread);
可以是線程組內其他線程對目標線程進行分離,也可以是線程自己分離:
pthread_detach(pthread_self());
但是如果分離出來的線程觸發異常,整個進程會掛掉。
#include <stdio.h>
#include <pthread.h>
void* Run(void* arg)
{
while(1)
{
printf("I am detach pthread!\n");
usleep(1000);
}
}
int main()
{
pthread_t tid;
if(pthread_create(&tid, NULL, Run, NULL) < 0)
{
perror("pthread_create");
return 1;
}
if(pthread_detach(tid) < 0)
{
perror("pthread_detach()");
return 2;
}
sleep(1);
if(pthread_join(tid, NULL) == 0)
{
printf("join success!\n");
}
else
{
printf("join failed!\n");
}
return 0;
}