線程概念與線程創建

什麼是線程?爲什麼要有多線程?

       一家公司需要生產某種產品,然後爲生產這種產品提供了各種原材料和幾層樓的資源。而這件產品是有很多個零件組成的,各個零件需要的材料可能是不同的,即,有些零件之間的製造是不相互影響的。現在要生產一種產品,由A、B兩種零件組成。公司分配了1、2、3這三層樓(2樓是用於生產的該產品的各種器械)用於生產該產品。假設加工零件A是將材料都準備好了放到2樓的機器裏邊,然後等上很長一段時間。在生產的時候可能產品負責人可能就是分派一部分人在1樓完成加工零件A的前期準備,然後再分配一部分人在3樓加工零件B,最後再將所有的零件組合起來。負責人肯定不會讓大家一塊先加工零件A,然後大家都在那閒等着機器操作,等着把零件A加工完成纔開始零件B的加工,因爲這樣效率太低了。

       同樣的,如果將執行一個進程比作生產一種產品,公司就好比操作系統,產品的原材料和提供的生產產地就是分配給進程的資源。爲了讓進程更合理的進行,將進程的分成多個執行流(加工多個零件),各個執行流相當於就是不同的函數。而這樣的執行流其實就是線程,一個執行流就是一個線程。這些執行流同屬於一個進程(同是一個產品的一部分),且共享地址空間(都在那分配的那幾層樓裏)和共享資源(都要使用2樓的器械)。實際的進程程比這個稍微要複雜一點,線程之間共享的東西遠不止這些。同一進程下的線程之間共享了哪些資源呢?

    1)、共享同一地址空間,因此Text Segment(代碼段) 和 Data Segment(數據段)都是共享的,如果定義一個函數,在各線程都可以調用,如果定義一個全局變量,在各線程都可以訪問到。

    2)、文件描述符表

    3)、每種信號的處理方式

    4)、當前工作目錄

    5)、用戶id 和 組id

但是,就像每個零件的原材料可能不同,每個線程都有自己獨特的東西,每個線程都有自己的線程id、一組寄存器(用於保存上下文信息的)、棧、errno、信號屏蔽字、調度優先級。

       總結一下,其實線程是進程的一部分,描述指令流執行狀態。它是進程中的指令執行流的最小單位,是CPU調度的基本單位。

 

線程標識:

       和每個進程都有一個進程ID一樣,每個線程也有自己的ID。進程ID是一個非負整數,用pid_t來表示,線程ID用pthread_t 數據類型表示。由於在不同的平臺,用於表示pthread_t的結構是不同的。而每個平臺都有着自己的線程庫,並在庫裏邊實現了一些公有的接口。當我們在對線程ID進行一些操作時,爲了程序的可移植性,我們需要利用這些公有的接口來實現。

    獲取自身的線程ID

    #include <pthread.h>

    pthread_t  pthread_self ( void )

 

    比較兩個線程ID是否相等

    #include <pthread.h>

    int pthread_equal( pthread_t tid1,pthread_t tid2 )

    相等返回非0值,不相等返回0。

 

線程創建:

當我們創建出一個進程時,該進程只有一個執行流,但是在進程的執行過程中,我們可以通過pthread_create函數來創建其他執行流,也就是創建其他線程,跟使用fork()創建一個新進程不一樣,創建出來的新線程與原來的線程是沒有父子關係的,他們之間的關係是平等的,被調用的順序也是不確定的。

 

int pthread_create(pthread_t* thread, const pthread_attr*  attr,  void*  (*start_routine)(void*), void* arg)

參數:thread:返回線程ID

          attr:設置線程的屬性,attr爲NULL表示使用默認屬性

          start_routine:是一個函數指針,創建一個線程是要爲其安排執行的任務(函數)

          arg:傳給線程啓動函數的參數(即執行第三個參數指向的函數時所需要的參數)

          attr:設置線程的屬性,attr爲NULL表⽰示使⽤用默認屬性

返回值:成功返回0;失敗返回錯誤碼

 

說明:

傳統的一些函數是成功返回0,失敗返回-1,並且對全局變量errno賦值以指示錯誤。

pthreads函數出錯時不會設置全局變量errno(大部分其他POSIX函數會這樣做)而是通過返回值將錯誤碼返回

pthreads同樣也提供了線程內的全局變量errno,以支持其他使用errno的代碼。

對於pthreads函數的錯誤,建議通過返回值來判定,因爲讀取返回值要比讀取線程內的errno變量的開銷要小。

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
void* routine(void* arg)
{
    while(1)
    {
        printf("I'm new thread,pid is:%u tid is:%u\n",getpid(),(unsigned int)pthread_self());
        sleep(1);
    }
    return (void*) 1;
}
int main()
{
    pthread_t tid;
    int ret = pthread_create(&tid,NULL,routine,NULL);
    if(ret != 0)
    {
        fprintf(stderr,"pthread_create err:%s\n",strerror(ret));
        exit(EXIT_FAILURE);
    }
    while(1)
    {
        sleep(1);
        printf("I'm main thread,pid is:%u tid is:%u\n",getpid(),(unsigned int)pthread_self());
    }
    return 0;
}

 

該進程內的每一個線程的pid都是一樣的,由此也可以證明這些線程同屬於一個進程。

在此解釋一下LWP與線程ID的區別

這塊比較繞,可能不太好理解。要有耐心~

       線程分爲用戶級線程和內核級線程,而我們創建的線程都是用戶級線程。在Linux中,是沒有真正意義上的線程的,線程都是通過pthread庫模擬進程而實現出來的,而在CPU看來,就沒有進程線程之分,都是一個個的PCB,所以線程又被稱爲輕量級進程。既然被認爲是PCB,那麼就有進程ID,而實際上LWP就是線程在被CPU認爲是進程併爲其分配的pid,這個pid在整個內核中都是唯一的。而我們所說的線程id只是相對於用戶級來說的,線程id只是在用戶級唯一。事實上,線程id的實質就是在進程內部爲其分配的地址空間的地址。

       多線程的進程,又被稱爲線程組,線程組內的每一個線程在內核之中都存在一個進程描述符(task_struct)與之對應。進程描述符結構體中的pid,表面上看對應的是進程ID,其實不然,它對應的是線程ID(也就是上邊所說的LWP);進程描述符中的tgid,含義是Thread Group ID(即進程組ID),該值對應的是用戶層面的進程ID。

       線程組內的第一個線程,在用戶態被稱爲主線程(main thread),在內核中被稱爲group leader,內核在創建第一個線程時,會將線程組的ID的值設置成第一個線程的線程ID,group_leader指針則指向自身,既主線程的進程描述符。所以線程組內存在一個線程ID等於進程ID,而該線程即爲線程組的主線程。

 

線程的終止:

在進程中的任一線程調用了exit,_Exit、_exit或者主執行流(第一個執行流)return,都會導致整個進程終止。與此類似,如果進程組中的某一線程收到一個默認處理動作是終止進程的信號,整個進程也會終止。

首先來驗證一下在一個新線程中調用exit會不會使整個進程都退出。

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>
void* routine(void* arg)
{
    int count = 3;
    while(count--)
    {
        printf("I'm new thread,pid is:%u tid is:%u\n",getpid(),(unsigned int)pthread_self());
        sleep(1);
    }
    exit(0);
}
int main()
{
    pthread_t tid;
    int ret = pthread_create(&tid,NULL,routine,NULL);
    if(ret != 0)
    {
        fprintf(stderr,"pthread_create err:%s\n",strerror(ret));
        exit(EXIT_FAILURE);
    }
    while(1)
    {
        sleep(1);
        printf("I'm main thread,pid is:%u tid is:%u\n",getpid(),(unsigned int)pthread_self());
    }
    return 0;
}

在此段程序中,新線程循環3次後調用exit退出,而主線程(第一個線程)一直在死循環。

當新線程循環完3次之後,整個進程都退出了。其他的就不一一驗證了。

如果需要只終止某個線程而不終止整個進程,可以有三種方法:

        1. 從線程函數return。這種方法對主線程不適用,從main函數return相當於調用exit。

        2. 線程可以調用pthread_ exit終止自己。

        3. 一個線程可以調用pthread_ cancel終止同一進程中的另一個線程。

 

pthread_exit函數  ----->線程終止

void pthread_exit(void *value_ptr);

參數:value_ptr是一個無類型的指針,不能向一個局部變量。進程中的其他線程可以通過pthread_jion函數來訪問到該指針。

返回值:無返回值,跟進程一樣,線程結束的時候無法返回到它的調用者(自身)

需要注意,pthread_exit或者return返回的指針所指向的內存單元必須是全局的或者是用malloc分配的(malloc分配空間是在堆上分配的),不能在線程函數的棧上分配,因爲當其它線程得到這個返回指針時線程函數已經退出了。

 

pthread_cancel函數 ----->取消一個執行中的線程

原型:int pthread_cancel(pthread_t thread);

參數:thread --- 線程ID

返回值:成功返回0;失敗返回錯誤碼。

pthrea_cancell函數會使得進程thread表現的如同調用PTRHEAD_CANCELED的pthread_exit函數,但是進程可以選擇忽略取消方式或者控制取消方式。一個線程調用pthread_cancel函數去取消另一個線程,調用線程只是相當於只是提出請求,並不會等待線程被取消。

 

看一看使用pthread_exit函數退出會不會導致整個進程的退出。

將上邊的調用exit函數改成調用pthread_exit函數

重新執行程序,發現,新線程循環3次之後就退出了,而主線程依然在執行,說明,調用pthread_exit函數可以使線程終止而進程不終止

驗證一下使用pthread_cancel來終止一個正在運行的線程。

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>
void* routine(void* arg)
{
    while(1)
    {
        printf("I'm new thread,pid is:%u tid is:%u\n",getpid(),(unsigned int)pthread_self());
        sleep(1);
    }
}
int main()
{
    pthread_t tid;
    int ret = pthread_create(&tid,NULL,routine,NULL);
    if(ret != 0)
    {
        fprintf(stderr,"pthread_create err:%s\n",strerror(ret));
        exit(EXIT_FAILURE);
    }
    sleep(3);
    pthread_cancel(tid);
    while(1)
    {
        sleep(1);
        printf("I'm main thread,pid is:%u tid is:%u\n",getpid(),(unsigned int)pthread_self());
    }
    return 0;
}

這段程序主線程先創建一個新線程,然後隔了3秒便使用pthread_cancel函數將其終止了,然後主線程在死循環。

新線程被終止,主線程依然在執行,說明進程還沒有結束。

 

線程等待與分離

在多進程中父進程可以調用wait函數來獲取子進程的退出信息,在多線程中,一個線程退出了也需要其他進程來等待,爲什麼線程也需要等待呢?

已經退出的線程,其空間還沒有退出,仍然在進程的地址空間內。當再創建新的線程也不會複用剛剛纔退出線程的空間。這就造成進程內部資源的浪費,相當於殭屍進程中的資源泄漏。同一進程內部的其他線程可以通過pthread_jion函數獲取線程的退出信息。

int pthread_jion(pthread thread,void** value_ptr);

       參數:thread爲需要進行等待的線程id

                value_ptr是用於獲取線程的退出信息的。

       返回值:等待成功返回0,失敗返回錯誤碼。

說明:調用pthread_jion函數的線程會一直阻塞,直到指定的線程調用pthread_exit退出或者從啓動例程中返回或者被取消。如果是從啓動例程中返回,那麼value_ptr將包含返回碼。如果是被取消了,那麼,value_ptr指定的內存單元就會被置爲PTHREAD_CANCELED(#define  PTHREAD_CANCELED  (void*  -1))。如果只是想要在指定線程結束後再執行,並不想知道線程的具體退出信息,調用的時候可以將第二個參數value_ptr設置爲NULL。

 

線程的分離

       默認情況下,新創建的線程是可結合的(jionable),需要對其進行pthread_jion操作,否則無法釋放資源,從而造成系統泄露。如果我們不關心線程的返回值,jion就變成了一種負擔,爲了減少這種負擔,當我們認爲線程的返回值不重要的時候,可以提前告訴系統,當線程退出時,自動釋放資源。這就是線程的分離。

線程的分離是通過函數thread_detach實現的。

int pthread_detach(pthread_t  thread);

       參數:thread是要進行分離的線程的id。

       返回值:成功返回0,失敗返回錯誤碼。

線程分離可以是被動的(被同一個進程裏邊的其他線程分離),也可以是主動的(自己將自己分離)。pthread_detach ( pthread_self() );

jionable和分離是相互矛盾的,一個線程不能既是jionable的又是分離的。

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

pthread_t ptid;

void* thread_run(void* arg)
{
	pthread_detach(pthread_self());
	printf("%s\n",(char*)arg);
	return (void*)0;
}

int main()
{
	int ret;
	ret = pthread_create(&ptid,NULL,thread_run,"new thread is running");
	if(ret != 0)
	{
		printf("can't create a thread\n");
		exit(0);
	}

	sleep(1);
	ret = pthread_join(ptid,NULL);
	if(ret == 0)
		printf("thread wait success\n");
	else
		printf("thread wait failure\n");
	return 0;
	}

上邊這個例子在主線程裏邊創建一個新線程並使用pthread_join函數等待新線程,然後,讓新線程自我分離。如果等待不成功,說明等待與分離是相互矛盾的。

結果表明等待失敗,分離後的線程是不能被等待的,如果等待,一定會等待失敗。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章