中文亚洲精品无码_熟女乱子伦免费_人人超碰人人爱国产_亚洲熟妇女综合网

當前位置: 首頁 > news >正文

費縣做網(wǎng)站收錄提交入口網(wǎng)址

費縣做網(wǎng)站,收錄提交入口網(wǎng)址,專業(yè)做網(wǎng)站的,企業(yè)官網(wǎng)登錄12. 多線程編程 注:博客中有書中沒有的內(nèi)容,均是來自 黑馬06-線程概念_嗶哩嗶哩_bilibili 早期Linux不支持線程,直到1996年,Xavier Leroy等人開發(fā)出第一個基本符合POSIX標準的線程庫LinuxThreads,但LinuxThreads效率…

12. 多線程編程

注:博客中有書中沒有的內(nèi)容,均是來自
黑馬06-線程概念_嗶哩嗶哩_bilibili

早期Linux不支持線程,直到1996年,Xavier Leroy等人開發(fā)出第一個基本符合POSIX標準的線程庫LinuxThreads,但LinuxThreads效率低且問題多,自內(nèi)核2.6開始,Linux才開始提供內(nèi)核級的線程支持,并有兩個組織致力于編寫新的線程庫:NGPT(Next Generation POSIX Threads)和NPTL(Native POSIX Thread Library),但前者在2003年就放棄了,因此新的線程庫就是NPTL。NPTL比LinuxThreads效率高,且更符合POSIX規(guī)范,所以它已經(jīng)成為glibc的一部分,本書使用的線程庫是NPTL。

本章要討論的線程相關的內(nèi)容都屬于POSIX線程(簡稱 pthread)標準,不局限于NPTL實現(xiàn),包括:

  • 創(chuàng)建線程和結(jié)束線程;
  • 讀取和設置線程屬性;
  • POSIX線程同步方式:POSIX信號量、互斥鎖和條件變量。

文章目錄

  • 12. 多線程編程
    • 1.線程概念
      • 1.什么是線程
      • 2.Linux內(nèi)核線程實現(xiàn)原理
      • 3.線程共享資源
      • 4.線程優(yōu)缺點
    • 2.Linux 線程概述
      • 1.線程模型
      • 2.Linux 線程庫
    • 3.線程控制原語
      • 1.pthread_self
      • 2.pthread_create
        • 循環(huán)創(chuàng)建多個子線程
      • 3.pthread_exit
      • 4.pthreda_join
      • 5pthread_detach
      • 6.pthread_cancel
      • 7.終止線程方式
      • 8.控制原語對比
    • 4.線程屬性
      • 1.概述
      • 2.線程屬性初始化和銷毀
      • 3.線程的分離狀態(tài)
        • 線程分離狀態(tài)的函數(shù)
      • 4.線程屬性控制示例
    • 5.線程使用注意事項
    • 以下是線程同步的內(nèi)容:
    • 6.同步概念
      • 同步
      • 線程同步
    • 7.互斥鎖
      • 1.概述
      • 2.互斥鎖基礎 API
        • 1.初始化和銷毀
        • 2.加鎖解鎖
        • 3.使用案例
        • 4.注意事項
        • 5.try鎖
      • 3.互斥鎖屬性
      • 4.死鎖
    • 8.讀寫鎖
      • 1.原理
      • 2.特性
      • 3.對應函數(shù)
        • 1.初始化和銷毀
        • 2.加鎖解鎖
      • 4.示例
    • 9.條件變量
      • 1.工作原理
      • 2.對應函數(shù)
        • 1.總覽
        • 2.創(chuàng)建和銷毀
        • 3.wait函數(shù)
        • 4.pthread_cond_timedwait 函數(shù)
      • 3.使用條件變量模擬實現(xiàn)生產(chǎn)者—消費者問題
      • 4.條件變量優(yōu)勢
    • 9.POSIX 信號量
      • 1.概述
      • 2.對應函數(shù)
        • 1.總覽
        • 2.初始化和銷毀
        • 3.PV操作主要函數(shù)
      • 3.實現(xiàn)生產(chǎn)者消費者
    • 10.線程同步機制包裝類
    • 11.多線程環(huán)境
      • 1.可重入函數(shù)
      • 2.進程和線程
        • 人話翻譯一下它這三個函數(shù)在干什么
        • 總結(jié)
      • 3.線程和信號

1.線程概念

1.什么是線程

LWP:light weight process 輕量級的進程,本質(zhì)仍是進程(在Linux環(huán)境下)

進程:獨立地址空間,擁有PCB

線程:有獨立的PCB,但沒有獨立的地址空間(與進程共享)

區(qū)別:在于是否共享地址空間。獨居(地址空間就是進程一個) ; 合租(地址空間有多個線程)

Linux 下:

線程:最小的執(zhí)行(調(diào)度)單位

進程:最小分配資源單位,可看成是只有一個線程的進程

執(zhí)行該命令可以得到線程號LWP(LWP是cpu執(zhí)行的最小單位,但和后面講到的線程id不是一回事)

ps aux
ps -Lf 進程id          //得到該進程里面的線程

image-20241219135644520

image-20241219135833554

圖中LWP就是線程號,可以看到是接著4388和4213往后寫的,所以其實內(nèi)核把這些進程的線程當做進程看,所以更容易獲得CPU

2.Linux內(nèi)核線程實現(xiàn)原理

類Unix系統(tǒng)中,早期是沒有“線程”概念的,80年代才引入,借助進程機制實現(xiàn)出了線程的概念。因此在這類系統(tǒng)中,進程和線程關系密切

1.輕量級進程(light-weight process),也有PCB,創(chuàng)建線程使用的底層函數(shù)和進程一樣,都是clone

2.從內(nèi)核里看進程和線程是一樣的,都有各自不同的PCB,但是PCB中指向內(nèi)存資源的三級頁表是相同的

? 這句話中關于進程和線程都有各自不同的 PCB 部分正確,但線程的 PCB 相對進程的 PCB 有很大差異且不那么獨立;而說進程和線程指向內(nèi)存資源的三級頁表相同是錯誤的,應該是一個進程內(nèi)的線程共享進程的頁表,不同進程之間頁表是不同的。

3.進程可以蛻變成線程

4.線程可看做寄存器和的集合

5.在linux下,線程最是小的執(zhí)行單位;進程是最小的分配資源單位

三級映射:進程PCB->頁目錄->頁表->物理頁面

3.線程共享資源

線程共享資源

  • 1.文件描述符表

  • 2.每種信號的處理方式

  • 3.當前工作目錄

  • 4.用戶ID和組 ID

  • 5.內(nèi)存地址空間( 具體共享的部分是: .text/.data/.bss/heap/共享庫)------>線程間共享全局變量

線程非共享資源

  • 1.線程id

  • 2.處理器現(xiàn)場和棧指針(內(nèi)核棧)

  • 3.獨立的空間(用戶空間拔)

  • 4.errno 變量

  • 5.信號屏蔽字

  • 6.調(diào)度優(yōu)先級

不推薦信號和線程一起用,最好就別混著用。

4.線程優(yōu)缺點

優(yōu)點:

  • 提高程序并發(fā)性

  • 開銷小

  • 數(shù)據(jù)通信、共享數(shù)據(jù)方便

缺點:

  • 庫函數(shù),不如系統(tǒng)調(diào)用穩(wěn)定性強

  • 調(diào)試、編寫困難、gdb不支持

  • 對信號支持不好

優(yōu)點相對突出,缺點均不是硬傷。Linux下由于實現(xiàn)方法導致進程、線程差別不是很大

如果線程進程都可用,那優(yōu)先選擇線程

2.Linux 線程概述

1.線程模型

線程是程序中完成一個獨立任務的完整執(zhí)行序列,即一個可調(diào)度的實體。根據(jù)運行環(huán)境和調(diào)度者的身份,線程可分為內(nèi)核線程和用戶線程。

  • 內(nèi)核線程在有的系統(tǒng)上也稱為LWP(Light Weight Process,輕量級進程),運行在內(nèi)核空間,由內(nèi)核調(diào)度;
  • 用戶線程運行在用戶空間,由線程庫調(diào)度。當進程的一個內(nèi)核線程獲得CPU的使用權時,它就加載并運行一個用戶線程,可見,內(nèi)核線程相當于用戶線程運行的容器,一個進程可以擁有M個內(nèi)核線程和N個用戶線程,其中M<=N,并且在一個系統(tǒng)的所有進程中,M和N的比值都是固定的。

按照M:N的取值,線程的實現(xiàn)可分為三種模式:完全在用戶空間實現(xiàn)、完全由內(nèi)核調(diào)度、雙層調(diào)度(two level scheduler)。

完全在用戶空間實現(xiàn)的線程無須內(nèi)核的支持,內(nèi)核甚至不知道這些線程的存在,線程庫負責管理所有執(zhí)行線程,如線程的優(yōu)先級、時間片等。線程庫利用longjmp函數(shù)來切換線程的執(zhí)行,使它們看起來像是并發(fā)執(zhí)行的,但實際上內(nèi)核仍然是把整個進程作為最小單位來調(diào)度,換句話說,一個進程中的所有線程共享該進程的時間片,它們對外表現(xiàn)出相同的優(yōu)先級(即所有線程使用相同的優(yōu)先級,因為它們都是以進程為單位調(diào)度的)。對這種實現(xiàn)方式而言,M=1,即 N 個用戶線程對應一個內(nèi)核線程,而該內(nèi)核線程對實際就是進程本身。完全在用戶空間實現(xiàn)的線程的優(yōu)點:創(chuàng)建和調(diào)度線程都無需內(nèi)核干預,因此速度快,且它不占用額外內(nèi)核資源,所以即使一個進程創(chuàng)建了很多線程,也不會對系統(tǒng)性能造成明顯的影響。其缺點是:對于多處理器系統(tǒng),一個進程的多個線程無法運行在不同的CPU上,因為內(nèi)核是按照其最小調(diào)度單位來分配CPU的,且線程的優(yōu)先級只在相對于進程內(nèi)的其他線程生效,比較不同進程內(nèi)的線程的優(yōu)先級無意義。

完全由內(nèi)核調(diào)度的模式將創(chuàng)建、調(diào)度線程的任務交給了內(nèi)核,運行在用戶空間的線程庫無須執(zhí)行管理任務,這與完全在用戶空間實現(xiàn)的線程恰恰相反,因此二者的優(yōu)缺點也正好互換。較早的Linux內(nèi)核對內(nèi)核線程的控制能力有限,線程庫通常還要提供額外的控制能力,尤其是線程同步機制,但現(xiàn)代Linux內(nèi)核已經(jīng)大大增強了對線程的支持。完全由內(nèi)核調(diào)度的線程實現(xiàn)滿足M : N=1 : 1,即1個用戶空間線程被映射為1個內(nèi)核線程。

雙層調(diào)度模式是兩種實現(xiàn)模式的混合體,內(nèi)核調(diào)度M個內(nèi)核線程,線程庫調(diào)度N個用戶線程,這種線程實現(xiàn)方式結(jié)合了前兩種方式的優(yōu)點,不會消耗過多的內(nèi)核資源,且線程切換速度也較快,同時還能充分利用多處理器優(yōu)勢。

完全在用戶空間實現(xiàn):一個內(nèi)核級線程對多個用戶級別線程

完全由內(nèi)核調(diào)度:一個內(nèi)核級線程對一個用戶級線程

雙層調(diào)度:多個內(nèi)核級線程對多個用戶級線程

2.Linux 線程庫

Linux上兩個有名的線程庫是LinuxThreads和NPTL,它們都采用1:1方式實現(xiàn)(完全由內(nèi)核調(diào)度的模式)?,F(xiàn)代Linux上默認使用的線程庫是NPTL,用戶可用以下命令查看當前系統(tǒng)使用的線程庫:

getconf GNU_LIBPTHREAD_VERSION

image-20241219134420097

LinuxThreads線程庫的內(nèi)核線程是用clone系統(tǒng)調(diào)用創(chuàng)建的進程模擬的,clone系統(tǒng)調(diào)用和fork系統(tǒng)調(diào)用的作用類似,都創(chuàng)建調(diào)用進程的子進程,但我們可以為clone系統(tǒng)調(diào)用指定CLONE_THREAD標志,此時它創(chuàng)建的子進程與調(diào)用進程共享相同的虛擬地址空間、文件描述符、信號處理函數(shù),這些都是線程的特點,但用進程模擬內(nèi)核線程會導致很多語義問題:

  • 每個線程擁有不同的PID,不符合POSIX規(guī)范。
  • Linux信號處理本來是基于進程的,但現(xiàn)在一個進程內(nèi)部的所有線程都能且必須處理信號。
  • 用戶ID、組ID對一個進程中的不同線程來說可能不同。
  • 進程產(chǎn)生的核心轉(zhuǎn)儲文件不會包含所有線程的信息,而只包含該核心轉(zhuǎn)儲文件的線程的信息。
  • 由于每個線程都是一個進程,因此系統(tǒng)允許的最大進程數(shù)就是最大線程數(shù)。

LinuxThreads線程庫一個有名的特性是所謂的管理線程,它是進程中專門用于管理其他工作線程的線程,其作用為:

  • 系統(tǒng)發(fā)送給進程的終止信號先由管理線程接收,管理線程再給其他工作線程發(fā)送同樣的信號以終止它們。
  • 當終止工作線程或工作線程主動退出時,管理線程必須等待它們結(jié)束,以避免僵尸進程。
  • 如果主線程即將先于其他工作線程退出,則管理線程將阻塞主線程,直到所有其他工作線程都結(jié)束后才喚醒它。
  • 回收每個線程堆棧使用的內(nèi)存。

管理線程的引入,增加了額外的系統(tǒng)開銷,且由于管理線程只能運行在一個CPU上,所以LinuxThreads線程庫不能充分利用多處理器系統(tǒng)的優(yōu)勢(所有管理操作只能在一個CPU上完成)。

要解決LinuxThreads線程庫的一系列問題,不僅需要改進線程庫,最主要的是需要內(nèi)核提供更完善的線程支持,因此Linux內(nèi)核從2.6版本開始,提供了真正的內(nèi)核線程,新的NPTL線程庫也應運而生,相比LinuxThreads,NPTL的主要優(yōu)勢在于:

  • 內(nèi)核線程不再是一個進程,因此避免了很多用進程模擬線程導致的語義問題。
  • 摒棄了管理線程,終止線程、回收線程堆棧等工作都可以由內(nèi)核完成。
  • 由于不存在管理線程,所以一個進程的線程可以運行在不同CPU上,從而充分利用了多處理器系統(tǒng)的優(yōu)勢。
  • 線程的同步由內(nèi)核來完成,隸屬于不同進程的線程之間也能共享互斥鎖,因此可實現(xiàn)跨進程的線程同步。

3.線程控制原語

創(chuàng)建和結(jié)束線程的API在Linux上定義在pthread.h頭文件。

1.pthread_self

獲取當前線程的線程ID。其作用對應進程中getpid()函數(shù)。

#include<pthread.h>
pthread_t pthread_self(void);

返回值:

成功:0;失敗:無 !

線程ID:pthread_t類型,本質(zhì):在Linux下為無符號整數(shù)(%lu),其他系統(tǒng)中可能是結(jié)構(gòu)體實現(xiàn)。

線程ID是進程內(nèi)部識別線程的標志。(兩個進程間,線程ID允許相同)

注意:不應使用全局變量 pthread_t tid,在子線程中通過 pthread_create 傳出參數(shù)來獲取線程 ID,而應使用pthread self

2.pthread_create

pthread_create函數(shù)創(chuàng)建一個線程:

#include <pthread.h>
int pthread_create(pthread_t* thread, count pthread_attr_t* attr, void* (*start_routine)(void*), void* arg);

參數(shù)

thread:傳出參數(shù),表示新創(chuàng)建的子線程的線程id,新線程的標識符,后續(xù)pthread_*函數(shù)通過它來引用新線程,其類型pthread_t定義如下:

#include <bits/pthreadtypes.h>
typedef unsignde long int ptherad_t;

attr:用于設置新線程的屬性,給它傳遞NULL表示使用默認線程屬性。

start_routine:子線程的回調(diào)函數(shù)

arg:子線程回調(diào)函數(shù)的參數(shù),沒有的話傳一個NULL

返回值

成功時返回0,失敗時返回錯誤碼,并且第一個參數(shù)thread不會有值。

一個用戶可以打開的線程數(shù)不能超過RLIMIT_NPROC軟資源限制,此外,系統(tǒng)上所有用戶能創(chuàng)建的線程總數(shù)也不能超過/proc/sys/kernel/threads-max內(nèi)核參數(shù)定義的值。

循環(huán)創(chuàng)建多個子線程
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <string.h>
#include <stdlib.h>
#include<pthread.h>void *tfn(void *arg)
{int i=(int)arg;sleep(i);printf("I'm %dth thread:pid=%d,tid=%lu\n",i+1,getpid(),pthread_self());return NULL;
}int main(int argc,char* argv[])
{int i,ret;pthread_t tid;for(i=0;i<5;i++){ret=pthread_create(&tid,NULL,tfn,(void*)i);//ret=pthread_create(&tid,NULL,tfn,(void*)&i);//這樣寫是不行的,因為傳入的是i的地址,但是i是循環(huán)變量,一直在變化,可能現(xiàn)在傳入的是2,但是等子線程讀取的時候就是3或4了if(ret!=0){perror("pthread_create error");exit(1);}}sleep(i);printf("main: pid=%d,tid=%d\n",getpid(),pthread_self());return 0;
}

image-20241219152142911

arg傳值采用值傳遞,借助強轉(zhuǎn)

3.pthread_exit

線程一旦被創(chuàng)建,內(nèi)核就可以調(diào)度內(nèi)核線程來執(zhí)行start_routine函數(shù)指針參數(shù)所指向的函數(shù)了,線程函數(shù)在結(jié)束時最好調(diào)用pthread_exit函數(shù),以確保安全、干凈地退出:

#include <pthread.h>
void pthread_exit(void* retval);

pthread_exit函數(shù)會通過retval參數(shù)向線程的回收者傳遞其退出信息,它執(zhí)行完后不會返回到調(diào)用者,且永遠不會失敗。

參數(shù):

retval:回調(diào)函數(shù)的返回值。 無返回值時,傳NULL

辨析:

exit(); 退出當前進程

return: 返回到調(diào)用者那里去

pthread_exit(): 退出當前線程

4.pthreda_join

一個進程中的所有線程都能調(diào)用pthread_join函數(shù)來回收其他線程(前提是目標線程是可回收的),即等待其他線程結(jié)束,這類似回收進程的waitwaitpid系統(tǒng)調(diào)用。

阻塞等待線程退出,獲取線程狀態(tài)。

#include <pthread.h>
int pthread_join(pthread_t thread, void** retval);

參數(shù)

thread:目標線程的線程號(標識符)。

retval:傳出參數(shù),目標線程返回的退出信息。就是接收pthread_exit的參數(shù)。

返回值

pthread_join函數(shù)會一直阻塞,直到被回收的線程結(jié)束為止。

成功時返回0,失敗則返回錯誤碼。

使用示例

#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <string.h>
#include <stdlib.h>
#include<pthread.h>struct thrd{int var;char str[256];
};void *tfn(void *arg)
{   struct thrd *tval;tval=malloc(sizeof(tval));tval->var=100;strcpy(tval->str,"hello pthread\n");return (void*)tval;
}int main(int argc,char* argv[])
{int i,ret;pthread_t tid;struct thrd *retval;ret=pthread_create(&tid,NULL,tfn,(void*)i);if(ret!=0){perror("pthread_create error");exit(1);}ret=pthread_join(tid,(void **)&retval);if(ret!=0){perror("pthread_join error");exit(1);}printf("child thread exit with var=%d,str=%s\n",retval->var,retval->str);return 0;
}

5pthread_detach

設置線程分離。當一個線程處于分離狀態(tài)時,它的資源在結(jié)束時會自動被系統(tǒng)回收,而不需要其他線程調(diào)用pthread_join函數(shù)來等待它結(jié)束并回收資源。

int pthread_detach(pthread_t thread);		

參數(shù):

thread: 待分離的線程id

返回值:

成功:0

失敗:errno

非分離狀態(tài)(默認情況):在默認情況下,線程是處于非分離狀態(tài)(也稱為可連接狀態(tài))。當一個線程創(chuàng)建后,如果不進行pthread_detach操作,那么它的資源(如線程棧等)在結(jié)束后不會自動釋放,需要其他線程通過pthread_join函數(shù)來等待它結(jié)束,并且在pthread_join函數(shù)返回后,系統(tǒng)才會回收該線程的資源。這就好像線程結(jié)束后,它的 “后事”(資源回收)需要其他線程來幫忙處理。

分離狀態(tài):使用pthread_detach函數(shù)將線程設置為分離狀態(tài)后,線程結(jié)束時就會自動釋放自身的資源。這類似于線程自己處理自己的 “后事”,不需要其他線程專門等待它并回收資源。例如,在一些簡單的多線程應用場景中,對于那些執(zhí)行完任務后就不需要再關注其結(jié)果的線程,將它們設置為分離狀態(tài)可以簡化程序的邏輯,避免因為忘記調(diào)用pthread_join而導致資源泄漏。

1.進程若有該機制,將不會產(chǎn)生僵尸進程。僵尸進程的產(chǎn)生主要由于進程死后,大部分資源被釋放,一點殘留資
源仍存于系統(tǒng)中,導致內(nèi)核認為該進程仍存在

2.**不能對一個已經(jīng)處于detach狀態(tài)的線程調(diào)用pthread_join,這樣的調(diào)用將返回EINVAL錯誤。**也就是說,如果已
經(jīng)對一個線程調(diào)用了pthread_detach就不能再調(diào)用pthread_join了。

3.也可使用 pthread_create函數(shù)參2(線程屬性)來設置線程分離

6.pthread_cancel

有時候我們希望異常終止一個線程,即取消(殺死)線程,它是通過pthread_cancel函數(shù)實現(xiàn)的:

#include <ptrhead.h>
int pthread_cancel(pthread_t thread);

參數(shù)

thread:要殺死的線程的線程號(標識符)

返回值

pthread_cancel函數(shù)成功時返回0,失敗則返回錯誤碼。

接收到取消請求的目標線程可以決定是否允許被取消以及如何取消,這通過以下函數(shù)完成:

#include <pthread.h>
int pthread_setcancelstate(int state, int *oldstate);
int pthread_setcanceltype(int type, int *oldtype);

注意:線程的取消并不是實時的,而有一定的延時。需要等待線程到達某個取消點(檢查點)

類似于玩游戲存檔,必須到達指定的場所(存檔點,如:客棧、倉庫、城里等)才能存儲進度。殺死線程也不是

立刻就能完成,必須要到達取消點。

取消點:是線程檢查是否被取消,并按請求進行動作的一個位置。通常是一些系統(tǒng)調(diào)用 creat,open,pause,
close,read,write … 執(zhí)行命令man 7 pthreads 可以查看具備這些取消點的系統(tǒng)調(diào)用列表。。心

**可粗略認為一個系統(tǒng)調(diào)用(進入內(nèi)核)即為一個取消點。**如線程中沒有取消點,可以通過調(diào)用 pthread_testcancel
函數(shù)自行設置一個取消點

被取消的線程,退出值定義在Linux的pthread庫中。常數(shù)PTHREAD_CANCELED的值是-1。可在頭文件pthread.h
中找到它的定義:#define PTHREAD_CANCELED((void*)-1)。因此當我們對一個已經(jīng)被取消的線程使用pthread_join回收時,得到的返回值為-1

示例:

如代碼所示,如果tfn中沒有設置保存點,也沒有進入內(nèi)核的語句(只是一些for if之類的),那cancel就會失效的

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>void *tfn(void *arg)
{while(1){/*printf("I'm thread,pid:%d,tid:%lu\n",getpid(),pthread_self());sleep(1);*/pthread_testcancel();//設置保存點}return NULL;
}int main(void)
{pthread_t tid;//創(chuàng)建線程int ret = pthread_create(&tid,NULL,tfn,NULL);if(ret!=0){perror("pthread_create error");exit(1);}printf("I'm main,pid:%d,tid:%lu\n",getpid(),pthread_self());// 終止線程ret = pthread_cancel(tid);while(1);pthread_exit(NULL);
}

7.終止線程方式

總結(jié):終止某個線程而不終止整個進程,有三種方法:

  1. 從線程主函數(shù) return。這種方法對主控線程不適用,從 main 函數(shù) return 相當于調(diào)用 exit。

  2. 一個線程可以調(diào)用 pthread_cancel 終止同一進程中的另一個線程。

  3. 線程可以調(diào)用 pthread_exit 終止自己。

8.控制原語對比

image-20241219161609981

4.線程屬性

1.概述

pthread_attr_t結(jié)構(gòu)體定義了一套完整的線程屬性:

#include <bits/pthreadtypes.h>
#define __SIZEOF_PTHREAD_ATTR_T 36
typedef union{char __size[__SIZEOF_PTHREAD_ATTR_T];long int __align;
} pthread_attr_t;

線程的各種屬性都包括在一個字符數(shù)組里,線程庫定義了一系列函數(shù)來操作pthread_attr_t類型變量,方便我們設置和獲取線程屬性:

#include <pthread.h>int pthread_attr_init ( pthread_attr_t* attr ); /* 初始化線程屬性對象 */
int pthread_attr_destroy(pthread_attr_t* attr ); /* 銷毀線程屬性對象。被銷毀的線程屬性對象只有再次初始化之后才能繼續(xù)使用 *//* 下面這些函數(shù)用于獲取和設置線程屬性對象的某個屬性 */
int pthread_attr_getdetachstate ( const pthread_attr_t* attr, int* detachstate );
int pthread_attr_setdetachstate( pthread_attr_t* attr, int detachstate );
int pthread_attr_getstackaddr( const pthread_attr_t* attr,void ** stackaddr );
int pthread_attr_setstackaddr( pthread_attr_t* attr, void* stackaddr );
int pthread_attr_getstacksize(const pthread_attr_t* attr, size_t* stacksize );
int pthread_attr_setstacksize(pthread_attr_t* attr, size_t stacksize);                        
int pthread_attr_getstack ( const pthread_attr_t* attr, void** stackaddr, size_t* stacksize);/* 還有很多,不在這里一一列舉了,具體可查看書籍P273頁 */

linux 下線程的屬性是可以根據(jù)實際項目需要,進行設置,之前我們討論的線程都是采用線程的默認屬性,默認屬性已經(jīng)可以解決絕大多數(shù)開發(fā)時遇到的問題。如我們對程序的性能提出更高的要求那么需要設置線程屬性,比如可以通過設置線程棧的大小來降低內(nèi)存的使用,增加最大線程個數(shù)。

typedef struct
{
int etachstate; //線程的分離狀態(tài)
int schedpolicy; //線程調(diào)度策略
struct sched_param 
schedparam; //線程的調(diào)度參數(shù)
int inheritsched; //線程的繼承性
int scope; //線程的作用域
size_t guardsize; //線程棧末尾的警戒緩沖區(qū)大小int stackaddr_set; //線程的棧設置void* stackaddr; //線程棧的位置size_t stacksize; //線程棧的大小} pthread_attr_t; 

主要結(jié)構(gòu)體成員:

  1. 線程分離狀態(tài)

  2. 線程棧大小(默認平均分配)

  3. 線程棧警戒緩沖區(qū)大小(位于棧末尾)

屬性值不能直接設置,須使用相關函數(shù)進行操作,初始化的函數(shù)為 pthread_attr_init,這個函數(shù)必須在

pthread_create 函數(shù)之前調(diào)用。之后須用 pthread_attr_destroy 函數(shù)來釋放資源。

**線程屬性主要包括如下屬性:**作用域(scope)、棧尺寸(stack size)、棧地址(stack address)、優(yōu)先級(priority)、分離的狀態(tài)(detached state)、調(diào)度策略和參數(shù)(scheduling policy and parameters)。默認的屬性為非綁定、非分離、缺省的堆棧、與父進程同樣級別的優(yōu)先級。

最重要的部分,不過太麻煩了,不如detach

//設置分離屬性
pthread_attr_t attr  	創(chuàng)建一個線程屬性結(jié)構(gòu)體變量pthread_attr_init(&attr);	初始化線程屬性pthread_attr_setdetachstate(&attr,  PTHREAD_CREATE_DETACHED);		設置線程屬性為 分離態(tài)pthread_create(&tid, &attr, tfn, NULL); 借助修改后的 設置線程屬性 創(chuàng)建為分離態(tài)的新線程pthread_attr_destroy(&attr);	銷毀線程屬性

2.線程屬性初始化和銷毀

注意:應先初始化線程屬性,再 pthread_create 創(chuàng)建線程

初始化線程屬性

int pthread_attr_init(pthread_attr_t *attr);

成功:0;失敗:錯誤號

銷毀線程屬性所占用的資源

int pthread_attr_destroy(pthread_attr_t *attr); 

成功:0;失敗:錯誤號

3.線程的分離狀態(tài)

線程的分離狀態(tài)決定一個線程以什么樣的方式來終止自己。

非分離狀態(tài):線程的默認屬性是非分離狀態(tài),這種情況下,原有的線程等待創(chuàng)建的線程結(jié)束。只有當 pthread_join()

函數(shù)返回時,創(chuàng)建的線程才算終止,才能釋放自己占用的系統(tǒng)資源。

分離狀態(tài):分離線程沒有被其他的線程所等待,自己運行結(jié)束了,線程也就終止了,馬上釋放系統(tǒng)資源。應該

根據(jù)自己的需要,選擇適當?shù)姆蛛x狀態(tài)。

線程分離狀態(tài)的函數(shù)

設置線程屬性,分離 or 非分離

int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate); 

獲取程屬性,分離 or 非分離

int pthread_attr_getdetachstate(pthread_attr_t *attr, int *detachstate); 

參數(shù):

attr:已初始化的線程屬性

detachstate: PTHREAD_CREATE_DETACHED(分離線程)

? PTHREAD _CREATE_JOINABLE(非分離線程)

這里要注意的一點是,如果設置一個線程為分離線程,而這個線程運行又非常快,它很可能在 pthread_create函數(shù)返回之前就終止了,它終止以后就可能將線程號和系統(tǒng)資源移交給其他的線程使用,這樣調(diào)用pthread_create的線程就得到了錯誤的線程號。要避免這種情況可以采取一定的同步措施,最簡單的方法之一是可以在被創(chuàng)建的線程里調(diào)用 pthread_cond_timedwait 函數(shù),讓這個線程等待一會兒,留出足夠的時間讓函數(shù) pthread_create 返回。設置一段等待時間,是在多線程編程里常用的方法。但是注意不要使用諸如 wait()之類的函數(shù),它們是使整個進程睡眠,并不能解決線程同步的問題。

//設置分離屬性
pthread_attr_t attr  	創(chuàng)建一個線程屬性結(jié)構(gòu)體變量pthread_attr_init(&attr);	初始化線程屬性pthread_attr_setdetachstate(&attr,  PTHREAD_CREATE_DETACHED);		設置線程屬性為 分離態(tài)pthread_create(&tid, &attr, tfn, NULL); 借助修改后的 設置線程屬性 創(chuàng)建為分離態(tài)的新線程pthread_attr_destroy(&attr);	銷毀線程屬性

4.線程屬性控制示例

//設置分離屬性
pthread_attr_t attr  	創(chuàng)建一個線程屬性結(jié)構(gòu)體變量pthread_attr_init(&attr);	初始化線程屬性pthread_attr_setdetachstate(&attr,  PTHREAD_CREATE_DETACHED);		設置線程屬性為 分離態(tài)pthread_create(&tid, &attr, tfn, NULL); 借助修改后的 設置線程屬性 創(chuàng)建為分離態(tài)的新線程pthread_attr_destroy(&attr);	銷毀線程屬性
#include <pthread.h>#define SIZE 0x100000void *th_fun(void *arg)
{while(1);sleep(1);
}int main(void)
{pthread_t tid;int err, detachstate, i = 1;pthread_attr_t attr;size_t stacksize;void *stackaddr;pthread_attr_init(&attr);pthread_attr_getstack(&attr, &stackaddr, &stacksize);pthread_attr_getdetachstate(&attr, &detachstate);if (detachstate == PTHREAD_CREATE_DETACHED)printf("thread detached\n");else if (detachstate == PTHREAD_CREATE_JOINABLE)printf("thread join\n");elseprintf("thread unknown\n");pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);while (1) {stackaddr = malloc(SIZE);if (stackaddr == NULL) {perror("malloc");exit(1);}stacksize = SIZE;pthread_attr_setstack(&attr, stackaddr, stacksize);err = pthread_create(&tid, &attr, th_fun, NULL);if (err != 0) {printf("%s\n", strerror(err));exit(1);}printf("%d\n", i++);}pthread_attr_destroy(&attr);return 0;
} 

5.線程使用注意事項

1.主線程退出其他線程不退出,主線程應調(diào)用pthread_exit

2.避免僵尸線程

  • pthread_join

  • pthread_detach

  • pthread_create 指定分離屬性

被join線程可能在join函數(shù)返回前就釋放完自己的所有內(nèi)存資源,所以不應當返回被回收線程棧中的值;

3.malloc和mmap申請的內(nèi)存可以被其他線程釋放

4.應避免在多線程模型中調(diào)用fork除非,馬上exec,子進程中只有調(diào)用fork的線程存在,其他線程在子進程
中均pthread_exit

5.信號的復雜語義很難和多線程共存,應避免在多線程引入信號機制

以下是線程同步的內(nèi)容:

6.同步概念

同步

所謂同步,即同時起步,協(xié)調(diào)一致。

不同的對象,對“同步”的理解方式略有不同。如,設備同步,是指在兩個設備之間規(guī)定一個共同的時間參考;數(shù)據(jù)庫同步,是指讓兩個或多個數(shù)據(jù)庫內(nèi)容保持一致,或者按需要部分保持一致;文件同步,是指讓兩個或多個文件夾里的文件保持一致。等等。

而,編程中、通信中所說的同步與生活中大家印象中的同步概念略有差異。“同”字應是指協(xié)同、協(xié)助、互相配合。主旨在協(xié)同步調(diào),按預定的先后次序運行。

線程同步

線程同步,指一個線程發(fā)出某一功能調(diào)用時,在沒有得到結(jié)果之前,該調(diào)用不返回。同時其它線程為保證數(shù)據(jù)
一致性,不能調(diào)用該功能。

舉例1:銀行存款 5000。柜臺,折:取3000;提款機,卡:取3000。剩余:2000

舉例2:內(nèi)存中100字節(jié),線程T1欲填入全1,線程T2欲填入全0。但如果T1執(zhí)行了50個字節(jié)失去cpu,T2
執(zhí)行,會將T1寫過的內(nèi)容覆蓋。當T1再次獲得cpu繼續(xù) 從失去cpu的位置向后寫入1,當執(zhí)行結(jié)束,內(nèi)存中的
100字節(jié),既不是全1,也不是全0。

產(chǎn)生的現(xiàn)象叫做“與時間有關的錯誤”(time related)。

為了避免這種數(shù)據(jù)混亂,線程需要同步。

“同步”的目的,是為了避免數(shù)據(jù)混亂,解決與時間有關的錯誤。實際上,不僅線程間需要同步,進程間、信
號間等等都需要同步機制。因此,所有“多個控制流,共同操作一個共享資源”的情況,都需要同步。

7.互斥鎖

1.概述

數(shù)據(jù)混亂原因:

  1. 資源共享(獨享資源則不會)

  2. 調(diào)度隨機(意味著數(shù)據(jù)訪問會出現(xiàn)競爭)

  3. 線程間缺乏必要的同步機制。

以上 3 點中,前兩點不能改變,欲提高效率,傳遞數(shù)據(jù),資源必須共享。只要共享資源,就一定會出現(xiàn)競爭。只要存在競爭關系,數(shù)據(jù)就很容易出現(xiàn)混亂。所以只能從第三點著手解決。使多個線程在訪問共享資源的時候,出現(xiàn)互斥。

互斥鎖(也稱互斥量)用于保護關鍵代碼段,以確保其獨占式的訪問,這有些像二進制信號量(信號量),當進入關鍵代碼段時,我們需要獲得互斥鎖并將其加鎖,這等價于二進制信號量的P操作;當離開關鍵代碼段時,我們需要對互斥鎖解鎖,以喚醒其他等待該互斥鎖的線程,這相當于二進制信號量的V操作。

注意:同一時刻,只能有一個線程持有該鎖

當 A 線程對某個全局變量加鎖訪問,B 在訪問前嘗試加鎖,拿不到鎖,B 阻塞。C 線程不去加鎖,而直接訪問該全局變量,依然能夠訪問,但會出現(xiàn)數(shù)據(jù)混亂。

所以,互斥鎖實質(zhì)上是操作系統(tǒng)提供的一把“建議鎖”(又稱“協(xié)同鎖”),建議程序中有多線程訪問共享資源的時候使用該機制。但,并沒有強制限定。

因此,即使有了 mutex,如果有線程不按規(guī)則來訪問數(shù)據(jù),依然會造成數(shù)據(jù)混亂。

2.互斥鎖基礎 API

#include <pthread.h>
int pthread_mutex_init ( pthread_mutex_t* mutex,					/* 初始化互斥鎖 */const pthread_mutexattr_t* mutexattr );	
int pthread_mutex_destory ( pthread_mutex_t* mutex );				/* 銷毀互斥鎖 */
int pthread_mutex_lock ( pthread_mutex_t* mutex );					/* 以原子操作的方式給一個互斥鎖加鎖 */
int pthread_mutex_trylock ( pthread_mutex_t* mutex );				/* 相當于 pthread_mutex_lock 的非阻塞版本 ,非阻塞輪詢*/
int pthread_mutex_unlock ( pthread_mutex_t* mutex );				/* 以原子操作的方式給一個互斥鎖解鎖 */

以上5個函數(shù)的返回值都是:成功返回0,失敗返回錯誤號。

pthread_mutex_t 類型,其本質(zhì)是一個結(jié)構(gòu)體。為簡化理解,應用時可忽略其實現(xiàn)細節(jié),簡單當成整數(shù)看待。

pthread_mutex_t mutex;

變量 mutex只有兩種取值1、0

使用mutex(互斥量、互斥鎖)一般步驟:

pthread_mutex_t 類型。 1. pthread_mutex_t lock;  創(chuàng)建鎖2  pthread_mutex_init; 初始化		13. pthread_mutex_lock;加鎖		1--  	--> 04. 訪問共享數(shù)據(jù)(stdout)		5. pthrad_mutext_unlock();解鎖		0++	  --> 16. pthead_mutex_destroy;銷毀鎖
1.初始化和銷毀
int pthread_mutex_init ( pthread_mutex_t* mutex,					/* 初始化互斥鎖 */const pthread_mutexattr_t* mutexattr );	

參數(shù):

mutex:咱們創(chuàng)建的鎖

mutexattr:鎖的屬性

int pthread_mutex_destory ( pthread_mutex_t* mutex );				/* 銷毀互斥鎖 */

參數(shù)同上

restrict關鍵字,用來限定指針變量。被該關鍵字限定的指針變量所指向的內(nèi)存操作,必須由本指針完成。

pthread_mutex_t mutex;
pthread_mutex_init(&mutex, NULL);   			//動態(tài)初始化。
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;	//靜態(tài)初始化。
2.加鎖解鎖
int pthread_mutex_lock ( pthread_mutex_t* mutex );					/* 以原子操作的方式給一個互斥鎖加鎖 */
int pthread_mutex_trylock ( pthread_mutex_t* mutex );				/* 相當于 pthread_mutex_lock 的非阻塞版本 ,非阻塞輪詢*/
int pthread_mutex_unlock ( pthread_mutex_t* mutex );				/* 以原子操作的方式給一個互斥鎖解鎖 */

參數(shù)都是我們創(chuàng)建的鎖

3.使用案例
#include <stdio.h>
#include <string.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>pthread_mutex_t mutex;void err_thread(int ret, char *str)
{if (ret != 0) {fprintf(stderr, "%s:%s\n", str, strerror(ret));pthread_exit(NULL);}
}void *tfn(void *arg)
{srand(time(NULL));while (1) {pthread_mutex_lock(&mutex);printf("hello ");sleep(rand() % 3);	/*模擬長時間操作共享資源,導致cpu易主,產(chǎn)生與時間有關的錯誤*/printf("world\n");pthread_mutex_unlock(&mutex);sleep(rand() % 3);}return NULL;
}int main(void)
{int flag = 5;pthread_t tid;srand(time(NULL));pthread_mutex_init(&mutex, NULL);pthread_create(&tid, NULL, tfn, NULL);while (flag--) {pthread_mutex_lock(&mutex);printf("HELLO ");sleep(rand() % 3);printf("WORLD\n");pthread_mutex_unlock(&mutex);sleep(rand() % 3);}pthread_cancel(tid);                //  將子線程殺死,子線程中自帶取消點pthread_join(tid, NULL);pthread_mutex_destroy(&mutex);return 0;                           //main中的return可以將整個進程退出
}
4.注意事項

1.盡量保證鎖的粒度, 越小越好。(訪問共享數(shù)據(jù)前,加鎖。訪問結(jié)束立即解鎖。)

2.互斥鎖,本質(zhì)是結(jié)構(gòu)體。 我們可以看成整數(shù)。 可以認為是初值為 1。(pthread_mutex_init() 函數(shù)調(diào)用成功。)

3.加鎖: --操作, 阻塞線程。

4.解鎖: ++操作, 換醒阻塞在鎖上的線程。

5.try鎖
int pthread_mutex_trylock ( pthread_mutex_t* mutex );				/* 相當于 pthread_mutex_lock 的非阻塞版本 ,非阻塞輪詢*/

lock加鎖失敗會阻塞,等待鎖釋放

trylock不斷嘗試加鎖,加鎖失敗直接返回錯誤號(如:EBUSY),不阻塞

3.互斥鎖屬性

pthread_mutex_t結(jié)構(gòu)體描述互斥鎖的屬性,線程庫提供了一系列函數(shù)來操作pthread_mutexattr_t類型的變量,以方便我們獲取和設置互斥鎖屬性,以下是其中一些主要的函數(shù):

#include <pthread.h>/* 初始化互斥鎖屬性對象 */
int pthread_mutexattr_init ( pthread_mutexattr_t* attr );/* 銷毀互斥鎖屬性對象 */
inrt pthread_mutexattr_destroy ( pthread_mutexattr_t* attr );/* 獲取和設置互斥鎖的 pshared 屬性 */
int pthread_mutexattr_getpshared ( const pthread_mutexattr_t* attr, int* pshared );
int pthread_muextattr_setpshared ( pthread_mutexattr_t* attr, int* pshared );/* 獲取和設置互斥鎖的 type 屬性 */
int pthread_mutexattr_gettype ( const pthread_mutexattr_t* attr, int* type );
int pthread_mutexattr_settype ( pthread_mutexattr_t* attr, int* type );

本書僅討論互斥鎖的兩種常用屬性:psharedtype。

互斥鎖屬性pshared指定是否允許跨進程共享互斥鎖,其可選值為:

  • PTHREAD_PROCESS_SHARED:互斥鎖可以被跨進程共享。
  • PTHREAD_PROCESS_PRIVATE:互斥鎖只能和鎖的初始化線程隸屬于同一個進程的線程共享。

互斥鎖屬性type指定互斥鎖的類型,Linux支持以下4種互斥鎖:

  • PTHREAD_MUTEX_NORMAL:普通鎖,這是互斥鎖的默認類型。當一個線程對一個普通鎖加鎖后,其余請求該鎖的線程將形成一個等待隊列,并在該鎖解鎖后按優(yōu)先級獲得它。這種鎖類型保證了資源分配的公平性,但也容易引發(fā)問題:一個線程如果對一個已經(jīng)加鎖的普通鎖再次加鎖,將引發(fā)死鎖;對一個已經(jīng)被其他線程加鎖的普通鎖解鎖,或者對一個已經(jīng)解鎖的普通鎖再次解鎖,將導致不可預期的后果。
  • PTHREAD_MUTEX_ERRORCHECK:檢錯鎖。一個線程如果對一個自己加鎖的檢錯鎖再次加鎖,則加鎖操作返回EDEADLK。對一個已經(jīng)被其他線程加鎖的檢錯鎖解鎖,或?qū)σ粋€已經(jīng)解鎖的檢錯鎖再次解鎖,則解鎖操作返回EPERM。
  • PTHREAD_MUTEX_RECURSIVE:嵌套鎖。這種鎖允許一個線程在釋放鎖前多次對它加鎖而不發(fā)生死鎖,但如果其他線程要獲得這個鎖,則當前鎖的擁有者必須執(zhí)行相應次數(shù)的解鎖操作。對一個已經(jīng)被其他線程加鎖的嵌套鎖解鎖,或?qū)σ粋€已經(jīng)解鎖的嵌套鎖再次解鎖,則解鎖操作將返回EPERM。
  • PTHREAD_MUTEX_DEFAULT:默認鎖。通常被映射為以上三種鎖之一。

4.死鎖

是使用鎖不恰當造成的現(xiàn)象:

  1. 線程試圖對同一個互斥量A加鎖兩次。

  2. 線程1擁有A鎖,請求獲得B鎖;線程2擁有B鎖,請求獲得A鎖

死鎖使一個或多個線程被掛起而無法繼續(xù)執(zhí)行,且這種情況還不容易被發(fā)現(xiàn)。

在一個線程中對一個已經(jīng)加鎖的普通鎖再次加鎖將導致死鎖。另外,如果兩個線程按照不同順序來申請兩個互斥鎖,也容易產(chǎn)生死鎖,如以下代碼所示:

#include <pthread.h>
#include <unistd.h>
#include <stdio.h>int a = 0;
int b = 0;
pthread_mutex_t mutex_a;
pthread_mutex_t mutex_b;void *another (void *arg) {pthread_mutex_lock(&mutex_b);	/* 子線程上鎖 mutex_b */printf("in child thread, got mutex b, waiting for mutex a\n");sleep(5);++b;pthread_mutex_lock(&mutex_a);	/* 子線程上鎖 mutex_a */b += a++;pthread_mutex_unlock(&mutex_a);	/* 解鎖 */pthread_mutex_unlock(&mutex_b);pthread_exit(NULL);
}int main () {pthread_t id;pthread_mutex_init(&mutex_a, NULL);	/* 初始化互斥鎖 */pthread_mutex_init(&mutex_b, NULL);pthread_create(&id, NULL, another, NULL);	/* 創(chuàng)建線程 */pthread_mutex_lock(&mutex_a);	/* 主線程上鎖 mutex_a */printf("in parent thread, got mutex a, waiting for mutex b\n");sleep(5);++a;pthread_mutex_lock(&mutex_b);	/* 主線程上鎖 mutex_b */a += b++;pthread_mutex_unlock(&mutex_b);pthread_mutex_unlock(&mutex_a);/* 主線程等待子線程結(jié)束,然后銷毀互斥鎖以釋放資源 */pthread_join(id, NULL);pthread_mutex_destroy(&mutex_a);pthread_mutex_destroy(&mutex_b);return 0;
}

由于兩個線程都在等待對方已經(jīng)持有的鎖釋放,因此會發(fā)生死鎖,兩個線程都將永遠等待下去。 為了避免死鎖,應確保所有線程以相同的順序獲取互斥鎖。

編譯:-lpthread 選項確保鏈接了 POSIX 線程庫。

g++ -o test test.cpp -lpthread

img

8.讀寫鎖

1.原理

1.鎖只有一把。以讀方式給數(shù)據(jù)加鎖,那鎖就是讀鎖,以寫方式給數(shù)據(jù)加鎖,那鎖就是寫鎖。

2.讀共享,寫獨占。

3.寫鎖優(yōu)先級高。

  • 如果有五個進程同時請求鎖,1個寫請求4個讀請求,那么優(yōu)先給寫鎖。

  • 如果4個讀請求比寫請求先到,并且已經(jīng)加鎖成功,那么不會斷開讀請求的進程給寫請求的進程鎖的。

  • **如果讀鎖和寫鎖在同一隊列阻塞等待,那么優(yōu)先給寫鎖:**如果進程1是讀進程已經(jīng)加鎖成功在讀了,后邊同時來了3個進程,2,4進程寫請求,3進程讀請求,這個時候是這樣的:1讀完之后,2和4寫,寫完之后3再讀(寫優(yōu)先級高)

2.特性

  1. 讀寫鎖是“寫模式加鎖”時,解鎖前,所有對該鎖加鎖的線程都會被阻塞。

  2. 讀寫鎖是“讀模式加鎖”時,

  3. 讀寫鎖是“讀模式加鎖”時,既有試圖以寫模式加鎖的線程,也有試圖以讀模式加鎖的線程。那么讀寫鎖
    會阻塞隨后的讀模式鎖請求。優(yōu)先滿足寫模式鎖。讀鎖、寫鎖并行阻塞,寫鎖優(yōu)先級高

**讀寫鎖也叫共享-獨占鎖。**當讀寫鎖以讀模式鎖住時,它是以共享模式鎖住的;當它以寫模式鎖住時,它是以獨
占模式鎖住的。寫獨占、讀共享。

讀寫鎖非常適合于對數(shù)據(jù)結(jié)構(gòu)讀的次數(shù)遠大于寫的情況。

如果線程以讀模式對其加鎖會成功;如果線程以寫模式加鎖會阻塞。

相較于互斥量而言,當讀線程多的時候,提高訪問效率

3.對應函數(shù)

pthread_rwlock_t  rwlock;pthread_rwlock_init(&rwlock, NULL);pthread_rwlock_rdlock(&rwlock);		
pthread_rwlock_wrlock(&rwlock);	pthread_rwlock_tryrdlock(&rwlock);		
pthread_rwlock_trywrlock(&rwlock);	pthread_rwlock_unlock(&rwlock);
pthread_rwlock_destroy(&rwlock);

以上都是成功返回0失敗返回錯誤號

pthread_rwlock_t類型 用于定義一個讀寫鎖變量。

pthread rwlock t rwlock;
1.初始化和銷毀

pthread_rwlock_init 函數(shù)

初始化一把讀寫鎖

int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);

參 2:attr 表讀寫鎖屬性,通常使用默認屬性,傳 NULL 即可。

pthread_rwlock_destroy 函數(shù)

銷毀一把讀寫鎖

int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
2.加鎖解鎖

加鎖

pthread_rwlock_rdlock 函數(shù)

以讀方式請求讀寫鎖。(常簡稱為:請求讀鎖)

int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);

pthread_rwlock_wrlock 函數(shù)

以寫方式請求讀寫鎖。(常簡稱為:請求寫鎖)

int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);

pthread_rwlock_tryrdlock 函數(shù)

非阻塞以讀方式請求讀寫鎖(非阻塞請求讀鎖)

int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);

pthread_rwlock_trywrlock 函數(shù)

非阻塞以寫方式請求讀寫鎖(非阻塞請求寫鎖)

int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);

解鎖

pthread_rwlock_unlock 函數(shù)

int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

4.示例

/* 3個線程不定時 "寫" 全局資源,5個線程不定時 "讀" 同一全局資源 */#include <stdio.h>
#include <unistd.h>
#include <pthread.h>int counter;                          //全局資源
pthread_rwlock_t rwlock;void *th_write(void *arg)
{int t;int i = (int)arg;while (1) {t = counter;                    // 保存寫之前的值usleep(1000);pthread_rwlock_wrlock(&rwlock);printf("=======write %d: %lu: counter=%d ++counter=%d\n", i, pthread_self(), t, ++counter);pthread_rwlock_unlock(&rwlock);usleep(9000);               // 給 r 鎖提供機會}return NULL;
}void *th_read(void *arg)
{int i = (int)arg;while (1) {pthread_rwlock_rdlock(&rwlock);printf("----------------------------read %d: %lu: %d\n", i, pthread_self(), counter);pthread_rwlock_unlock(&rwlock);usleep(2000);                // 給寫鎖提供機會}return NULL;
}int main(void)
{int i;pthread_t tid[8];pthread_rwlock_init(&rwlock, NULL);for (i = 0; i < 3; i++)pthread_create(&tid[i], NULL, th_write, (void *)i);for (i = 0; i < 5; i++)pthread_create(&tid[i+3], NULL, th_read, (void *)i);for (i = 0; i < 8; i++)pthread_join(tid[i], NULL);pthread_rwlock_destroy(&rwlock);            //釋放讀寫瑣return 0;
}

9.條件變量

如果說互斥鎖是用于同步線程對共享數(shù)據(jù)的訪問,那么條件變量則是用于在線程之間同步共享數(shù)據(jù)的值。條件變量提供了一種線程間的通知機制:當某個共享數(shù)據(jù)達到某個值時,喚醒等待這個共享數(shù)據(jù)的線程。

條件變量是多線程編程中用于同步的一種機制,它允許線程在某些條件未被滿足時暫停執(zhí)行,并在條件滿足時被喚醒繼續(xù)執(zhí)行。條件變量通常與互斥鎖(mutexes)一起使用,以協(xié)調(diào)對共享資源的訪問。這是一種避免忙等(busy-waiting)并減少CPU資源浪費的有效方式。

1.工作原理

  1. 等待條件變量:當線程需要訪問某個共享資源,但條件不滿足時,它會通過互斥鎖保護條件變量,并在該條件變量上等待。在這個等待過程中,線程會釋放互斥鎖,以便其他線程可以修改這個條件。
  2. 喚醒等待的線程:其他線程在修改了條件之后,可以通過條件變量來喚醒一個或多個正在等待這個條件的線程。
  3. 重新檢查條件:被喚醒的線程會重新獲取互斥鎖,并再次檢查條件是否滿足。如果條件滿足,線程繼續(xù)執(zhí)行;如果不滿足,線程可能會再次等待。

2.對應函數(shù)

1.總覽

條件變量的相關函數(shù)如下:

#incldue <pthread.h>/* 初始化條件變量 */
int pthread_cond_init (pthread_cond_t* cond, const pthread_condattr_t* cond_attr);/* 銷毀條件變量 */
int pthread_cond_destroy (pthread_cond_t* cond);/* 以廣播方式喚醒所有等待目標條件的線程 */
int pthread_cond_broadcast (pthread_cond_t* cond);/* 喚醒一個等待目標條件變量的線程,喚醒哪個線程取決于線程的優(yōu)先級和調(diào)度策略 */
int pthread_cond_signal (pthread_cond_t* cond);/* 等待目標條件變量 */
int pthread_cond_wait (ptread_cond_t* cond, pthread_mutex_t* mutex);/*限時等待一個條件變量*/
int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);
pthread_cond_t 類型 用于定義條件變量
pthread_cond_t cond;

成功返回0失敗直接返回錯誤號

pthread_cond_signal(): 喚醒阻塞在條件變量上的 (至少)一個線程。
pthread_cond_broadcast(): 喚醒阻塞在條件變量上的 所有線程。
2.創(chuàng)建和銷毀

創(chuàng)建

/* 初始化條件變量 */
int pthread_cond_init (pthread_cond_t* cond, const pthread_condattr_t* cond_attr);

參 2:attr 表條件變量屬性,通常為默認值,傳 NULL 即可

也可以使用靜態(tài)初始化的方法,初始化條件變量:

pthread_cond_t cond = PTHREAD_COND_INITIALIZER

銷毀

 /* 銷毀條件變量 */
int pthread_cond_destroy (pthread_cond_t* cond);
3.wait函數(shù)

阻塞等待一個條件變量

int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);

參數(shù)

cond:條件變量

mutex:互斥鎖

函數(shù)作用:

  1. 阻塞等待條件變量 cond(參 1)滿足

  2. 釋放已掌握的互斥鎖(解鎖互斥量)相當于 pthread_mutex_unlock(&mutex);

? **1.2.**兩步為一個原子操作。

  1. 當被喚醒,pthread_cond_wait 函數(shù)返回時,解除阻塞并重新申請獲取互斥鎖 pthread_mutex_lock(&mutex);

image-20241219195113151

這張圖是對wait過程的說明:

1.鎖是提前創(chuàng)建和初始化好的,然后加鎖

2.調(diào)用pthread_cond_wait,看條件變量是否滿足

3.不滿足就阻塞等待,阻塞等待的時候就把鎖給解了,讓別人用去了(判斷阻塞和解鎖這兩步是一個原子操作)

4.等到滿足條件變量滿足的時候,再申請重新加鎖(重新加鎖是條件變量內(nèi)部實現(xiàn),不需要咱們自己加鎖)

4.pthread_cond_timedwait 函數(shù)

限時等待一個條件變量

int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);

參 3:

參看 man sem_timedwait 函數(shù),查看 struct timespec 結(jié)構(gòu)體。

struct timespec {
time_t tv_sec; 
/* seconds */long tv_nsec; 
/* nanosecondes*/ 納秒
}

形參 abstime:絕對時間。

如:time(NULL)返回的就是絕對時間。而 alarm(1)是相對時間,相對當前時間定時 1 秒鐘。

struct timespec t = {1, 0};

pthread_cond_timedwait (&cond, &mutex, &t); 只能定時到 1970 年 1 月 1 日 00:00:01 秒(早已經(jīng)過去)

正確用法:

time_t cur = time(NULL); //獲取當前時間。struct timespec t; //定義 timespec 結(jié)構(gòu)體變量 tt.tv_sec = cur+1; //定時 1 秒pthread_cond_timedwait (&cond, &mutex, &t); //傳參 

在講解 setitimer 函數(shù)時我們還提到另外一種時間類型:

struct timeval {
time_t tv_sec; /* seconds */susecods_t tv_usec; /* microseconds */ 微秒
};

3.使用條件變量模擬實現(xiàn)生產(chǎn)者—消費者問題

流程

image-20241219200446922

完整代碼

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>void err_thread(int ret, char *str)
{if (ret != 0) {fprintf(stderr, "%s:%s\n", str, strerror(ret));pthread_exit(NULL);}
}//魔方公共區(qū)域的鏈表
struct msg {int num;struct msg *next;
};struct msg *head;pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;      // 定義/初始化一個互斥量
pthread_cond_t has_data = PTHREAD_COND_INITIALIZER;      // 定義/初始化一個條件變量void *produser(void *arg)
{while (1) {struct msg *mp = malloc(sizeof(struct msg));mp->num = rand() % 1000 + 1;                        // 模擬生產(chǎn)一個數(shù)據(jù)`printf("--produce %d\n", mp->num);pthread_mutex_lock(&mutex);                         // 加鎖 互斥量mp->next = head;                                    // 寫公共區(qū)域head = mp;pthread_mutex_unlock(&mutex);                       // 解鎖 互斥量pthread_cond_signal(&has_data);                     // 喚醒阻塞在條件變量 has_data上的線程.sleep(rand() % 3);}return NULL;
}void *consumer(void *arg)
{while (1) {struct msg *mp;pthread_mutex_lock(&mutex);                         // 加鎖 互斥量while (head == NULL) {pthread_cond_wait(&has_data, &mutex);           // 阻塞等待條件變量, 解鎖}                                                   // pthread_cond_wait 返回時, 重新加鎖 mutexmp = head;head = mp->next;pthread_mutex_unlock(&mutex);                       // 解鎖 互斥量printf("---------consumer id: %lu :%d\n", pthread_self(), mp->num);free(mp);sleep(rand()%3);}return NULL;
}int main(int argc, char *argv[])
{int ret;pthread_t pid, cid;srand(time(NULL));ret = pthread_create(&pid, NULL, produser, NULL);           // 生產(chǎn)者if (ret != 0) err_thread(ret, "pthread_create produser error");ret = pthread_create(&cid, NULL, consumer, NULL);           // 消費者if (ret != 0) err_thread(ret, "pthread_create consuer error");ret = pthread_create(&cid, NULL, consumer, NULL);           // 消費者if (ret != 0) err_thread(ret, "pthread_create consuer error");ret = pthread_create(&cid, NULL, consumer, NULL);           // 消費者if (ret != 0) err_thread(ret, "pthread_create consuer error");pthread_join(pid, NULL);pthread_join(cid, NULL);return 0;
}

在公共區(qū)域為空的時候,消費者都會阻塞等待hasdata這個條件變量,都會把鎖釋放掉,而生產(chǎn)者這時會拿到鎖進行生產(chǎn),生產(chǎn)者生產(chǎn)完喚醒消費者進行消費。

4.條件變量優(yōu)勢

相較于 mutex 而言,條件變量可以減少競爭。

如直接使用 mutex,除了生產(chǎn)者、消費者之間要競爭互斥量以外,消費者之間也需要競爭互斥量,但如果匯(鏈表)中沒有數(shù)據(jù),消費者之間競爭互斥鎖是無意義的。有了條件變量機制以后,只有生產(chǎn)者完成生產(chǎn),才會引起消費者之間的競爭。提高了程序效率。

9.POSIX 信號量

1.概述

在Linux上,信號量API有兩組,一組是System V IPC信號量(信號量),另一組是我們要討論的POSIX信號量。這兩組接口很相似,且語義完全相同,但不保證能互換。

進化版的互斥鎖(1 --> N)

由于互斥鎖的粒度比較大,如果我們希望在多個線程間對某一對象的部分數(shù)據(jù)進行共享,使用互斥鎖是沒有辦

法實現(xiàn)的,只能將整個數(shù)據(jù)對象鎖住。這樣雖然達到了多線程操作共享數(shù)據(jù)時保證數(shù)據(jù)正確性的目的,卻無形中導

致線程的并發(fā)性下降。線程從并行執(zhí)行,變成了串行執(zhí)行。與直接使用單進程無異。

信號量,是相對折中的一種處理方式,既能保證同步,數(shù)據(jù)不混亂,又能提高線程并發(fā)

POSIX信號量函數(shù)的名字都以sem_開頭,不像大多線程函數(shù)那樣以pthread_開頭。常用的POSIX信號量函數(shù)如下:

#include <semaphore.h>
int sem_init(sem_t* sem, int pshared, unsigned int value);	/* 初始化一個未命名信號量 */
int sem_destory(sem_t* sem);								/* 銷毀信號量 */
int sem_wait(sem_t* sem);									/* 以原子操作的方式將信號量的值減1 */
int sem_trywait(sem_t *sem);								/* 相當于sem_wait函數(shù)的非阻塞版本 */
int sem_post(sem_t *sem);									/* 以原子操作的方式將信號量的值加1 */

上圖中函數(shù)的第一個參數(shù)sem指向被操作的信號量。

  • sem_init函數(shù)用于初始化一個未命名信號量(POSIX信號量API支持命名信號量,但本書不討論)。pshared參數(shù)指定信號量類型,如果值為0,就表示這個信號量是當前進程的局部信號量,否則該信號量可以在多個進程間共享。value參數(shù)指定信號量的初始值。初始化一個已經(jīng)被初始化的信號量將導致不可預期的結(jié)果。
  • sem_destroy函數(shù)用于銷毀信號量,以釋放其占用的內(nèi)核資源,銷毀一個正被其他線程等待的信號量將導致不可預期的結(jié)果。
  • sem_wait函數(shù)以原子操作的方式將信號量的值減1,如果信號量的值為0,則sem_wait函數(shù)將被阻塞,直到這個信號量具有非0值。
  • sem_trywait函數(shù)與sem_wait函數(shù)類似,但它始終立即返回,而不論被操作的信號量是否具有非0值,相當于sem_wait函數(shù)的非阻塞版本。當信號量非0時,sem_trywait函數(shù)對信號量執(zhí)行減1操作,當信號量的值為0時,該函數(shù)返回-1并設置errno為EAGAIN。
  • sem_post函數(shù)以原子操作的方式將信號量的值加1,當信號量的值從0變?yōu)?時,其他正在調(diào)用sem_wait等待信號量的線程將被喚醒。

上圖中的函數(shù)成功時返回0,失敗則返回-1并設置errno。

2.對應函數(shù)

1.總覽
 #include<semaphore.h>
sem_t sem;
sem_init 函數(shù)
sem_destroy 函數(shù)
sem_wait 函數(shù)
sem_trywait 函數(shù)
sem_timedwait 函數(shù)
sem_post 函數(shù)

以上 6 個函數(shù)的返回值都是:成功返回 0, 失敗返回-1,同時設置 errno。(注意,它們沒有 pthread 前綴)

sem_t 類型,本質(zhì)仍是結(jié)構(gòu)體。但應用期間可簡單看作為整數(shù),忽略實現(xiàn)細節(jié)(類似于使用文件描述符)。

規(guī)定信號量 sem 不能 < 0。

信號量基本操作:

sem_wait:

1.信號量大于 0,則信號量-- (類比 pthread_mutex_lock)

2.信號量等于 0,造成線程阻塞

對應

sem_post: 將信號量++,同時喚醒阻塞在信號量上的線程 (類比 pthread_mutex_unlock)

但,由于 sem_t 的實現(xiàn)對用戶隱藏,所以所謂的++、–操作只能通過函數(shù)來實現(xiàn),而不能直接++、–符號。

信號量的初值,決定了占用信號量的線程的個數(shù)。

2.初始化和銷毀

初始化一個信號量

int sem_init(sem_t *sem, int pshared, unsigned int value);

參 1:sem 信號量

參 2:pshared 取 0 用于線程間;取非 0(一般為 1)用于進程間

參 3:value 指定信號量初值

sem_destroy 函數(shù)

銷毀一個信號量

int sem_destroy(sem_t *sem);
3.PV操作主要函數(shù)

sem_wait 函數(shù)

給信號量加鎖 –

int sem_wait(sem_t *sem);

sem_post 函數(shù)

給信號量解鎖 ++

int sem_post(sem_t *sem);

sem_trywait 函數(shù)

嘗試對信號量加鎖 – (與 sem_wait 的區(qū)別類比 lock 和 trylock)

int sem_trywait(sem_t *sem);

sem_timedwait 函數(shù)

限時嘗試對信號量加鎖 –

int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);

參 2:abs_timeout 采用的是絕對時間。

定時 1 秒:
time_t cur = time(NULL); 獲取當前時間。
struct timespec t; 定義 timespec 結(jié)構(gòu)體變量 t
t.tv_sec = cur+1; 定時 1 秒
t.tv_nsec = t.tv_sec +100; 
sem_timedwait(&sem, &t); 傳參

3.實現(xiàn)生產(chǎn)者消費者

流程

image-20241219212728460

完整代碼

/*信號量實現(xiàn) 生產(chǎn)者 消費者問題*/#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <stdio.h>
#include <semaphore.h>#define NUM 5               int queue[NUM];                                     //全局數(shù)組實現(xiàn)環(huán)形隊列
sem_t blank_number, product_number;                 //空格子信號量, 產(chǎn)品信號量void *producer(void *arg)
{int i = 0;while (1) {sem_wait(&blank_number);                    //生產(chǎn)者將空格子數(shù)--,為0則阻塞等待queue[i] = rand() % 1000 + 1;               //生產(chǎn)一個產(chǎn)品printf("----Produce---%d\n", queue[i]);        sem_post(&product_number);                  //將產(chǎn)品數(shù)++i = (i+1) % NUM;                            //借助下標實現(xiàn)環(huán)形sleep(rand()%1);}
}void *consumer(void *arg)
{int i = 0;while (1) {sem_wait(&product_number);                  //消費者將產(chǎn)品數(shù)--,為0則阻塞等待printf("-Consume---%d\n", queue[i]);queue[i] = 0;                               //消費一個產(chǎn)品 sem_post(&blank_number);                    //消費掉以后,將空格子數(shù)++i = (i+1) % NUM;sleep(rand()%3);}
}int main(int argc, char *argv[])
{pthread_t pid, cid;sem_init(&blank_number, 0, NUM);                //初始化空格子信號量為5, 線程間共享 -- 0sem_init(&product_number, 0, 0);                //產(chǎn)品數(shù)為0pthread_create(&pid, NULL, producer, NULL);pthread_create(&cid, NULL, consumer, NULL);pthread_join(pid, NULL);pthread_join(cid, NULL);sem_destroy(&blank_number);sem_destroy(&product_number);return 0;
}

10.線程同步機制包裝類

為了充分復用代碼,同時后文需要,我們將前面討論的三種線程同步機制分別封裝為三個類,實現(xiàn)在locker.h頭文件中:

#ifndef LOCKER_H
#define LOCKER_H#include <exception>
#include <pthread.h>
#include <semaphore.h>// 封裝信號量的類
class sem {
public:// 創(chuàng)建并初始化信號量sem() {if (sem_init(&m_sem, 0, 0) != 0) {// 構(gòu)造函數(shù)沒有返回值,可通過拋出異常來報告錯誤throw std::exception();}}// 銷毀信號量~sem() {sem_destroy(&m_sem);}// 等待信號量bool wait() {return sem_wait(&m_sem) == 0;}// 增加信號量bool post() {return sem_post(&m_sem) == 0;}private:sem_t m_sem;
};// 封裝互斥鎖的類
class locker {
public:// 創(chuàng)建并初始化互斥鎖locker() {if (pthread_mutex_init(&m_mutex, NULL) != 0) {throw std::exception();}}// 銷毀互斥鎖~locker() {pthread_mutex_destroy(&m_mutex);}// 獲取互斥鎖bool lock() {return pthread_mutex_lock(&m_mutex) == 0;}// 釋放互斥鎖bool unlock() {return pthread_mutex_unlock(&m_mutex) == 0;}private:pthread_mutex_t m_mutex;
};// 封裝條件變量的類
class cond {
public:// 創(chuàng)建并初始化條件變量cond() {if (pthread_mutex_init(&m_mutex, NULL) != 0) {throw std::exception();}if (pthread_cond_init(&m_cond, NULL) != 0) {// 構(gòu)造函數(shù)中一旦出現(xiàn)問題,就應立即釋放已經(jīng)成功分配的資源pthread_mutex_destroy(&m_mutex);throw std::exception();}}// 銷毀條件變量~cond() {pthread_mutex_destroy(&m_mutex);pthread_cond_destroy(&m_cond);}// 等待條件變量bool wait() {int ret = 0;// 作者在此處對互斥鎖加鎖,保護了什么?這導致其他人無法使用該封裝類pthread_mutex_lock(&m_mutex);ret = pthread_cond_wait(&m_cond, &m_mutex);pthread_mutex_unlock(&m_mutex);return ret == 0;}// 喚醒等待條件變量的線程bool signal() {return pthread_cond_signal(&m_cond) == 0;}private:pthread_mutex_t m_mutex;pthread_cond_t m_cond;
};#endif

11.多線程環(huán)境

1.可重入函數(shù)

如果一個函數(shù)能被多個線程同時調(diào)用且不發(fā)生競態(tài)條件,則我們稱它是線程安全的(thread safe),或者說它是可重入函數(shù)。Linux庫函數(shù)只有一小部分是不可重入的。這些庫函數(shù)之所以不可重入,主要是因為其內(nèi)部使用了靜態(tài)變量,但Linux對很多不可重入的庫函數(shù)提供了對應的可重入版本,這些可重入版本的函數(shù)名是在原函數(shù)名尾部加上_r,如localtime函數(shù)對應的可重入函數(shù)是localtime_r。

在多線程程序中調(diào)用庫函數(shù),一定要使用其可重入版本,否則可能導致預想不到的結(jié)果。

2.進程和線程

多線程環(huán)境中,使用fork調(diào)用產(chǎn)生的死鎖問題

如果多線程的某個線程(可以理解為一個進程)調(diào)用了fork函數(shù),那么新創(chuàng)建的子進程只擁有一個執(zhí)行線程,它是調(diào)用fork的那個線程的完整復制,且子進程將自動繼承父進程中互斥鎖、條件變量的狀態(tài),即父進程中已被加鎖的互斥鎖在子進程中也是被鎖住的,這就引起了一個問題:子進程可能不清楚從父進程繼承而來的互斥鎖的具體狀態(tài)(是加鎖還是解鎖狀態(tài)),這個互斥鎖可能被加鎖了,但不是由調(diào)用fork的線程鎖住的,而是由其他線程鎖住的,此時,子進程若再次對該互斥鎖加鎖會導致死鎖,如以下代碼所示:

#include <pthread.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <wait.h>pthread_mutex_t mutex;/* 子線程運行的函數(shù),它首先獲得互斥鎖 mutex ,然后暫停5s,再釋放該互斥鎖 */
void *another(void *arg) {printf("in child thread, lock the mutex\n");pthread_mutex_lock(&mutex);sleep(5);pthread_mutex_unlock(&mutex);
}int main() {pthread_mutex_init(&mutex, NULL);pthread_t id;pthread_create(&id, NULL, another, NULL);/* 父進程中的主線程暫停1s,以確保在執(zhí)行 fork 前,子線程已經(jīng)開始運行并獲得了互斥量 mutex */sleep(1);int pid = fork();if (pid < 0) {pthread_join(id, NULL);pthread_mutex_destroy(&mutex);return 1;} else if (pid == 0) {printf("I am in the child, want to get the lock\n");/* 子進程從父進程繼承了互斥鎖 mutex 的狀態(tài),該互斥鎖處于鎖住的狀態(tài) *//* 這是由父進程中的子線程執(zhí)行 pthread_mutex_lock 引起的,因此以下加鎖操作會一直阻塞 *//* 盡管從邏輯上來說它是不應該阻塞的 */pthread_mutex_lock(&mutex);printf("I can not run to here, oop...\n");pthread_mutex_unlock(&mutex);exit(0);} else {wait(NULL);}pthread_join(id, NULL);pthread_mutex_destroy(&mutex);return 0;
}

關鍵點

  1. 子線程加鎖:在主線程(進程)中創(chuàng)建的子線程首先獲取互斥鎖并休眠5秒。
  2. fork 調(diào)用:在子線程獲取互斥鎖后,主線程休眠1秒以確保子線程鎖定互斥鎖,然后調(diào)用 fork。fork 之后,父進程和子進程都有一個拷貝的互斥鎖狀態(tài)。
  3. 子進程中的鎖行為:由于 fork 后子進程繼承了互斥鎖的狀態(tài),如果該鎖被鎖定,子進程中的互斥鎖也將處于鎖定狀態(tài)。不同的是,子進程中并沒有線程擁有這個鎖(因為鎖的擁有者是父進程的一個線程),因此嘗試獲取這個鎖將會導致子進程永久阻塞。

效果:子進程被阻塞。

image-20241219221429647

不過,pthread提供了一個專門的函數(shù)pthread_atfork,以確保fork調(diào)用后父進程和子進程都擁有一個清楚的鎖狀態(tài):

#include <pthread.h>
int pthread_atfork(void (*prepare)(void), void (*parent)(void), void (*child) (void));

pthread_atfork函數(shù)將建立3個fork句柄幫助我們清理互斥鎖的狀態(tài)。

  • prepare句柄將在fork函數(shù)創(chuàng)建出子進程前被執(zhí)行,它可以用來鎖住父進程中的互斥鎖。
  • parent句柄是fork函數(shù)創(chuàng)建出子進程后,fork函數(shù)返回前,在父進程中被執(zhí)行,它的作用是釋放所有在prepare句柄中被鎖住的互斥鎖。
  • child句柄是在fork函數(shù)返回前,在子進程中執(zhí)行,它和parent句柄一樣,也是用于釋放所有在prepare句柄中被鎖住的互斥鎖。

該函數(shù)成功時返回0,失敗則返回錯誤碼。

要讓以上代碼正常工作,需要在fork調(diào)用前加上以下代碼:

void prepare () {pthread_mutex_lock ( &mutex );
}void infork () {pthread_mutex_unlock ( &mutex );
}pthread_atfork ( prepare, infork, infork );

效果:未發(fā)生死鎖。

image-20241219221454321

人話翻譯一下它這三個函數(shù)在干什么
  1. 第一段代碼導致死鎖的原因
    • 父子進程共享互斥鎖狀態(tài)導致阻塞:
      • 在第一段代碼中,首先創(chuàng)建了一個互斥鎖mutex,并在子線程中獲取了這個互斥鎖(pthread_mutex_lock(&mutex)),然后線程休眠 5 秒。接著在父進程中執(zhí)行fork操作。
      • 子進程會繼承父進程中互斥鎖的狀態(tài),此時互斥鎖在子進程中仍然是被鎖定的狀態(tài),因為它是從父進程繼承過來的。當子進程試圖再次獲取已經(jīng)被鎖定的互斥鎖(pthread_mutex_lock(&mutex))時,根據(jù)互斥鎖的默認行為(這里假設是標準互斥鎖PTHREAD_MUTEX_NORMAL),它會被阻塞,等待鎖被釋放。但是這個鎖不會被釋放,因為在子進程中沒有其他線程可以釋放它(子進程中獲取鎖的代碼還沒執(zhí)行成功),從而導致死鎖。
  2. 第二段代碼不會死鎖的原因
    • pthread_atfork函數(shù)的作用:
      • 在第二段代碼中,添加了pthread_atfork相關的函數(shù)prepareinfork。pthread_atfork函數(shù)的作用是在fork操作前后對互斥鎖進行特殊處理,以避免父子進程之間由于互斥鎖狀態(tài)不一致而導致的問題。
      • fork操作之前,prepare函數(shù)會被調(diào)用,它會獲取互斥鎖(pthread_mutex_lock(&mutex))。這里需要注意的是,pthread庫對pthread_atfork中的prepare操作可能會有特殊處理,即使互斥鎖已經(jīng)被獲取(被父進程中的子線程獲取),這個操作也不會導致死鎖,而是將互斥鎖狀態(tài)穩(wěn)定在被鎖定狀態(tài)。
      • fork操作完成后,在子進程中infork函數(shù)會被調(diào)用,它會釋放互斥鎖(pthread_mutex_unlock(&mutex))。這樣就使得子進程從一個互斥鎖已經(jīng)被解鎖的狀態(tài)開始,當子進程后續(xù)嘗試獲取互斥鎖(pthread_mutex_lock(&mutex))時,就不會像第一段代碼那樣被阻塞,從而避免了死鎖。
  3. 兩段代碼的主要區(qū)別
    • 互斥鎖狀態(tài)處理方式:
      • 第一段代碼沒有對fork操作前后的互斥鎖狀態(tài)進行特殊處理,子進程繼承了父進程中被鎖定的互斥鎖狀態(tài),并且由于再次嘗試獲取已鎖定的互斥鎖而導致死鎖。
      • 第二段代碼通過pthread_atfork函數(shù)及其相關的prepareinfork函數(shù),在fork操作前后對互斥鎖狀態(tài)進行了清理和調(diào)整,使得子進程從一個合理的、互斥鎖已解鎖的狀態(tài)開始,避免了子進程因繼承不適當?shù)幕コ怄i狀態(tài)而導致的死鎖問題。
總結(jié)

就是pthread_atfork函數(shù)在進入子進程前會獲取一下父進程鎖的狀態(tài),不管有沒有被鎖咱給它鎖上,如果被鎖了,我們知道互斥鎖被鎖了以后再次加鎖會導致死鎖,可能它函數(shù)內(nèi)部設計避免了這種情況。然后再把咱鎖上的通通解鎖,再去執(zhí)行子進程代碼。

相當于子進程代碼的初始狀態(tài)就是任何鎖都沒有被加鎖的情況,就避免了死鎖的發(fā)生。

3.線程和信號

每個線程都能獨立設置信號掩碼,進程設置信號掩碼的函數(shù)是sigprocmask(見信號掩碼),但在多線程環(huán)境下應使用pthread_sigmask函數(shù)設置信號掩碼:

#include <pthread.h>
#include <signal.h>
int pthread_sigmask ( int how, const sigset_t* newmask, sigset_t* oldmask );

pthread_sigmask函數(shù)的參數(shù)與sigprocmask函數(shù)的參數(shù)完全相同。pthread_sigmask函數(shù)成功時返回0,失敗返回錯誤碼。

由于進程中所有線程共享該進程的信號,所以線程庫將根據(jù)線程掩碼決定把信號發(fā)送給哪個具體的線程。而且,所有線程共享信號處理函數(shù),當我們在一個線程中設置了某個信號的信號處理函數(shù)后,它將覆蓋其他線程為同一信號設置的信號處理函數(shù)。

因此,我們應該定義一個專門的線程來處理所有信號,這可通過以下兩個步驟實現(xiàn):

  1. 在主線程創(chuàng)建出其他子線程前就調(diào)用pthread_sigmask來設置好信號掩碼,所有新創(chuàng)建的子線程將自動繼承這個信號掩碼,這樣,所有線程都不會響應被屏蔽的信號了。
  2. 在某個線程中調(diào)用以下函數(shù)等待信號并處理:
#include <signal.h>
int sigwait ( const sigset_t* set, int* sig );

set參數(shù)指定要等待的信號的集合,我們可以將其指定為在第 1 步中創(chuàng)建的信號掩碼,表示在該線程中等待所有被屏蔽的信號。參數(shù)sig指向的整數(shù)用于存儲該函數(shù)返回的信號值。sigwait成功時返回0,失敗則返回錯誤碼。一旦sigwait函數(shù)成功返回,我們就能對收到的信號做處理了,顯然,如果我們使用了sigwait函數(shù),就不應再為信號設置信號處理函數(shù)了。

pthread還提供了pthread_kill函數(shù),使我們可以把信號發(fā)送給指定線程:

#include <signal.h>
int pthread_kill ( pthread_t thread, int sig );

thread參數(shù)指定目標線程。sig參數(shù)指定待發(fā)送信號,如果sig參數(shù)為0,則pthread_kill不發(fā)送信號,但它仍會進行錯誤檢查。我們可用此方法檢查目標線程是否存在。pthread_kill函數(shù)成功時返回0,失敗則返回錯誤碼。

在一個線程中統(tǒng)一處理所有信號

以下代碼取自pthread_sigmask函數(shù)的man手冊,它展示了如何通過以上兩個步驟實現(xiàn)在一個線程中統(tǒng)一處理所有信號:

主線程設置了一個信號掩碼來阻塞特定的信號(在這個例子中是SIGQUITSIGUSR1),然后創(chuàng)建一個專門的線程來處理這些信號。這種模式是處理多線程環(huán)境中信號的推薦方式,因為它避免了信號處理和線程執(zhí)行之間的競爭條件。

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>// perror函數(shù)根據(jù)全局errno值打印其相應的錯誤信息到標準錯誤
#define handle_error_en(en, msg) \ do { errno = en; perror(msg); exit(EXIT_FAILURE); } while (0)/* 在 sig_thread 函數(shù)中,線程循環(huán)調(diào)用 sigwait 來等待信號 */
static void *sig_thread(void *arg) {sigset_t *set = (sigset_t *)arg;int s, sig;for (; ; ) {// 第二步,調(diào)用sigwait等待信號s = sigwait(set, &sig);if (s != 0) {handle_error_en(s, "sigwait");}printf("Signal handling thread got signal %d\n", sig);}
}int main(int argc, char *argv[]) {printf("The PID of this process is: %d\n", getpid()); /* 獲取進程 PID */pthread_t thread;			/* 線程 */sigset_t set;				/* 信號集 */int s;/* 第一步,在主線程中設置信號掩碼,信號集set被初始化并添加了SIGQUIT和SIGUSR1信號: */sigemptyset(&set);sigaddset(&set, SIGQUIT);sigaddset(&set, SIGUSR1);/* 使用 pthread_sigmask 來阻塞這些信號 */s = pthread_sigmask(SIG_BLOCK, &set, NULL);if (s != 0) {handle_error_en(s, "pthread_sigmask");}/* 創(chuàng)建處理信號的線程 */s = pthread_create(&thread, NULL, &sig_thread, (void *)&set);if (s != 0) {handle_error_en(s, "thread_create");}pause();
}

運行程序,并在另一個終端中使用kill命令發(fā)送SIGQUITSIGUSR1信號到程序。例如:

kill -SIGQUIT [pid]
kill -SIGUSR1 [pid]
http://www.risenshineclean.com/news/2848.html

相關文章:

  • wordpress 做公司網(wǎng)站網(wǎng)絡營銷的推廣
  • 網(wǎng)站下雪的效果怎么做的一鍵制作網(wǎng)站
  • 個人網(wǎng)站模板之家吳中seo網(wǎng)站優(yōu)化軟件
  • 網(wǎng)站做采集會有問題么寧波最好的推廣平臺
  • 哪家專門做特賣網(wǎng)站百度搜索網(wǎng)頁
  • wordpress 簡約windows優(yōu)化大師最新版本
  • 常熟網(wǎng)站建設icp備案長沙網(wǎng)址seo
  • 最專業(yè)的佛山網(wǎng)站建設牛排seo系統(tǒng)
  • 做的網(wǎng)站晚上偷偷看b站軟件推薦
  • 網(wǎng)站建設網(wǎng)絡推廣加盟蘇州seo公司
  • 網(wǎng)站做線旅游景區(qū)網(wǎng)絡營銷案例
  • 新疆建設職業(yè)培訓中心網(wǎng)站線上營銷推廣方案有哪些
  • 赤峰浩誠網(wǎng)站建設有限公司百度店鋪
  • 長白山網(wǎng)站學做管理下載百度2023最新版
  • 網(wǎng)站成本廣告公司網(wǎng)站制作
  • wap手機百度seo排名工具
  • 網(wǎng)站聊天系統(tǒng)怎么做友情網(wǎng)站
  • 住房和城鄉(xiāng)建設部電工證廊坊優(yōu)化技巧
  • 網(wǎng)頁使用怎么做太原seo推廣
  • 袁隆平網(wǎng)站設計模板貴州seo和網(wǎng)絡推廣
  • wordpress主題轉(zhuǎn)zblog網(wǎng)站網(wǎng)頁的優(yōu)化方法
  • 做高仿批發(fā)的網(wǎng)站有哪些百度熱門排行榜
  • 如何用nat123做網(wǎng)站做網(wǎng)站用哪個軟件
  • 南通營銷型網(wǎng)站建設自媒體平臺注冊官網(wǎng)
  • 網(wǎng)站后臺上傳圖片 不可用提高網(wǎng)站排名
  • 個人網(wǎng)站怎么申請注冊同城推廣
  • 學做網(wǎng)站學什么語言最成功的網(wǎng)絡營銷案例
  • 家里電腦如何做網(wǎng)站競價推廣賬戶競價托管費用
  • 迪慶企業(yè)網(wǎng)站建設seo待遇
  • 宜和購物電視購物官方網(wǎng)站??诰W(wǎng)站關鍵詞優(yōu)化