Linux多線程【初識線程】

?個人主頁: 北 海
🎉所屬專欄: Linux學習之旅
🎃操作環境: CentOS 7.6 阿里云遠程服務器

成就一億技術人


文章目錄

  • 🌇前言
  • 🏙?正文
    • 1、什么是線程?
      • 1.1、基本概念
      • 1.2、線程理解
      • 1.3、進程與線程的關系
      • 1.4、簡單使用線程
    • 2、重談地址空間
      • 2.1、頁表的大小
      • 2.2、內存與磁盤的交互
      • 2.3、深入頁表
      • 2.4、小結
    • 3、線程小結
      • 3.1、再談線程
      • 3.2、線程的優點
      • 3.3、線程的缺點
      • 3.4、線程的用途
  • 🌆總結


🌇前言

將一份代碼成功編譯后,可以得到一個可執行程序,程序運行后,相關代碼和數據被 load 到內存中,并且操作系統會生成對應數據結構(比如 PCB)對其進行管理及分配資源,準備工作做完之后,我們就可以得到一個運行中的程序,簡稱為 進程,對于操作系統來說,光有 進程 的概念是無法滿足高效運行的需求的,因此需要一種執行粒度更細、調度成本更低的執行流,而這就是 線程

Windows 中的線程

線程


🏙?正文

1、什么是線程?

1.1、基本概念

可能很多人第一次聽說 線程 這個詞是在 處理器 中,比如今年 英特爾第 13 代酷睿 系列芯片,就在其宣傳頁中提到了 線程 這個詞

圖示
硬件上的 線程 概念我們這里不討論,接下來看看操作系統層面的 線程概念

教材觀點

  1. 線程就是一個執行分支、執行粒度比進程更細、調度成本更低
  2. 線程就是進程內部的一個執行流

內核觀點

  • 進程是承擔系統資源分配的基本實體,而線程是 CPU 運行的基本單位

線程是對以往進程概念的補充完善,正確理解線程概念是一件十分重要的事

1.2、線程理解

注意:以下理解是站在 Linux 系統的角度,不同的系統具體實現方式略有差異

理解 線程 之前需要先簡單回顧一下 進程

  • 程序運行后,相關的代碼和數據會被 load 到內存中,然后操作系統為其創建對應的 PCB 數據結構、生成虛擬地址空間、分配對應的資源,并通過頁表建立映射關系

詳見 《Linux進程學習【進程地址】》

圖示

進程之間是相互獨立

即使是 父子進程,他們也有各自的 虛擬地址空間、映射關系、代碼和數據(可能共享部分數據,出現修改行為時引發 寫時拷貝機制

如果我們想要創建 其他進程 執行任務,那么 虛擬地址空間、映射關系、代碼和數據 這幾樣東西是必不可少的,想象一下:如果只有進程的概念,并且同時存在幾百個進程,那么操作系統調度就會變得十分臃腫

  • 操作系統在調度進程時,需要頻繁保存上下文數據、創建的虛擬地址空間及建立映射關系

為了避免這種繁瑣的操作,引入了 線程 的概念,所謂 線程 就是:額外創建一個 task_struct 結構,并且該 task_struct 同樣指向當前的虛擬地址空間,并且不需要建立映射關系及加載代碼和數據,如此一來,操作系統只需要 創建一個 task_struct 結構即可完成調度,成本非常低

為什么切換進程比切換線程開銷大得多?
CPU 內部包括:運算器、控制器、寄存器、MMU、硬件級緩存(cache,其中 硬件級緩存 cache 又稱為 高速緩存,遵循計算機設計的基本原則:局部性原理,會預先加載 部分用戶可能訪問的數據 以提高效率,如果切換進程,會導致 高速緩存 中的數據無法使用(進程具有獨立性),重新開始 預加載,這是非常浪費時間的(對于 CPU 來說);但切換線程就不一樣了,因此線程從屬于進程,切換線程時,所需要的數據的不會發生改變,這就意味值 高數緩存 中的數據可以繼續使用,并且可以接著 預加載 下一波數據

不同 CPU高速緩存 大小不同,足夠大的高速緩存 + 先進的工藝 就可以得到一塊性能優越的 CPU
圖示

注:高速緩存中預加載的是公共數據,并非線程的私有數據

圖示

進程(process)的 task_struct 稱為 PCB,線程(thread)的 task_struct 則稱為 TCB

從今天開始,無論是 進程 還是 線程,都可以稱為 執行流線程 從屬于 進程當進程中只有一個線程時,我們可以粗粒度的稱當前進程為一個單獨的執行流;當進程中有多個線程時,則稱當前進程為多執行流,其中每一個執行流都是一個個的線程

執行流的調度由操作系統負責,CPU 只負責根據 task_struct 結構進行運算

  • 若下一個待調度的執行流為一個單獨的進程,操作系統仍需創建 PCB 及 虛擬地址空間、建立映射關系、加載代碼和數據
  • 但如果下一個待調度的執行流為一個線程,操作系統只需要創建一個 TCB,并將其指向已有的虛擬地址空間即可

現在面臨著一個很關鍵的問題:進程和線程究竟是什么關系?

問號

1.3、進程與線程的關系

進程是承擔系統資源分配的實體,比如 程序運行必備的:虛擬地址空間、頁表映射關系、相關數據和代碼 這些都是存儲在 進程 中的,也就是我們歷史學習中 進程 的基本概念

線程是 CPU 運行的基本單位,程序運行時,CPU 只認 task_struct 結構,并不關心你是 線程 還是 進程,不過,線程 包含于 進程 中,一個 進程 可以只有一個 線程,也可以有很多 線程,當只有一個 線程 時,通常將其稱為 進程,但對于 CPU 來說,這個 進程 本質上仍然是 線程;因為 CPU 只認 task_struct 結構,并且 PCBTCB 都屬于 task_strcut,所以才說 線程是 CPU 運行的基本單位

總結:進程是由操作系統將程序運行所需地址空間、映射關系、代碼和數據打包后的資源包,而 線程/輕量級線程/執行流 則是利用資源完成任務的基本單位

線程包含于進程中,進程本身也是一個線程

我們之前學習的進程概念是不完整的,引入線程之后,可以對進程有一個更加全面的認識

通常將程序啟動,比如 main 函數中的這個線程稱為 主線程,其他線程則稱為 次線程

圖示

實際上 進程 = PCB + TCB + 虛擬地址空間 + 映射關系 + 代碼和數據,這才是一個完整的概念

以后談及進程時,就要想到 一批執行流+可支配的資源

圖示

進程與線程的概念并不沖突,而是相互成就

Linux 中,認為 PCBTCB 的共同點太多了,于是直接復用了 PCB 的設計思想和調度策略,在進行 線程管理 時,完全可以復用 進程管理 的解決方案(代碼和結構),這可以大大減少系統調度時的開銷,做到 小而美,因此 Linux 中實際是沒有真正的 線程 概念的,有的只是復用 PCB 設計思想的 TCB

在這種設計思想下,線程 注定不會過于龐大,因此 Linux 中的 線程 又可以稱為 輕量級進程(LWP輕量級進程 足夠簡單,且 易于維護、效率更高、安全性更強,可以使得 Linux 系統不間斷的運行程序,不會輕易 崩潰

一切皆文件一樣,這種設計思想注定 Linux 會成為一款 卓越 的操作系統

別的系統采用的是其他方案,比如 Windows 使用的是真線程方案,為 TCB 額外設計了一邏輯,這就導致操作系統在同時面臨 PCBTCB 時需要進行識別后切換成不同的處理手段,存在不同的邏輯容易增加系統運行不穩定的風險,這就導致 Windows 無法做到長時間運行,需要通過重啟來重置風險
此時我的電腦中同時存在幾百個進程和幾千個真線程,可想而知操作系統的負擔有多大

圖示

1.4、簡單使用線程

如何驗證 Linux 中的線程解決方案? 簡單使用一下就好了

接下來簡單使用一下 pthread 線程原生庫中的線程相關函數(只是簡單使用,不涉及其他操作)

#include <iostream>
#include <unistd.h>
#include <pthread.h>using namespace std;void *threadHandler1(void *args)
{while (true){cout << "我是次線程1,我正在運行..." << endl;sleep(1);}
}void *threadHandler2(void *args)
{while (true){cout << "我是次線程2,我正在運行..." << endl;sleep(1);}
}void *threadHandler3(void *args)
{while (true){cout << "我是次線程3,我正在運行..." << endl;sleep(1);}
}int main()
{pthread_t t1, t2, t3; // 創建三個線程pthread_create(&t1, NULL, threadHandler1, NULL);pthread_create(&t2, NULL, threadHandler2, NULL);pthread_create(&t3, NULL, threadHandler3, NULL);// 主線程運行while (true){cout << "我是主線程" << endl;sleep(1);}return 0;
}

編譯程序時,需要帶上 -lpthread 指明使用 線程原生庫

結果:主線程+三個次線程同時在運行

至于為什么打印結果會有點不符合預期,這就涉及到 加鎖 相關問題了,后面再解決

圖示

使用指令查看當前系統中正在運行的 線程 信息

ps -aL | head -1 && ps -aL | grep myThread | grep -v grep

圖示

可以看到此時有 四個線程

  • 細節1:四個線程的 PID 都是 13039
  • 細節2:四個線程的 LWP 各不相同
  • 細節3:第一個線程的 PIDLWP 是一樣的

其中,第一個線程就是 主線程,也就是我們之前一直很熟悉的 進程,因為它的 PIDLWP 是一樣的,所以只需要關心 PID 也行

操作系統如何判斷調度時,是切換為 線程 還是切換為 進程

  • 將待切換的執行流 PID 與當前執行流的 PID 進行比對,如果相同,說明接下來要切換的是 線程,否則切換的就是 進程
  • 操作系統只需要找到 LWPPID 相同的線程,即可輕松鎖定 主線程

線程是進程的一部分,給其中任何一個線程發送信號,都會影響到其他線程,進而影響到整個進程


2、重談地址空間

注:當前部分是拓展,與線程沒有很大的關系,但是一個比較重要的知識點

2.1、頁表的大小

頁表 是用來將 虛擬地址物理地址 之間建立映射關系的,除此之外,頁表 中還存在 其他屬性 字段

圖示

眾所周知,在 32 位系統中,存在 2^32 個地址(一個內存單元大小是 1byte),意味著虛擬地址空間 的大小為 4GB

假設極端情況:每個地址都在頁表中建立了映射關系,其中頁表的每一列大小都是 4 字節,那么頁表的大小就是 2^32 * 4 * 3 * 1byte = 48GB,這就意味著悲觀情況下頁表已經干掉 48GB 的內存了,但現在電腦普遍都只有 16GB 內存,更何況是幾十年前的電腦

所以說頁表絕對不是采用這種單純 地址->地址 的映射方案

2.2、內存與磁盤的交互

操作系統從 磁盤 中讀取數據時,一次讀取大量數據多次讀取少量數據 要快的多,因為 磁盤 是外設,每一次讀取都必然伴隨著尋址等機械運動(機械硬盤),無論是對于 內存 還是 CPU ,這都是非常慢的,為了盡可能提高效率,操作系統選擇一次 IO 大量數據的方式讀取數據

通常 IO 的數據以 為基本單位,在文件系統中,一個 的大小為 4KB(一個塊由8個扇區組成,單個扇區大小為 512Byte),即使我們一次只想獲取一個字節,操作系統最低也會 IO 一個 數據塊4KB

4KB 這個大小很關鍵

  • 文件系統/編譯器:文件存儲時,需要以 4KB 為單位進行存儲
  • 操作系統/內存:讀取文件或進行內存管理時,也是以 4KB 為單位的

也就是說,內存實際上是被切成大小為 4KB 的小塊的,在內存中,單塊內存(4KB)被稱為 Page,組成單塊內存的邊界(類似于下標)被稱為 頁框(頁幀)

圖示

為了將內存中的 Page 進行管理,需要 先描述,在組織,構建 struct page 結構體,用于描述 Page 的狀態,比如是否為臟數據、是否已經被占用了,因為存在很多 Page,所以需要將這些 struct page 結構進行管理,使用的就是 數組(天然有下標) struct page mem[N],其中 N 表示當前內存中的 Page 數量

struct page
{int status; // 基礎字段:狀態// 注意:這個結構不能設計的太復雜了,因為稍微大一點內存就爆了,所以里面的屬性非常少
};struct page mem[N]; // 管理 page 結構體的數組

假設我們的內存為 4GB,那么等分為 4KBPage,可以得到約 100wPage,其中 struct page 結構體不會設計的很大,大小是 字節 級別的,也就是說 struct page mem[100w] 占用的總大小不過 4~5MB,對于偌大的內存來說可以忽略不計

內存管理的本質:

  • 申請:無非就是尋找 mem 數組中一塊未被使用的足量空間,將對應的 頁 Page 屬性設置為已被申請,并返回起始地址(足量空間頁框的起始地址)
  • 使用:將磁盤中的指定的 4KB 大小數據塊存儲至內存中對應的 頁 Page
  • 釋放:將 頁 Page 屬性設置為可用狀態

關于 mem 數組的查找算法(內存分配算法):LRU、伙伴系統等

重新審視 4KB,為什么內存與磁盤交互的基本單位是 塊(4KB

這里就要提一下 局部性原理

圖示

局部性原理的特征

  • 現代計算機預加載的理論基礎
  • 允許我們提前加載正在訪問數據的 相鄰或者附加的數據(數據預加載)

局部性原理 的核心在于 預加載,如果沒有 局部性原理,那么我們可能今天都用不上電腦,因為如果沒有這個原則,那么內存在于磁盤交互時,只能做到用戶需要什么,就申請什么,這會直接拉低 CPU 的速度,而速度極快的 磁盤 又非常貴

局部性原理 有效避免了這個問題:用戶訪問數據時,操作系統不僅會加載用想要訪問的數據,同時還會加載當前數據的臨近數據,如此一來就可以做到用戶訪問下一份數據時,不必再次 IO,盡量減少 IO 的次數

  • 合理性:用戶訪問的數據大多都是具有一定連續性的,比如用戶訪問 668 號數據,那么他下一次想訪問的數據大概是 669 及以后,因此可以提前加載

配合上 4KB 的塊大小,可以使得每次 IO 足量的數據,并且有可能會多出,起到 預加載 的效果

所以現在就可以回答為什么是 4KB

  1. IO 的基本單位,內核系統/文件系統 都對其提供了支持
  2. 利于通過 局部性原理 預測數據的命中情況,盡可能提高效率

總結:IO 的基本單位是 4KB,內存實際上被劃分成了很多個 4KB 的小塊,并存在相應的數據結構對其進行管理

2.3、深入頁表

顯然,頁表 絕對不可能動輒幾十個 GB,實際在根據 虛擬地址 進行尋址時,頁表 也有自己的設計邏輯

虛擬地址(32 位操作系統) 大小也就是 32 比特位,大概也就是 4Byte,通常將一個 虛擬地址 分割為三份:101012

  • 10虛擬地址中的前 10 個比特位,用于尋址 頁表2
  • 10虛擬地址中間的 10 個比特位,用于尋找 頁框起始地址
  • 12虛擬地址中的后 12 個比特位,用于定位 具體地址(偏移量)

圖示

所以,實際上在通過 頁表 進行尋址時,需要用到 兩個頁表(為了方便演示,僅包含一組 kv 關系):

圖示
注:“頁表2” 中的 20 表示內存中的下標,即 頁框地址

通常將 “頁表1” 稱為 頁目錄,“頁表2” 稱為 頁表項

  • 頁目錄:使用 10 個比特位定位 頁表項
  • 頁表項:使用 10 個比特位定位 頁框地址
  • 偏移量:使用 12 個比特位,在 頁 Page 中進行任意地址的尋址

所以即使是每個 物理地址 都被尋址的的極端情況下,頁表 總大小不過為:(2^10 + 2^10) * (2^10 + 2^20),大約也就需要 4Mb 大小,即可映射至每一個 物理內存,但實際上 物理內存 并不會被時刻占滿,大多數情況下都是使用一部分,因此實際 頁表 大小不過 幾十字節

像這種 頁框起始地址+偏移量 的方式稱為 基地址+偏移量,是一種運用十分廣泛的思想,比如所謂的 類型(intdoublechar…)都是通過 類型的起始地址+類型的大小 來標識該變量大小的,也就是說我們只需要 獲得變量的起始地址,即可自由進行偏移操作(如果偏移過度了,就是越界),這也就解釋了為什么取地址只會取到 起始地址

總結:得益于 劃分+偏移 的思想,使得頁表的大小可以變得很小

擴展:動態內存管理

實際上,我們在進行 動態內存管理(malloc/new 申請堆空間時,操作系統 并沒有立即在物理內存中申請空間(因為你申請了可能不會立馬使用),而是 先在 虛擬地址 中進行申請(成本很低),當我們實際使用該空間時,操作系統 再去 填充相應的頁表信息+申請具體的物理內存

像這種操作系統賭博式的行為我們已經不是第一次見了,比如之前的 寫時拷貝,就是在賭你不會修改,這樣做的好處就是可以 最大化提高效率,對于內存來說,這種使用時再申請的行為會引發 缺頁中斷

圖示

當用戶 動態申請內存 時,操作系統只會在 虛擬地址 中申請,具體表現為 返回一塊未被使用的空間起始地址,用戶實際使用這塊空間時,遵循 查頁表、尋址物理內存 的原則,實際進行 查頁表 操作時,發現 頁表項 沒有記錄此地址的映射關系,于是就會引發 缺頁中斷,發出對應的 中斷信號,陷入內核態,通過 中斷控制器 識別 中斷信號 后做出相應的動作,比如這里的動作是:填充頁表信息、申請物理內存 ;把 物理內存 準備好后,用戶就可以進行正常使用了,整個過程非常快,對于用戶來說幾乎無感知

圖示

同理,在進行 磁盤文件讀取 時,也存在 缺頁中斷 行為,畢竟你打開文件了,并不是立即進行讀寫操作的

諸如這種 硬件級的中斷行為 我們已經在 信號產生 中學過了,即:從鍵盤按下的那一刻,發出硬件中斷信號,中斷控制器識別為 鍵盤 發出的信號后,去 中斷向量表 中查找執行方法,也就是 鍵盤 的讀取方法

所以操作系統根本不需要關系 硬件 是什么樣子,只需要關心對方是否發出了 信號(請求),并作出相應的 動作(執行方法) 即可,很好的實現了 解耦

對于 內存 的具體情況,諸如:是否命中、是否被占用、對應的 RWX 權限 需要額外的空間對其進行描述,而 頁表 中的 其他屬性 列就包含了這些信息

圖示

內存 進行操作時,勢必要進行 虛擬地址到物理地址 之間的轉換,而 MMU 機制 + 頁表信息 可以判斷 當前操作 是否合法,如果不合法會報錯

注:UK 權限用于區分當前是用戶級頁表,還是內核級頁表

比如這段代碼:

char *ps = "Change World!";
*ps = 'N'; // 此時程序會報錯(需要賦值為字符,否則無法編譯)

結合 頁表、信號 等知識,解釋整個報錯邏輯:

  • "Change World!" 屬于字符常量,存儲在字符常量區中,其中的權限為 R
  • char *ps 屬于一個指針變量,指向字符常量的起始地址
  • 當我們進行 *ps = "No" 操作時,首先會將字符常量的地址轉換為物理地址,在轉換過程中,MMU 機制發現該內存權限僅為 R,但 *ps 操作需要 W 權限,于是 MMU 引發異常 -> 操作系統識別到異常,將該異常轉換為 信號 -> 并把 信號 發給出現問題的 進程 -> 信號暫時被保存 -> 在 內核態轉為用戶態 的過程中,進行 信號處理 -> 最終結果是終止進程,也就是報錯

程序運行后,就會報錯

圖示

2.4、小結

所以目前 地址空間 的所有組成部分我們都已經打通了,再次回顧這種設計時,會發現 用戶壓根不知道、也不需要知道虛擬地址空間之后發生的事,只需要正常使用就好了,當引發異常操作時,操作系統能在 查頁表 階段就進行攔截,而不是等到真正影響到 物理內存 時才報錯

圖示

所謂的 虛擬地址空間 就是在進行設計時添加的一層 軟件層,它解決了 多進程時的物理內存訪問問題、也解決了物理內存的保護問題,同時還為用戶提供了一個簡單的虛擬地址空間,做到了 虛擬與物理 的 完美解耦

圖示

這種設計思想就是計算機界著名的 所有問題都可以通過添加一層 軟件層 解決,這種思想早在幾十年前就已經得到了運用

這種分層結構不僅適用于 操作系統,還適用于 網絡,比如大名鼎鼎的 OSI 七層網絡模型


3、線程小結

3.1、再談線程

Linux 中沒有 真線程,有的只是復刻 進程 代碼和管理邏輯的 輕量級線程(LWP

線程 有以下概念:

  • 在一個程序中的一個執行路線就叫做 線程(Thread),或者說 線程 是一個進程內部的控制程序
  • 每一個進程都至少包含一個 主線程
  • 線程 在進程內部執行,本質上仍然是在進程地址空間內運行
  • Linux 系統中,CPU 看到的 線程 TCB 比傳統的 進程 PCB 更加輕量化
  • 透過進程地址空間,可以看到進程的大部分資源,將資源合理分配給每個執行流,就形成了 線程執行流

3.2、線程的優點

線程 最大的優點就是 輕巧、靈活,更容易進行調度

  • 創建一個線程的代價比創建一個進程的代價要小得多
  • 調度線程比調度進程要容易得多
  • 線程占用的系統資源遠小于進程
  • 可以充分利用多處理器的并行數量(進程也可以)
  • 在等待慢速 IO 操作時,程序可以執行其他任務(比如看劇軟件中的 “邊下邊看” 功能)
  • 對于計算密集型應用,可以將計算分解到多個線程中實現(比如 壓縮/解壓 時涉及大量計算)
  • 對于 IO密集型應用,為了提高性能,將 IO操作重疊,線程可以同時等待資源,進行 高效IO(比如 文件/網絡 的大量 IO 需要,可以通過 多路轉接 技術,提高效率)

線程 的合理使用可以提高效率,但 線程 不是越多越好,而是 合適 最好,讓每一個線程都能參與到計算中

3.3、線程的缺點

線程 也是有缺點的:
1、性能損失,當 線程 數量過多時,頻繁的 線程 調度所造成的消耗會導致 計算密集型應用 無法專心計算,從而造成性能損失

2、 健壯性降低,在一個多線程程序里,因時間分配上的細微偏差或者因共享了不該共享的變量而造成不良影響的可能性是很大的,換句話說線程之間是缺乏保護的

在下面這個程序中,次線程2 出現異常后,會導致整個進程運行異常,進而終止進程

#include <iostream>
#include <unistd.h>
#include <pthread.h>using namespace std;void *threadHandler1(void *args)
{while (true){cout << "我是次線程1,我正在運行..." << endl;sleep(1);}
}void *threadHandler2(void *args)
{while (true){sleep(5); // 等其他線程先跑一會cout << "我是次線程2,我正在運行..." << endl;char *ps = "Change World!";*ps = 'N';}
}int main()
{pthread_t t1, t2; // 創建兩個線程pthread_create(&t1, NULL, threadHandler1, NULL);pthread_create(&t2, NULL, threadHandler2, NULL);// 主線程運行while (true){cout << "我是主線程" << endl;sleep(1);}return 0;
}

結果一輪到 次線程2 運行,因為觸發異常,從而整個進程就直接終止了

圖示

為什么 單個線程 引發的錯誤需要讓 整個進程 來承擔?

  • 站在技術角度,完全可以讓其自行承擔,但這不合理
  • 系統角度:線程是進程的執行分支,線程出問題了,進程也不應該繼續運行(比如一顆老鼠屎壞了一鍋湯)
  • 信號角度:線程出現異常后,MMU 識別到異常 -> 操作系統將異常轉換為信號 -> 發送信號給指定進程,信號的對象是進程,自然無法單發給 線程,進而整個進程也就都終止了

3、缺乏訪問控制,進程是訪問控制的基本粒度,在一個線程中調用某些OS函數會對整個進程造成影響

如何證明 輕量級線程 看到的是同一份資源?通過 多進程中,父子進程之間發生寫時拷貝的例子驗證

#include <iostream>
#include <unistd.h>
#include <pthread.h>using namespace std;int g_val = 0;void *threadHandler1(void *args)
{while (true){printf("我是次線程1,我正在運行... &g_val: %p  g_val: %d\n", &g_val, g_val);sleep(1);}
}void *threadHandler2(void *args)
{while (true){printf("我是次線程2,我正在運行... &g_val: %p  g_val: %d\n", &g_val, g_val);g_val++; // 次線程2 每次都需改這個全局變量sleep(1);}
}int main()
{pthread_t t1, t2; // 創建兩個線程pthread_create(&t1, NULL, threadHandler1, NULL);pthread_create(&t2, NULL, threadHandler2, NULL);// 主線程運行while (true){printf("我是主線程,我正在運行... &g_val: %p  g_val: %d\n", &g_val, g_val);sleep(1);}return 0;
}

結果:無論是主線程還是次線程,當其中的一個線程出現修改行為時,其他線程也會同步更改

圖示

多個線程訪問同時訪問一個資源,不加以保護的話,勢必會造成影響,當然這都是后話了(加鎖相關內容)

4、編程難度提高,編寫與調試一個多線程程序需要考慮許多問題,諸如 加鎖、同步、互斥 的等,面對多個執行流時,調試也是非常困難的

3.4、線程的用途

合理的使用 多線程,可以提高 CPU 計算密集型程序的效率

合理的使用 多線程,可以提高 IO 密集型程序中用戶的體驗(具體表現為用戶可以一邊下載,一邊做其他事情)

圖示


🌆總結

以上就是本次關于 Linux多線程【初識線程】的全部內容了,在本文中,我們主要學習了 線程 的基本概念,深入理解了地址空間,比如 如何頁表進行地址的轉換,最后復盤了 線程 的基本概念,學習了其優缺點及使用場景,多線程 是一個十分重要的章節,需要用心學習


星辰大海

相關文章推薦

Linux進程信號 ===== :>
【信號產生】、【信號保存】、【信號處理】

Linux進程間通信 ===== :>

【消息隊列、信號量】、【共享內存】、【命名管道】、【匿名管道】

Linux基礎IO ===== :>

【軟硬鏈接與動靜態庫】、【深入理解文件系統】、【模擬實現C語言文件流】、【重定向及緩沖區理解】、【文件理解與操作】

Linux進程控制 ===== :>

【簡易版bash】、【進程程序替換】、【創建、終止、等待】

Linux進程學習 ===== :>

【進程地址】、【環境變量】、【進程狀態】、【基本認知】

Linux基礎 ===== :>

【gdb】、【git】、【gcc/g++】、【vim】、Linux 權限理解和學習、聽說Linux基礎指令很多?這里都幫你總結好了

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

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

相關文章

分布式事務與解決方案

一、什么是分布式事務 首先我們知道本地事務是指事務方法中的操作只依賴本地數據庫&#xff0c;可保證事務的ACID特性。而在分布式系統中&#xff0c;一個應用系統被拆分為多個可獨立部署的微服務&#xff0c;在一個微服務的事務方法中&#xff0c;除了依賴本地數據庫外&#…

【深入理解ES6】塊級作用域綁定

1. var聲明及變量提升機制 提升&#xff08;Hoisting&#xff09;機制&#xff1a;通過關鍵字var聲明的變量&#xff0c;都會被當成在當前作用域頂部生命的變量。 function getValue(condition){if(condition){var value "blue";console.log(value);}else{// 此處…

代碼隨想錄算法訓練營第三十六天 | 435. 無重疊區間,763.劃分字母區間,56. 合并區間

代碼隨想錄算法訓練營第三十六天 | 435. 無重疊區間&#xff0c;763.劃分字母區間&#xff0c;56. 合并區間 435. 無重疊區間:eyes:題目總結:eyes: 763.劃分字母區間:eyes:題目總結:eyes: 56. 合并區間:eyes:題目總結:eyes: 435. 無重疊區間 題目鏈接 視頻講解 給定一個區間的…

并發編程系列-Semaphore

Semaphore&#xff0c;如今通常被翻譯為"信號量"&#xff0c;過去也曾被翻譯為"信號燈"&#xff0c;因為類似于現實生活中的紅綠燈&#xff0c;車輛是否能通行取決于是否是綠燈。同樣&#xff0c;在編程世界中&#xff0c;線程是否能執行取決于信號量是否允…

8.10 用redis實現緩存功能和Spring Cache

什么是緩存? 緩存(Cache), 就是數據交換的緩沖區,俗稱的緩存就是緩沖區內的數據,一般從數據庫中獲取,存儲于本地代碼。 通過Redis來緩存數據&#xff0c;減少數據庫查詢操作; 邏輯 每個分類的菜品保存一份緩存數據 數據庫菜品數據有變更時清理緩存數據 如何將商品數據緩存起…

p-級數的上界(Upper bound of p-series)

積分判別法-The Integral Test https://math.stackexchange.com/questions/2858067/upper-bound-of-p-series https://courses.lumenlearning.com/calculus2/chapter/the-p-series-and-estimating-series-value/ 兩個重要級數&#xff08;p級數和幾何級數&#xff09; ht…

WPF顯示初始界面--SplashScreen

WPF顯示初始界面–SplashScreen 前言 WPF應用程序的運行速度快&#xff0c;但并不能在瞬間啟動。當第一次啟動應用程序時&#xff0c;會有一些延遲&#xff0c;因為公共語言運行時&#xff08;CLR&#xff09;首先需要初始化.NET環境&#xff0c;然后啟動應用程序。 對于WPF中…

高憶管理:股票T+0交易是什么意思?t+0交易有什么好處?

股票的買賣準則有很多種&#xff0c;T0買賣便是其中之一。那么股票T0買賣是什么意思&#xff1f;t0買賣有什么優點&#xff1f;高憶管理也為大家預備了相關內容&#xff0c;以供參考。 股票T0買賣是什么意思&#xff1f; T0買賣準則是指出資者當天買入的股票能夠在當天賣出&am…

IP 多播協議(IP Multicast Protocol)

IP 多播協議&#xff08;IP Multicast Protocol&#xff09;是一種在網絡中一對多傳輸數據的通信方式。在傳統的單播通信中&#xff0c;數據從一個發送方發送到一個接收方&#xff1b;而在多播通信中&#xff0c;數據可以從一個發送方傳輸到多個接收方&#xff0c;從而有效地節…

SpringBoot 異步、郵件任務

異步任務 創建一個Hello項目 創建一個類AsyncService 異步處理還是非常常用的&#xff0c;比如我們在網站上發送郵件&#xff0c;后臺會去發送郵件&#xff0c;此時前臺會造成響應不動&#xff0c;直到郵件發送完畢&#xff0c;響應才會成功&#xff0c;所以我們一般會采用多線…

神經網絡基礎-神經網絡補充概念-03-邏輯回歸損失函數

概念 邏輯回歸使用的損失函數通常是"對數損失"&#xff08;也稱為"交叉熵損失"&#xff09;或"邏輯損失"。這些損失函數在訓練過程中用于衡量模型預測與實際標簽之間的差異&#xff0c;從而幫助模型逐步調整權重參數&#xff0c;以更好地擬合數…

指靜脈開集測試(OpenSet-test)代碼(包含7個數據集)

七個數據集:sdu、mmc、hkpu、scut、utfvp、vera、nupt 一、SDU 80%用于訓練,20%用于作為開集測試 1.數據集分割代碼 ①先把636個類別提取出來 func: 創建temp_sdu,將636個類劃分出來。下一個代碼塊將進行openset_sdu的分割import os from shutil import copy, rmtre…

c++--SLT六大組件之間的關系

1.SLT六大組件&#xff1a; 容器&#xff0c;迭代器&#xff0c;算法&#xff0c;仿函數&#xff0c;適配器&#xff0c;空間配置器 2.六大組件之間的關系 容器&#xff1a;容器是STL最基礎的組件&#xff0c;沒有容器&#xff0c;就沒有數據&#xff0c;容器的作用就是用來存…

IO流 詳細介紹

一、IO流概述 1.IO&#xff1a;輸入(Input讀取數據)/輸出(Output寫數據) 2.流&#xff1a;是一種抽象概念&#xff0c;是對數據傳輸的總稱,也就是說數據在設備間的傳輸稱為流&#xff0c;流的本質是數據傳輸IO流就是用來處理設備間數據傳輸問題的。 3.常見的應用&#xff1a…

【Sklearn】基于隨機森林算法的數據分類預測(Excel可直接替換數據)

【Sklearn】基于隨機森林算法的數據分類預測(Excel可直接替換數據) 1.模型原理1.1 模型原理1.2 數學模型2.模型參數3.文件結構4.Excel數據5.下載地址6.完整代碼7.運行結果1.模型原理 隨機森林(Random Forest)是一種集成學習方法,通過組合多個決策樹來構建強大的分類或回歸…

JVM - 垃圾回收機制

JVM的垃圾回收機制(簡稱GC) JVM的垃圾回收機制非常強大&#xff0c;是JVM的一個很重要的功能&#xff0c;而且這也是跟對象實例息息相關的&#xff0c;如果對象實例不用了要怎么清除呢&#xff1f; 如何判斷對象已經沒用了 當JVM認為一個對像已經沒用了&#xff0c;就會把這個…

初識Sentinel

目錄 1.解決雪崩的方式有4種&#xff1a; 1.1.2超時處理&#xff1a; 1.1.3倉壁模式 1.1.4.斷路器 1.1.5.限流 1.1.6.總結 1.2.服務保護技術對比 1.3.Sentinel介紹和安裝 1.3.1.初識Sentinel 1.3.2.安裝Sentinel 1.4.微服務整合Sentinel 2.流量控制 2.1.簇點鏈路 …

Ubuntu中怎么清空mysql數據

要清空 MySQL 數據&#xff0c;可以使用以下步驟來執行。請注意&#xff0c;這將會永久刪除數據庫中的所有數據&#xff0c;請謹慎操作&#xff0c;并在操作前備份重要數據。 登錄 MySQL&#xff1a; 打開終端&#xff0c;使用以下命令登錄到 MySQL 數據庫。根據情況&#xf…

黑馬項目一階段面試58題 Web14題(一)

一、什么是AJAX 異步的JavaScript和XML。用來做前端和后端的異步請求的技術。 異步請求&#xff1a;只更新部分前端界面的請求&#xff0c;做到局部更新。 比如注冊&#xff0c;提示用戶名已存在而整個頁面沒有動 比如百度圖片搜索美女&#xff0c;進度條越變越短&#xff…

== 和 equals 的對比 [面試題]

和 equals 的對比[面試題] 文章目錄 和 equals 的對比[面試題]1. 和 equals 簡介2. Object 類中 equals() 源碼3. String 類中 equals() 源碼4. Integer 類中 equals() 源碼5. 如何重寫 equals 方法 1. 和 equals 簡介 是一個比較運算符 &#xff1a;既可以判斷基本數據類型…