日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

Unix环境高级编程学习笔记(七) 多线程

發布時間:2023/12/2 编程问答 29 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Unix环境高级编程学习笔记(七) 多线程 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.



線程概述

線程(thread)技術早在60年代就被提出,但真正應用多線程到操作系統中去,是在80年代中期,solaris是這方面的佼佼者。傳統的Unix也支持線程的概念,但是在一個進程(process)中只允許有一個線程,這樣多線程就意味著多進程。現在,多線程技術已經被許多操作系統所支持,包括Windows/NT,當然,也包括Linux。  

為什么有了進程的概念后,還要再引入線程呢?使用多線程到底有哪些好處?什么的系統應該選用多線程?我們首先必須回答這些問題。  

使用多線程的理由之一是和進程相比,它是一種非常"節儉"的多任務操作方式。我們知道,在Linux系統下,啟動一個新的進程必須分配給它獨立的地址空間,建立眾多的數據表來維護它的代碼段、堆棧段和數據段,這是一種"昂貴"的多任務工作方式。而運行于一個進程中的多個線程,它們彼此之間使用相同的地址空間,共享大部分數據,啟動一個線程所花費的空間遠遠小于啟動一個進程所花費的空間,而且,線程間彼此切換所需的時間也遠遠小于進程間切換所需要的時間。據統計,總的說來,一個進程的開銷大約是一個線程開銷的30倍左右,當然,在具體的系統上,這個數據可能會有較大的區別。  

使用多線程的理由之二是線程間方便的通信機制。對不同進程來說,它們具有獨立的數據空間,要進行數據的傳遞只能通過通信的方式進行,這種方式不僅費時,而且很不方便。線程則不然,由于同一進程下的線程之間共享數據空間,所以一個線程的數據可以直接為其它線程所用,這不僅快捷,而且方便。當然,數據的共享也帶來其他一些問題,有的變量不能同時被兩個線程所修改,有的子程序中聲明為static的數據更有可能給多線程程序帶來災難性的打擊,這些正是編寫多線程程序時最需要注意的地方。  

除了以上所說的優點外,不和進程比較,多線程程序作為一種多任務、并發的工作方式,當然有以下的優點:  

1) 提高應用程序響應。這對圖形界面的程序尤其有意義,當一個操作耗時很長時,整個系統都會等待這個操作,此時程序不會響應鍵盤、鼠標、菜單的操作,而使用多線程技術,將耗時長的操作(time consuming)置于一個新的線程,可以避免這種尷尬的情況。  

2) 使多CPU系統更加有效。操作系統會保證當線程數不大于CPU數目時,不同的線程運行于不同的CPU上。  

3) 改善程序結構。一個既長又復雜的進程可以考慮分為多個線程,成為幾個獨立或半獨立的運行部分,這樣的程序會利于理解和修改。

一個線程所包含的信息呈現出了它在一個進程中的執行環境,它們包括線程ID,線程棧,時刻優先級和策略(a scheduling priority and policy),信號屏蔽字,error變量以及線程相關的特定數據(線程私有數據)。在一個進程中幾乎所有的東西都是可以共享的,包括代碼段,全局變量以及堆、棧,還包括文件描述符等。一個線程的線程ID是用于在進程中唯一確定的標識,和進程ID不同,它只有在該線程所在的進程中才有意義。

線程的創建

首先我們來看一下調用函數:

[cpp]?view plaincopy
  • int?pthread_create(pthread_t?*restrict?tidp,?const?pthread_attr_t?*restrict?attr,???
  • ????????void?*(*start_rtn)(void),?void?*restrict?arg);??

  • 它的第一個參數是O類型的,用于獲取新創建的線程ID,attr是線程屬性,默認時可賦值為空,start_rtn是一個函數指針,線程創建成功后,該函數將作為線程的入口函數開始運行,arg是傳遞給該線程函數的參數。

    新創建的線程有權訪問它所在進程的地址空間,它將繼承父線程的浮點環境(floating-point environment)以及信號屏蔽字,不過,已經處于阻塞隊列中的信號將被清空。

    新創建的線程默認將以分離模式運行,也就是說,當線程結束時,系統將自動回收其資源,并扔掉其結束狀態。當然,我們也可以通過設置其線程屬性來使線程以非分離模式運行,在此模式下,線程結束時,必須由其他線程調用join函數來釋放其資源并獲取其結束狀態。

    我們來看看線程屬性的設置方式,以下兩個函數用于初始化以及銷毀線程屬性結構體(并非釋放內存):

    [cpp]?view plaincopy
  • int?pthread_attr_init(pthread_attr_t?*attr);//?將屬性結構體初始化為默認值:??
  • int?pthread_attr_destroy(pthread_attr_t?*attr);??

  • 這樣,在初始化后,屬性結構體就被初始化為默認值,下面的函數可用于獲取以及設置線程的分離屬性:

    [cpp]?view plaincopy
  • int?pthread_attr_getdetachstate(const?pthread_attr_t?*restrict?attr,?int?*detachstate);??
  • int?pthread_attr_setdetachstate(pthread_attr_t?*attr,?int?detachstate);??

  • 在設置時,第二個參數分離狀態只能是這兩種常量:PTHREAD_CREATE_DETACHED(分離模式)和PTHREAD_CREATE_JOINABLE。

    當然,我們也可以在運行期間修改線程的分離屬性,以下函數可以在線程運行時將其改變為分離模式:

    [cpp]?view plaincopy
  • int?pthread_detach(pthread_t?thread);??

  • 線程屬性不止用于分離屬性,我們知道,由于所有的線程都使用同一個地址空間,每一個線程棧的大小就受到了限制,根據應用的業務邏輯,我們可能需要修改一個線程的棧的默認大小,例如,當存在的線程過多,我們就希望它的棧能夠小一點,而如果某個線程將要執行的業務邏輯需要進行很深的遞歸調用,我們就希望其運行棧能夠大一點。通過修改屬性的方式也能對這些進行設置:

    [cpp]?view plaincopy
  • int?pthread_attr_getstack(const?pthread_attr_t?*restrict?attr,???
  • ????void?**restrict?stackaddr,?size_t?*restrict?stacksize);??
  • int?pthread_attr_setstack(const?pthread_attr_t?*attr,???
  • ????void?*stackaddr,?size_t?*stacksize);??

  • stackaddr是棧的首地址,stacksize則是棧的大小。當然,許多時候,我們不希望自己來處理內存的分配等事宜,也可以通過以下的函數只指定棧的大小:

    [cpp]?view plaincopy
  • int?pthread_attr_getstacksize(const?pthread_attr_t?*restrict?attr,??
  • ????size_t?*restrict?stacksize);??
  • int?pthread_attr_setstacksize(pthread_attr_t?*attr,?size_t?stacksize);??

  • 一個屬性對象在被設置之后可以用于多個線程的創建,同時,當屬性對象使用完畢后,我們再對屬性對象的更改或是destroy都不會影響到那些已經創建完成的線程。


    線程終止

    以下是線程正常終止的三種方式:

    1. 線程從線程入口函數處返回,其返回值將是該線程的終止狀態。

    2. 線程調用pthread_exit函數終止當前線程,該函數的參數將被作為終止狀態。

    3. 該線程被相同進程中的其他線程所取消。

    由于任意一個線程調用exit系列函數都將終止整個進程,因此當我們只想要終止線程時可以使用pthread_exit函數:

    [cpp]?view plaincopy
  • void?pthread_exit(void?*rval_ptr);??

  • 如果線程被取消了,則其終止狀態將是:PTHREAD_CANCELED。那么,我們該如何獲得線程的終止狀態呢?前面我們講過線程的兩種運行模式:分離模式和接合(join)模式。當處于分離狀態終止時,其終止狀態將自動被系統所舍棄,只有處于接合模式下,我們才能獲得其終止狀態,參看以下函數:

    [cpp]?view plaincopy
  • int?pthread_join(pthread_t?thread,?void?**rval_ptr);??

  • 這個方法將阻塞直到它所指定的線程終止,參數rval_ptr是O類型的,用于獲取線程的終止狀態。

    類似于atexit函數一樣,我們也可以為線程提供清理函數,以下是線程清理函數的注冊函數;

    [cpp]?view plaincopy
  • void?pthread_cleanup_push(void?(*rtn)(void?*),?void?*arg);??
  • void?pthread_cleanup_poppthread_cleanup_pop(int?execute);??

  • 這兩個函數分別用于增加與刪除清理函數,清理函數可以不止一個,其組織形式按照棧的結構組織,所以增加刪除都是在棧頂操作,并且其調用順序也是與其添加順序相反的。出口函數在以下三種情況下會被調用:

    1. 調用pthread_exit函數

    2. 響應對線程的取消請求

    3. 使用非0參數調用pthread_cleanup_pop函數。

    從上面可以看出來,有一點也許會另我們意外,那就是,當線程從入口函數處正常返回時,清理函數并不會得到調用。

    pthread_cleanup_push用于增加清理函數,我們來看一下pthread_cleanup_pop函數,它的作用是刪除最后一個被注冊的清理函數,如果其調用參數非0,那么在刪除該清理函數的同時,它也將得到調用。有一點需要注意的是,由于pthread_cleanup_push和pthread_cleanup_push可能被宏來實現,所以我們必須成對的使用它們,否則會報編譯錯誤。下面是linux的實現方式:

    [cpp]?view plaincopy
  • #??define?pthread_cleanup_push(routine,?arg)?\??
  • do?{????????????????????????????????????????\??
  • __pthread_cleanup_class?__clframe?(routine,?arg)??
  • ??
  • #??define?pthread_cleanup_pop(execute)?\??
  • __clframe.__setdoit?(execute);????????????????????????\??
  • }?while?(0)??

  • 線程取消(cancellation)

    [cpp]?view plaincopy
  • int?pthread_cancel(pthread_t?tid);??

  • 該函數的默認效果是取消tid線程,這使得該線程仿佛自己調用了pthread_exit,使用PTHREAD_CANCELED作為其結束狀態。不過實際上,一個線程不必馬上對該取消請求進行響應,甚至可以忽略該請求。

    在默認情況下,只有當線程運行到取消點(cancellation point)時才會對取消請求進行響應,一個取消點是指線程檢查其是否已經被取消的地方,POSIX.1定義了如下的一些函數作為取消點,當這些函數被調用時,將檢查是否被取消,從而作出響應:


    當然,還有一些其他的函數也有可能作為取消點,不過那些是可選的,依照具體的實現而不同,不具備可移植性。實際上,我們也可以自己定義取消點。請看下面的函數:

    [cpp]?view plaincopy
  • void?pthread_testcancel(void);??

  • 當取消請求發生時,默認情況下,它會被阻塞,直到線程對它進行響應,該函數調用時,如果已有取消請求被阻塞住,并且線程取消功能并沒有被關閉(這個等會兒解釋)的話,該線程將被取消。

    前面說的這些都是默認操作,實際上我們也可以修改取消的狀態和類型。先說取消類型(cancellation type),通過對該屬性進行設置可以決定線程是否在取消點才被取消,請看函數聲明:

    [cpp]?view plaincopy
  • int?pthread_setcanceltype(int?type,?int?*oldtype);??

  • type參數只能是如下常量之一:PTHREAD_CANCEL_DEFERRED(這個是默認的) or PTHREAD_CANCEL_ASYNCHRONOUS。使用第一個常量,則線程只有到取消點才會檢查取消請求,但后者則決定線程可以在任何時候被取消,不必等到取消點。oldtype是一個O類型參數,用于獲取歷史取消類型。

    我們也可以修改其取消狀態使線程忽略其他線程的取消請求,使用如下函數:

    [cpp]?view plaincopy
  • int?pthread_setcancelstate(int?state,?int?*oldstate);??

  • 通過此函數可以設置線程是否對線程取消進行響應,state只能是如下常量之一:PTHREAD_CANCEL_ENABLE 或是 PTHREAD_CANCEL_DISABLE。需要注意的是,當取消狀態被設置為PTHREAD_CANCEL_DISABLE時,取消請求并沒有被舍棄,它只是被阻塞住了,直到當該功能再此被啟用時,如果有阻塞的取消請求,線程將會被取消。


    多線程下的信號量機制

    每一個線程都有它自己的信號量屏蔽字,但是信號處理方式(signal disposition)卻是在進程內共享的。如果一個信號量是有硬件錯誤或是時鐘到點導致的,那么該信號將被發送給發生這些事件的線程,而如果不是這些情況引發的信號,它們將被發送給該進程下的任意一個線程。在多線程環境下使用sigprocmask函數修改信號屏蔽字的行為是未定義的,我們應該使用另外一個函數代替,那就是pthread_sigmask。

    [cpp]?view plaincopy
  • int?pthread_sigmask(int?how,?const?sigset_t?*restrict?set,?sigset_t?*restrict?oset);??

  • 他的使用方式和sigprocmask函數是一致的,這里就不再多作討論了。

    在多線程環境下,我們通常可以指定某個特定的線程來專門完成信號處理的工作,從而可以防止因其他工作線程被打斷而引發的異常情況,先來看一個函數:

    [cpp]?view plaincopy
  • int?sigwait(const?sigset_t?*restrict?set,?int?*restrict?signop);??

  • set參數的類型是我們前面將信號機制時所提到過的信號集,在這里它被用來指定我們想要處理的信號,而第二個參數是O類型參數,當該函數返回時,它存有我們實際接收到的信號number。當該函數調用后,線程會被阻塞,直到有它所等待的信號發生(實際上,該函數也是可能會被其他信號所中斷的)。如果在調用時,已經有它要等待的信號在阻塞隊列里了,那么該函數將立即返回,而不需要再阻塞。在返回之前,sigwait函數將移除掉阻塞隊列中它所等待的信號。為避免可能出現的空檔,造成錯誤的信號處理行為,線程在調用sigwait函數之前應該先把它要等待的信號給阻塞調。而在掉用sigwait函數的時候,它將自動unblock這些信號并開始等待直到那些信號中的一個被交付。在該函數返回以前,這些被unblock了的信號會被再次自動恢復阻塞。如果多個線程在調用wigwait時等待了相同的信號,當該信號發生后,只有一個線程會獲得該信號并從阻塞中返回。

    在linux的實現中,必須要注意的是,由于linux中實際上沒有真正的線程,它所謂的線程實際上只是一個輕量級的進程,所以,當信號發生時,如果主線程沒有阻塞這個信號,其他線程是sigwait不到那個信號的。因此,在使用sigwait函數時,我們最好讓其他線程都把那些信號都給阻塞住。

    實際上,我們也可以將信號發送給某個特定的線程,類似于kill,其函數聲明如下:

    [cpp]?view plaincopy
  • int?pthread_kill(pthread_t?thread,?int?signo);??

  • 該函數的作用就是向指定的線程發送指定的信號量,這里有一個小技巧,當我們指定信號量為0時,該函數可以用來檢測該線程是否存在。如果接受到信號的線程對信號的默認操作是終止進程的話,那么整個進程都將被終止。

    對于信號量,還有一點值得注意的是,alarm timers是屬于進程的資源,所有的線程都共享了同一個alarm,所以,對于同一個進程中的多個線程來說,不用擔心其他線程的干擾而使用alarm timers的方法是不存在的。

    參考文獻

    《Linux下的多線程編程》 姚繼鋒

    總結

    以上是生活随笔為你收集整理的Unix环境高级编程学习笔记(七) 多线程的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。