?個人主頁: 北 海
🎉所屬專欄: 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
代酷睿 系列芯片,就在其宣傳頁中提到了 線程 這個詞
硬件上的 線程 概念我們這里不討論,接下來看看操作系統層面的 線程概念
教材觀點
- 線程就是一個執行分支、執行粒度比進程更細、調度成本更低
- 線程就是進程內部的一個執行流
內核觀點
- 進程是承擔系統資源分配的基本實體,而線程是
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
結構,并且 PCB
與 TCB
都屬于 task_strcut
,所以才說 線程是 CPU
運行的基本單位
總結:進程是由操作系統將程序運行所需地址空間、映射關系、代碼和數據打包后的資源包,而 線程/輕量級線程/執行流 則是利用資源完成任務的基本單位
線程包含于進程中,進程本身也是一個線程
我們之前學習的進程概念是不完整的,引入線程之后,可以對進程有一個更加全面的認識
通常將程序啟動,比如 main
函數中的這個線程稱為 主線程,其他線程則稱為 次線程
實際上 進程 = PCB
+ TCB
+ 虛擬地址空間 + 映射關系 + 代碼和數據,這才是一個完整的概念
以后談及進程時,就要想到 一批執行流+可支配的資源
進程與線程的概念并不沖突,而是相互成就
在 Linux
中,認為 PCB
與 TCB
的共同點太多了,于是直接復用了 PCB
的設計思想和調度策略,在進行 線程管理 時,完全可以復用 進程管理 的解決方案(代碼和結構),這可以大大減少系統調度時的開銷,做到 小而美,因此 Linux
中實際是沒有真正的 線程 概念的,有的只是復用 PCB
設計思想的 TCB
在這種設計思想下,線程 注定不會過于龐大,因此 Linux
中的 線程 又可以稱為 輕量級進程(LWP
),輕量級進程 足夠簡單,且 易于維護、效率更高、安全性更強,可以使得 Linux
系統不間斷的運行程序,不會輕易 崩潰
與 一切皆文件一樣,這種設計思想注定 Linux
會成為一款 卓越 的操作系統
別的系統采用的是其他方案,比如
Windows
使用的是真線程方案,為TCB
額外設計了一邏輯,這就導致操作系統在同時面臨PCB
和TCB
時需要進行識別后切換成不同的處理手段,存在不同的邏輯容易增加系統運行不穩定的風險,這就導致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:第一個線程的
PID
和LWP
是一樣的
其中,第一個線程就是 主線程,也就是我們之前一直很熟悉的 進程,因為它的 PID
和 LWP
是一樣的,所以只需要關心 PID
也行
操作系統如何判斷調度時,是切換為 線程 還是切換為 進程 ?
- 將待切換的執行流
PID
與當前執行流的PID
進行比對,如果相同,說明接下來要切換的是 線程,否則切換的就是 進程 - 操作系統只需要找到
LWP
與PID
相同的線程,即可輕松鎖定 主線程
線程是進程的一部分,給其中任何一個線程發送信號,都會影響到其他線程,進而影響到整個進程
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
,那么等分為 4KB
的 頁 Page
,可以得到約 100w
個 頁 Page
,其中 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
:
IO
的基本單位,內核系統/文件系統 都對其提供了支持- 利于通過 局部性原理 預測數據的命中情況,盡可能提高效率
總結:IO
的基本單位是 4KB
,內存實際上被劃分成了很多個 4KB
的小塊,并存在相應的數據結構對其進行管理
2.3、深入頁表
顯然,頁表 絕對不可能動輒幾十個 GB
,實際在根據 虛擬地址 進行尋址時,頁表 也有自己的設計邏輯
虛擬地址(32
位操作系統) 大小也就是 32
比特位,大概也就是 4Byte
,通常將一個 虛擬地址 分割為三份:10
、10
、12
10
:虛擬地址中的前10
個比特位,用于尋址 頁表210
:虛擬地址中間的10
個比特位,用于尋找 頁框起始地址12
:虛擬地址中的后12
個比特位,用于定位 具體地址(偏移量)
所以,實際上在通過 頁表 進行尋址時,需要用到 兩個頁表(為了方便演示,僅包含一組 kv
關系):
注:“頁表2” 中的 20
表示內存中的下標,即 頁框地址
通常將 “頁表1” 稱為 頁目錄,“頁表2” 稱為 頁表項
- 頁目錄:使用
10
個比特位定位 頁表項 - 頁表項:使用
10
個比特位定位 頁框地址 - 偏移量:使用
12
個比特位,在 頁Page
中進行任意地址的尋址
所以即使是每個 物理地址 都被尋址的的極端情況下,頁表 總大小不過為:(2^10 + 2^10) * (2^10 + 2^20)
,大約也就需要 4Mb
大小,即可映射至每一個 物理內存,但實際上 物理內存 并不會被時刻占滿,大多數情況下都是使用一部分,因此實際 頁表 大小不過 幾十字節
像這種 頁框起始地址+偏移量 的方式稱為 基地址+偏移量,是一種運用十分廣泛的思想,比如所謂的 類型(int
、double
、char
…)都是通過 類型的起始地址+類型的大小 來標識該變量大小的,也就是說我們只需要 獲得變量的起始地址,即可自由進行偏移操作(如果偏移過度了,就是越界),這也就解釋了為什么取地址只會取到 起始地址
總結:得益于 劃分+偏移 的思想,使得頁表的大小可以變得很小
擴展:動態內存管理
實際上,我們在進行 動態內存管理(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基礎指令很多?這里都幫你總結好了