瞭解一下線程模型還是很有必要的,如果不清楚語言層面上的線程在操作系統層面怎麼映射使用,在使用過程中就會不清不楚,可能會踩一些坑 ...
背景知識
- 軟體是如何驅動硬體的?
硬體是需要相關的驅動程式才能執行,而驅動程式是安裝在操作系統內核中。如果寫了一個程式A,A程式想操作硬體工作,首先需要進行系統調用,由內核去找對應的驅動程式驅使硬體工作。而驅動程式怎麼讓硬體工作的呢?驅動程式作為硬體和操作系統之間的媒介,可以把操作系統中相關的指令翻譯成硬體能夠識別的電信號,同時,驅動程式也可以將硬體的電信號轉為操作系統能夠識別的指令。 - 進程、輕量級進程、線程關係
一個進程由於所運行的空間不同,被分為內核線程和用戶進程,之所有稱之為內核線程,是因為其不擁有虛擬地址空間。如果創建一個新的用戶進程,會分配一個新的虛擬地址空間,不同用戶進程之間資源是隔離的。由於創建一個新的進程需要消耗很多的資源,並且在進程之間切換的代價也很昂貴,因此引入了輕量級進程。輕量級進行本質上也是對內核線程的高層抽象,雖然不同的輕量級進程之間可以共用某些資源,但由於輕量級進程本質上還是內核線程,如果進行輕量級線程之間的切換,需要進行系統調用,代價也是比較昂貴的。內核本質上只能感知到進程的存在,像不同語言的多線程技術,是在用戶進程的基礎上創建的線程庫,線程本身不參與處理器競爭,而是由其所屬的用戶進程參與處理器的競爭。 - 如何理解用戶態和內核態
首先我們需要理解到電腦資源是有限的,不管是CPU資源、記憶體資源、IO資源、網路資源,為了保證這些資源的合理利用,需要有一個管控機制,而這個管控機制都是交於操作系統來處理的。用戶態和內核態是操作系統的一種邏輯劃分,本質上是進行許可權控制,處於用戶態的進程可以直接使用分配給其的記憶體空間,但如果想使用CPU等稀缺資源,處於用戶態的進程就沒有這個許可權了,必須通過系統調用,讓當前進程進入內核態,這樣可以有更大的許可權去申請CPU資源、記憶體資源、IO資源等;
操作系統線程模型
java語言
線程模型
在Java誕生之初,在Java中就引入了線程,最初稱之為“綠色線程”,完全由JVM進行管理,這和操作系統用戶線程是多對一的實現,但隨著操作系統對線程支持越來越強大,java中的線程實現採用了一對一的實現,即一個java線程對應於一個操作系統用戶線程,但是這個線程的堆棧大小是固定的,隨著線程數量創建過多,可能導致記憶體溢出。在java19版本中引入了虛擬線程的概念,虛擬線程有一個動態的堆棧,可以增大和縮小,這和操作系統用戶線程之間是一個多對多的關係,隨著後面的發展,java中的線程模型會變得越來越強大。
優缺點
作為一對一的線程模型維護起來比較簡單,但是由於每一個線程棧信息是固定的,不利於創建大量的線程,並且多線程操作時可能涉及頻繁的系統調用,上下文切換代價高。
使用方式(以生產者消費者模型來說明)
public class ThreadTest {
public static final Object P = new Object();
static List<Integer> list = new ArrayList<>();
@Test
public void test() throws Exception {
Thread thread1 = new Thread(()-> {
while(true) {
try {
product();
}catch (Exception e) {
e.printStackTrace();
}
}
});
Thread thread2 = new Thread(() -> {
while(true) {
try {
consume();
}catch (Exception e) {
e.printStackTrace();
}
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
}
private static void product() throws Exception {
synchronized (P) {
if(list.size() == 1) {
// 讓出鎖
P.wait();
}
list.add(1);
System.out.println("produce");
P.notify();
}
}
private static void consume() throws Exception {
synchronized (P) {
if(list.size() == 0) {
P.wait();
}
list.remove(list.size() - 1);
System.out.println("consume");
P.notify();
}
}
}
go語言
go語言線程模型
在go語言中,線程模型就是比較強大了,包含了三個概念:內核線程(M)、goroutine(G)、G的上下文環境(P)。其中G表示基於協程創建的用戶線程,M直接關聯一個內核線程,P裡面一般存放正在運行的goroutine的上下文環境(函數指針、堆棧地址和地址邊界等)。
優缺點
go語言中的線程模型算是很強大了,引用了協程,線程棧大小可以動態調整,很好地避免了java中目前的線程模型缺點。
使用方式(以生產者消費者模型來說明)
package main
import (
"fmt"
)
type ThreadTest struct {
lock chan int
}
func (t *ThreadTest) produce() {
for {
t.lock <- 10
fmt.Println("produce:", 10)
}
}
func (t *ThreadTest) consume() {
for {
v := <-t.lock
fmt.Println("consume:", v)
}
}
func main() {
maxLen := 10
t := &ThreadTest{
make(chan int, maxLen),
}
// 重點在這裡,開啟新的協程,配合通道,讓go的多線程變成非常優雅
go t.consume()
go t.produce()
select {}
}
c++語言
c++語言線程模型
在c++11中增加了操作thread庫,提供對線程操作的進一步封裝,而這個庫底層是使用了pthread庫,這個庫底層採用了1:1線程模型,跟java中的線程模型類似。
優缺點
作為一對一的線程模型維護起來比較簡單,但是由於每一個線程棧信息是固定的,不利於創建大量的線程,並且多線程操作時可能涉及頻繁的系統調用,上下文切換代價高。
使用方式(以生產者消費者模型來說明)
#include
#include
#include
#include
static const int SIZE = 10;
static const int ITEM_SIZE = 30;
std::mutex mtx;
std::condition_variable not_full;
std::condition_variable not_empty;
int items[SIZE];
static std::size_t r_idx = 0;
static std::size_t w_idx = 0;
void produce(int i) {
std::unique_lock lck(mtx);
while((w_idx+ 1) % SIZE == r_idx) {
std::cout << "隊列滿了" << std::endl;
not_full.wait(lck);
}
items[w_idx] = i;
w_idx = (w_idx+ 1) % SIZE;
not_empty.notify_all();
lck.unlock();
}
int consume() {
int data;
std::unique_lock lck(mtx);
while(w_idx == r_idx) {
std::cout << "隊列為空" << std::endl;
not_empty.wait(lck);
}
data = items[r_idx];
r_idx = (r_idx + 1) % SIZE;
not_full.notify_all();
lck.unlock();
return data;
}
void p_t() {
for(int i = 0; i < ITEM_SIZE; i++) {
produce(i);
}
}
void c_t() {
static int cnt = 0;
while(1) {
int item = consume();
std::cout << "消費第" << item << "個商品" << std::endl;
if(++cnt == ITEM_SIZE) {
break;
}
}
}
int main() {
std::thread producer(p_t);
std::thread consumer(c_t);
producer.join();
consumer.join();
}
python語言
python線程模型
python中的線程使用了操作系統的原生線程,python虛擬機使用了一個全局互斥鎖(GIL)來互斥線程對Python虛擬機的使用,當一個線程獲取GIL的許可權之後,其他的線程必須等待這個線程釋放GIL鎖,索引再多核CPU上,python多線程也會退化為單線程,無法利用多核的優勢。
優缺點
python語言多線程由於GIL的存在,在計算密集型場景上,很難體現到優勢,並且由於涉及線程切換的代碼,反而可能性能還不如單線程好。
使用方式(以生產者消費者模型來說明)
#! /usr/bin/python3
import threading
import random
import time
total = 100
lock = threading.Lock()
totalTime = 10
gTime = 0
class Consumer(threading.Thread):
def run(self):
global total
global gTime
while True:
cur = random.randint(10, 100)
lock.acquire()
if total >= cur:
total -= cur
print("{}使用了{}, 當前剩餘{}".format(threading.current_thread(), cur, total))
else:
print("{}準備使用{},當前剩餘{},不足,不能消費".format(threading.current_thread(), cur, total))
if gTime == totalTime:
lock.release()
break
lock.release()
time.sleep(0.7)
class Producer(threading.Thread):
def run(self):
global total
global gTime
while True:
cur = random.randint(10, 100)
lock.acquire()
if gTime == totalTime:
lock.release()
break
total += cur
print("{}生產了{}, 剩餘{}".format(threading.current_thread(), cur, total))
gTime+= 1
lock.release()
time.sleep(0.5)
if __name__ == '__main__':
t1 = Producer(name="生產者")
t1.start()
t2 = Consumer(name="消費者")
t2.start()
總結
在目前的線程模型中,有1:1、M:1、M:N多種線程模型,具體採用哪種線程模型也和硬體和操作系統的支持程度有關,像誕生比較早的語言,普通採用M:1、1:1線程模型,像c++、java。而新誕生不久的go語言,採用的是M:N線程模型,在多線程的支持上更加強大。
感覺瞭解一下線程模型還是很有必要的,如果不清楚語言層面上的線程在操作系統層面怎麼映射使用,在使用過程中就會不清不楚,可能會踩一些坑,我們都知道在java中不同無限的創建線程,這會導致記憶體溢出,go語言中對多線程支持更加強大,很多事情不需要我們再去關註了,在語言底層已經幫助我們做了。
每種語言的底層細節太多了,如果想深入研究某一個技術,還是得花精力去研究。
作者:京東零售 薑昌偉
來源:京東雲開發者社區