【Linux】多線程1——線程概念與線程控制

文章目錄

  • 1. 線程概念
    • 什么是線程
    • Linux中的線程
    • 線程的優點
    • 線程的缺點
    • 線程的獨立資源和共享資源
  • 2. 線程控制
    • Linux的pthread庫
    • 用戶級線程


  • 📝 個人主頁 :超人不會飛)
  • 📑 本文收錄專欄:《Linux》
  • 💭 如果本文對您有幫助,不妨點贊、收藏、關注支持博主,我們一起進步,共同成長!

1. 線程概念

什么是線程

💭理解線程需要和進程的概念緊密聯系。

  1. 線程是一個執行分支,執行粒度比進程更細,調度成本更低;
  2. 進程是分配系統資源的基本單位,線程是CPU調度的基本單位。
  3. 線程是運行在進程中的一個執行流,本質上是在進程的地址空間中運行,一個進程至少包含一個線程,稱為主線程。

Linux中的線程

線程是操作系統中的抽象概念,用于實現多任務并發執行。不同的操作系統可以有不同的線程實現方法和模型。例如,在Windows操作系統中,與進程PCB對標的,構建了描述線程的數據結構 —— 線程控制塊,但這樣子設計有以下幾個缺點:

  1. 創建線程在Windows中開銷較大,因為它涉及到較多的內核資源和數據結構的分配
  2. 線程與進程無法統一組織起來
  3. 線程的調度效率低

Linux的設計者發現,線程控制塊與進程控制塊(PCB)大部分描述屬性相同,且進程與其內部創建的線程看到的都是同一個地址空間。因此,在Linux中,線程控制塊直接復用了PCB的代碼,也就是說,Linux底層并沒有真正的“線程”,這種復用之后的線程稱之為輕量級進程

在這里插入圖片描述

  • 每個輕量級進程(后面直接稱為線程)都有自己的一個編號——LWP,同一個進程中的各個線程具有相同的PID。

🔎那我們之前討論的進程是什么?這里都是輕量級進程的話,需要另有一個進程PCB來管理整個進程嗎?

答案是不用。事實上,在Linux中,因為每個進程都至少有一個線程,即主線程(主執行流),這個線程的LWP和PID是相同的,因此,我們之前討論的進程PCB,實際上就是這個主線程的task_struct。

ps -aL命令查看系統中的輕量級進程。

測試:在一個進程中,創建了10個線程,并用ps -aL命令查看。可以看到有一個主線程和10個新線程,主線程的PID和LWP相同。

在這里插入圖片描述

  • 線程的調度成本低于進程,是因為同一個進程中的線程共享同一個地址空間,因此這些線程的調度只需要保存和更改一些上下文信息、CPU寄存器即可,如pc指針。而進程的調度需要修改較多的內存資源,如頁表、地址空間等,而開銷更大的是修改cache緩存的數據。

    cache緩存

    CPU內部的高速存儲器中,保存著一些頻繁訪問的指令和數據,基于局部性原理,這些數據可能是未來將要被訪問的,也可能是當前正在訪問的。這么做的目的是減少CPU與內存的IO次數,以便快速響應CPU的請求,而不必每次都從較慢的內存中獲取數據。不同進程的cache緩存數據是不同的,因此調度進程是需要切換這部分數據,而同一個進程的不同線程的cache緩存相同。

    CPU根據PID和LWP的對比,區分當前調度是線程級還是進程級,進而執行對應的調度策略。

線程的優點

  1. 線程占用的資源比進程少很多,因此創建線程的開銷比創建進程小
  2. 線程的調度成本低于進程調度,線程切換時OS的工作量小
  3. 充分利用多處理器的可并行數量
  4. 在等待慢速I/O操作結束的同時,程序可執行其他的計算任務
  5. 計算密集型應用,為了能在多處理器系統上運行,將計算分解到多個線程中實現
  6. I/O密集型應用,為了提高性能,將I/O操作重疊。線程可以同時等待不同的I/O操作。

線程的缺點

  1. 性能損失。 一個很少被外部事件阻塞的計算密集型線程往往無法與其它線程共享同一個處理器。 如果計算密集型線程的數量比可用的處理器多,那么可能會有較大的性能損失,這里的性能損失指的是增加了額外的同步和調度開銷,而可用的資源不變。 例如有10個處理器,11個線程,一對一的關系被破壞后,多出來的線程就增加了額外的調度開銷。
  2. 復雜性和錯誤難以調試。 多線程編程涉及到共享資源、并發訪問和同步等問題,這增加了程序的復雜性。
  3. 健壯性降低。 編寫多線程需要更全面更深入的考慮,在一個多線程程序里,因時間分配上的細微偏差或者因共享了不該共享的變量而造成不良影響的可能性是很大的,換句話說多線程之間是缺乏保護的。

?補充:

線程發生異常(如野指針、除零錯誤等),會導致線程崩潰,進而引發整個進程退出。從宏觀角度,因為線程是進程的一個執行分支,線程干的事就是進程干的事,因此線程異常相當于進程異常,進程就會退出。從內核角度,線程出錯,OS發送信號給進程,而不是單發給線程。

線程的獨立資源和共享資源

進程是資源分配的基本單位,線程是調度的基本單位。一個進程中的多個線程共享線程數據,當然也有自己獨立的數據。

線程的獨立資源:

  • 寄存器中的上下文信息
  • 線程ID(在Linux中表現為LWP)
  • errno
  • 信號屏蔽字和未決信號集
  • 調度優先級

線程的共享資源:

  • 進程地址空間(包括進程的數據段、代碼段等)
  • 文件描述符表
  • 每種信號的處理方式(SIG_ IGN、SIG_ DFL或者自定義的信號處理函數)
  • 當前工作目錄
  • 用戶ID和組ID

2. 線程控制

Linux的pthread庫

Liunx中,提供給用戶層進行線程控制的函數被打包在一個動態庫中 —— pthread。使用線程控制接口時,需要包含頭文件pthread.h,并在gcc/g++編譯時加上-l pthread選項確定鏈接動態庫。

/lib64目錄下找到pthread庫:

在這里插入圖片描述

編譯時應該添加的選項:

g++ threadTest.cc -o threadTest -l pthread # -lpthread也可以
  1. pthread_create

    功能:

    ? 創建一個線程

    接口:

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

    參數:

    thread:線程庫中定義了一個線程ID類型phtread_t,這里的thread是一個輸出型參數,函數會向其指向的空間寫入創建線程的ID

    attr:線程的屬性,一般設為nullptr即可

    start_routine:線程執行的函數,是一個返回值類型void*,參數類型void*的函數指針

    arg:傳入start_routine的參數,使用前后一般需要類型轉換。

    返回值:

    RETURN VALUEOn success, pthread_create() returns 0; on error, it returns an error number, and the contents of *thread are undefined.
    

💭關于線程退出的問題:

同子進程退出,需要父進程回收,線程也需要被另外的線程回收。回收的原因如下:1. 一個線程退出后,對應的資源不會被釋放,而是留存在地址空間中。一個進程能運行的線程數是有限的,如果不加以回收,可能會導致內存泄漏!2. 一個線程退出后,其它線程可能需要獲取其執行任務的結果。

  1. pthread_join

    功能:

    ? 阻塞等待一個線程

    接口:

    int pthread_join(pthread_t thread, void **retval);
    

    參數:

    thread:線程ID

    retval:指向的空間中存儲的是線程返回的結果(注意類型轉換),因為線程函數的返回結果是void*類型,所以要用二級指針接收。如果不關心回收線程的結果,則設置為nullptr。

    返回值:

    RETURN VALUEOn success, pthread_join() returns 0; on error, it returns an error number.
    
  2. pthread_exit

    線程函數中,可以直接用return退出線程并返回結果(可以被其它線程join接收)

    void *run(void *arg)
    {int cnt = 5;while (cnt--){cout << "I am new thread" << endl;sleep(1);}return nullptr; //
    }
    

    也可以用pthread_exit函數。

    void pthread_exit(void *retval); //和return一樣,返回一個void*指針
    

Linux中,線程只有joinable和unjoinable兩種狀態。默認情況下,線程是joinable狀態,該狀態下的線程退出后,占有資源不會被釋放,必須等待其它線程調用pthread_join回收它,釋放資源,或者進程退出,資源全部被釋放。當然,可以通過調用pthread_detach分離線程,將線程設置為unjoinable狀態,使其無需被等待回收,退出即被系統自動釋放資源。

  1. pthread_detach

    功能:

    ? 分離線程ID為thread的線程,使其無需被join等待。

    接口:

    int pthread_detach(pthread_t thread);
    

    返回值:

    RETURN VALUEOn success, pthread_detach() returns 0; on error, it returns an error number.
    

線程分離可以由別的線程分離,也可以自己分離。

  1. pthread_self

    功能:

    ? 獲取當前線程的線程ID

    接口:

     pthread_t pthread_self(void);
    

?測試

void *run(void *arg)
{int cnt = 10;while(cnt--){cout << "I am new thread, cnt: " << cnt << endl;sleep(1);}pthread_exit(nullptr);
}int main()
{cout << "I am main thread" << endl;pthread_t tid;pthread_create(&tid, nullptr, run, nullptr);int n = pthread_join(tid, nullptr);if (n != 0){cout << "join new thread fail!!" << endl;exit(1);}cout << "join new thread success!!" << endl;return 0;
}

主線程創建新線程后,調用pthread_join會阻塞等待新線程退出。運行結果如下:

[ckf@VM-8-3-centos lesson9_thread]$ ./mythread 
I am main thread
I am new thread, cnt: 9
I am new thread, cnt: 8
I am new thread, cnt: 7
I am new thread, cnt: 6
I am new thread, cnt: 5
I am new thread, cnt: 4
I am new thread, cnt: 3
I am new thread, cnt: 2
I am new thread, cnt: 1
I am new thread, cnt: 0
join new thread success!!

可以在主線程中detach線程ID為tid的新線程,也可以在新線程中detach自己。

void *run(void *arg)
{//pthread_detach(pthread_self()); // 在新線程中detach自己int cnt = 10;while(cnt--){cout << "I am new thread, cnt: " << cnt << endl;sleep(1);}pthread_exit(nullptr);
}int main()
{cout << "I am main thread" << endl;pthread_t tid;pthread_create(&tid, nullptr, run, nullptr);pthread_detach(tid); // 在主線程中detach線程ID為tid的新線程int n = pthread_join(tid, nullptr);if (n != 0){cout << "join new thread fail!!" << endl;exit(1);}cout << "join new thread success!!" << endl;return 0;
}
[ckf@VM-8-3-centos lesson9_thread]$ ./mythread 
I am main thread
join new thread fail!! #等待失敗,pthread_join無法等待已分離的線程,返回值非0

如果在新線程中detach自己,可能依然能夠join成功。要想成功detach線程,必須在join之前detach,因為調用pthread_join函數時,已經將線程視為joinable并阻塞等待了,此后再detach是無效的。上面代碼中,如果在新線程中detach自己,由于主線程和新線程調度的先后順序不確定性,很可能線程先join再detach,此時的detach是無效的。

  1. pthread_cancel

    功能:

    ? 撤銷(終止)一個線程ID為thread的線程

    接口:

    int pthread_cancel(pthread_t thread);
    

    返回值:

    RETURN VALUEOn success, pthread_cancel() returns 0; on error, it returns a nonzero error number.
    

    撤銷一個線程后,如果有另外的線程join該線程,那么其收到的退出結果是PTHREAD_CANCELED

    #define PTHREAD_CANCELED ((void *) -1)
    

?測試

void *run(void *arg)
{while (true){cout << "I am new thread" << endl;sleep(1);}pthread_exit(nullptr);
}int main()
{cout << "I am main thread" << endl;pthread_t tid;pthread_create(&tid, nullptr, run, nullptr);sleep(3);pthread_cancel(tid);void *ret = nullptr;int n = pthread_join(tid, &ret);if (n != 0){cout << "join new thread fail!!" << endl;exit(1);}if (ret == PTHREAD_CANCELED){cout << "new thread is canceled" << endl;}cout << "join new thread success!!" << endl;return 0;
}
[ckf@VM-8-3-centos lesson9_thread]$ ./mythread 
I am main thread
I am new thread
I am new thread
I am new thread
new thread is canceled #新線程被撤銷了
join new thread success!!

用戶級線程

💭pthread庫的線程控制接口,都不是直接操作Linux底層的輕量級進程,而是操作用戶級線程。pthread庫將底層的輕量級進程封裝成為用戶級線程,用戶看到的便是線程而不是所謂的輕量級進程。動態庫load到進程的共享區中,因此,用戶級線程的空間也是load到進程的共享區中,線程的大部分獨立資源保存在這塊空間中,包括線程棧。

🔎線程庫是怎么管理用戶級線程的?

先描述再組織。 創建類似TCB的數據結構來描述線程,并將這些數據結構組織為一張表,如下。

在這里插入圖片描述

  • 前面使用接口獲取到的線程tid,其實就是該線程的用戶級頁表的首地址,只不過將其轉換成整型的格式。

    int g_val = 100;string toHex(pthread_t tid)
    {char buf[64];snprintf(buf, sizeof(buf), "0x%x", tid);return string(buf);
    }void *run(void *arg)
    {cout << toHex(pthread_self()) << endl;pthread_exit(nullptr);
    }int main()
    {pthread_t t1;pthread_t t2;cout << "&g_val: " << &g_val <<endl;pthread_create(&t1, nullptr, run, nullptr);pthread_create(&t2, nullptr, run, nullptr);pthread_join(t1, nullptr);pthread_join(t2, nullptr);return 0;
    }
    
    [ckf@VM-8-3-centos lesson9_thread]$ ./mythread 
    &g_val: 0x6020cc #全局數據區
    0x4b30f700       #共享區
    0x4ab0e700		 #共享區
    
  • 全局變量默認是所有線程共享的,開發者需要處理多線程競爭問題。有些情況下我們需要保證一個線程獨享一份數據,其它線程無法訪問。這時候就要用到線程局部存儲。gcc/g++編譯環境中,可以__thread聲明一個全局變量,從而每個線程都會獨有一個該全局變量,存儲在線程局部存儲區中。

    __thread int g_val = 0; //__thread修飾全局變量,可以理解為從進程的全局變量變成線程的全局變量string toHex(pthread_t tid)
    {char buf[64];snprintf(buf, sizeof(buf), "0x%x", tid);return string(buf);
    }void *run(void *arg)
    {cout << "g_val: " << ++g_val << " " << "&g_val: " << &g_val << endl;pthread_exit(nullptr);
    }int main()
    {pthread_t t1;pthread_t t2;pthread_t t3;pthread_create(&t1, nullptr, run, nullptr);pthread_create(&t2, nullptr, run, nullptr);pthread_create(&t3, nullptr, run, nullptr);cout << "g_val: " << ++g_val << " " << "&g_val: " << &g_val << endl;pthread_join(t1, nullptr);pthread_join(t2, nullptr);pthread_join(t3, nullptr);return 0;
    }
    
    [ckf@VM-8-3-centos lesson9_thread]$ ./mythread #使用了線程局部存儲
    g_val: 1 &g_val: 0x7fcb7cfcb77c
    g_val: 1 &g_val: 0x7fcb7bf366fc
    g_val: 1 &g_val: 0x7fcb7b7356fc
    g_val: 1 &g_val: 0x7fcb7af346fc[ckf@VM-8-3-centos lesson9_thread]$ ./mythread #未使用線程局部存儲
    g_val: 1 &g_val: 0x6021d4
    g_val: 2 &g_val: 0x6021d4
    g_val: 3 &g_val: 0x6021d4
    g_val: 4 &g_val: 0x6021d4
    
  • 每個線程都有一個獨立的棧結構,用于存儲運行時的臨時數據和壓入函數棧幀。注意,主線程的棧就是進程地址空間中的棧。


ENDING…

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/39050.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/39050.shtml
英文地址,請注明出處:http://en.pswp.cn/news/39050.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

無腦入門pytorch系列(三)—— nn.Linear

本系列教程適用于沒有任何pytorch的同學&#xff08;簡單的python語法還是要的&#xff09;&#xff0c;從代碼的表層出發挖掘代碼的深層含義&#xff0c;理解具體的意思和內涵。pytorch的很多函數看著非常簡單&#xff0c;但是其中包含了很多內容&#xff0c;不了解其中的意思…

SpringBoot復習:(46)全局的bean懶加載是怎么實現的?

在application.properties中配置&#xff1a; spring.main.lazy-initializationtrue在運行SpringApplication的run方法時&#xff0c;代碼如下&#xff1a; 其中調用了prepareContext,prepareContext代碼如下&#xff1a; 當在配置文件中配置了spring.main.lazy-initializat…

JavaScript實現在線Excel的附件上傳與下載

摘要&#xff1a;本文由葡萄城技術團隊于CSDN原創并首發。轉載請注明出處&#xff1a;葡萄城官網&#xff0c;葡萄城為開發者提供專業的開發工具、解決方案和服務&#xff0c;賦能開發者。 前言 在本地使用Excel時&#xff0c;經常會有需要在Excel中添加一些附件文件的需求&am…

Clickhouse基于文件復制寫入

背景 目前clickhouse社區對于數據的寫入主要基于文件本地表、分布式表方式為主&#xff0c;但缺乏大批量快速寫入場景下的數據寫入方式&#xff0c;本文提供了一種基于clickhouse local 客戶端工具分布式處理hdfs數據表文件&#xff0c;并將clickhouse以文件復制的方式完成寫入…

解決并發沖突:Java實現MySQL數據鎖定策略

在并發環境下&#xff0c;多個線程同時對MySQL數據庫進行讀寫操作可能會導致數據沖突和不一致的問題。為了解決這些并發沖突&#xff0c;我們可以采用數據鎖定策略來保證數據的一致性和完整性。下面將介紹如何使用Java實現MySQL數據鎖定策略&#xff0c;以及相關的注意事項和最…

開源低代碼平臺Openblocks

網友 HankMeng 想看低代碼工具&#xff0c;正好手上有一個&#xff1b; 什么是 Openblocks &#xff1f; Openblocks 是一個開發人員友好的開源低代碼平臺&#xff0c;可在幾分鐘內構建內部應用程序。 傳統上&#xff0c;構建內部應用程序需要復雜的前端和后端交互&#xff0c;…

如何保證微信小游戲存檔不丟失?

引言 微信小游戲的興起為玩家提供了一個輕松便捷的娛樂方式&#xff0c;然而&#xff0c;存檔丟失問題一直以來都是開發者和玩家關注的焦點。為了確保玩家的游戲體驗和投入能夠得到充分的保障&#xff0c;開發團隊需要采取一系列方法來保障微信小游戲存檔不丟失。本文將介紹一…

學習筆記十五:基于YUM文件運行POD應用

基于YUM文件運行POD應用 通過資源清單文件創建第一個Pod更新資源清單文件查看pod是否創建成功查看pod的ip和pod調度到哪個節點上假如pod里有多個容器&#xff0c;進入到pod里的指定容器查看pod詳細信息查看pod具有哪些標簽&#xff1a;刪除pod通過kubectl run創建Pod Pod資源清…

word之插入尾注+快速回到剛才編輯的地方

1-插入尾注 在編輯文檔時&#xff0c;經常需要對一段話插入一段描述或者附件鏈接等&#xff0c;使用腳注經常因占用篇幅較大導致文檔頁面內容雜亂&#xff0c;這事可以使用快捷鍵 ControlaltD 即可在 整個行文的末尾插入尾注&#xff0c;這樣文章整體干凈整潔&#xff0c;需…

【枚舉邊+MST+組合計數】CF1857G

Problem - 1857G - Codeforces 題意&#xff1a; 思路&#xff1a; 首先觀察一下樣例&#xff1a; 可以發現對于每一對點&#xff0c;貢獻是 s - 這對點對應的環的最大邊 1 那么這樣就有了 n^2 的做法 然后&#xff0c;根據慣用套路&#xff0c;枚舉樹上的點對問題可以轉…

Prometheus的搭建與使用

一、安裝Prometheus 官網下載地址&#xff1a;Download | Prometheus 解壓&#xff1a;tar -zxvf prometheus-2.19.2.linux-amd64.tar.gz重命名&#xff1a; mv prometheus-2.19.2.linux-amd64 /home/prometheus進入對應目錄&#xff1a; cd /home/prometheus查看配置文件&am…

淺析市面電商CRM系統|排單系統存在的不足

筆者做CRM尤其是電商CRM系統7年&#xff0c;相信我的分享能夠幫助大家對電商CRM有個清晰的認知。 系統本身是用來提升效率的&#xff0c;針對不少電商賣家或服務商&#xff0c;都有使用CRM系統來管理粉絲鏈接與營銷、銷售推廣等環節&#xff0c;來實現完整的CRM鏈路。尤其是在當…

OpenCV-Python中的圖像處理-傅里葉變換

OpenCV-Python中的圖像處理-傅里葉變換 傅里葉變換Numpy中的傅里葉變換Numpy中的傅里葉逆變換OpenCV中的傅里葉變換OpenCV中的傅里葉逆變換 DFT的性能優化不同濾波算子傅里葉變換對比 傅里葉變換 傅里葉變換經常被用來分析不同濾波器的頻率特性。我們可以使用 2D 離散傅里葉變…

2308C++對稱轉移

原文 了解對稱轉移 協程組提供了個編寫異步代碼的絕妙方法,與同步代碼一樣.只需要在合適地點加上協待,編譯器就會負責掛起協程,跨掛起點保留狀態,并在操作完成后恢復協程. 但是,最初有個令人討厭的限制,如果不小心,很容易導致棧溢出.如果想避免它,則必須引入額外同步成本,以…

Unity Spine幀事件

SpinePro中添加事件幀 首先 選中右上角的層級樹 然后選擇事件選項 最后在右下角看到 新建 點擊它 新建一個事件 點擊左上角的設置按鈕 彈出編輯窗口 編輯窗口 在右上角 動畫欄 可以切換對應的動畫 點坐邊的那個小灰點來切換 亮點代表當前動畫 選中幀 添加事件 點擊對應事件…

突破防線!泛微OA任意文件上傳Getshell

子曰&#xff1a;“巧言令色&#xff0c;鮮矣仁。” 漏洞復現 訪問漏洞url&#xff1a; 存在漏洞的路徑為 /weaver/weaver.common.Ctrl/.css?arg0com.cloudstore.api.service.Service_CheckApp&arg1validateApp漏洞利用&#xff1a; 漏洞證明&#xff1a; 文筆生疏&…

ubuntu 20.0.4 搭建nvidia 顯卡環境

一、安裝docker 1、安裝dokcer sudo apt install docker.io2、docker 添加到用戶組 創建docker用戶組 sudo groupadd docker添加當前用戶加入docker用戶組 sudo usermod -aG docker ${USER}重啟docker服務 sudo systemctl restart docker切換或者退出當前賬戶再從新登入 …

openGauss學習筆記-41 openGauss 高級數據管理-匿名塊

文章目錄 openGauss學習筆記-41 openGauss 高級數據管理-匿名塊41.1 語法41.2 參數說明41.3 示例 openGauss學習筆記-41 openGauss 高級數據管理-匿名塊 匿名塊&#xff08;Anonymous Block&#xff09;是存儲過程的字塊之一&#xff0c;沒有名稱。一般用于不頻繁執行的腳本或…

NPM與外部服務的集成(下)

目錄 1、撤消訪問令牌 2、在CI/CD工作流中使用私有包 2.1 創建新的訪問令牌 持續整合 持續部署 交互式工作流 CIDR白名單 2.2 將令牌設置為CI/CD服務器上的環境變量 2.3 創建并簽入特定于項目的.npmrc文件 2.4 令牌安全 3、Docker和私有模塊 3.1 背景&#xff1a;運…

了解異或的好處和用途

1.什么是異或&#xff1f; 異或&#xff1a;對于二進制&#xff0c;相同為0 不同為11 ⊕ 1 00 ⊕ 0 01 ⊕ 0 10 ⊕ 1 1 2.異或的好處&#xff1f; 異或的好處&#xff1f;1.快速比較兩個值 2.xor a a例如 a 3 011xor 0110003.可以使用 異或 來使某些特定的位翻轉【原因…