在多線程項(xiàng)目開發(fā)時(shí),最常用、最常遇到的問題是
1,線程、協(xié)程安全
2,線程、協(xié)程間的通信和控制
本文主要探討不同開發(fā)語言go、java、python在進(jìn)程、線程和協(xié)程上的設(shè)計(jì)和開發(fā)方式的異同。
1. 進(jìn)程、線程和協(xié)程上的差異
1.1 進(jìn)程、線程、協(xié)程的定義
-
進(jìn)程
進(jìn)程是操作系統(tǒng)進(jìn)行資源分配的基本單位,每個(gè)進(jìn)程都有自己的獨(dú)立內(nèi)存空間,不同的進(jìn)程之間無法相互干擾。由于進(jìn)程比較重,占據(jù)獨(dú)立的內(nèi)存,所以上下文進(jìn)程間的切換開銷(棧、寄存器、虛擬內(nèi)存、文件句柄等)比較大,但相對(duì)比較穩(wěn)定安全。 -
線程
線程又叫做輕量級(jí)進(jìn)程,是進(jìn)程的一個(gè)實(shí)體,是處理器任務(wù)調(diào)度和執(zhí)行的基本單位位(能夠申請(qǐng)到cpu資源執(zhí)行相關(guān)任務(wù))。它是比進(jìn)程更小的能獨(dú)立運(yùn)行的基本單位。線程只擁有一點(diǎn)在運(yùn)行中必不可少的資源(如程序計(jì)數(shù)器,一組寄存器和棧),但是它可與同屬一個(gè)進(jìn)程的其他的線程共享進(jìn)程所擁有的全部資源。
線程的執(zhí)行需要申請(qǐng)對(duì)應(yīng)的cpu資源,因此線程切換涉及CPU的資源切換(保存cpu上下文、觸發(fā)軟中斷暫停當(dāng)前線程、從就緒線程中選擇一個(gè)執(zhí)行),過程中會(huì)涉及用戶態(tài) -> 內(nèi)核態(tài)(切換cpu)-> 用戶態(tài)的切換,因此開銷比較大。 -
協(xié)程
協(xié)程,又稱微線程,是一種用戶態(tài)的輕量級(jí)線程,協(xié)程的調(diào)度完全由用戶控制(也就是在用戶態(tài)執(zhí)行)。協(xié)程擁有自己的寄存器上下文和棧。協(xié)程調(diào)度切換時(shí),將寄存器上下文和棧保存到線程的堆區(qū),在切回來的時(shí)候,恢復(fù)先前保存的寄存器上下文和棧,直接操作棧則基本沒有內(nèi)核切換的開銷,所以上下文的切換非??欤?strong>協(xié)程切換,線程不變,因此不需要切換cpu,不進(jìn)行內(nèi)核態(tài)切換,成本較低)。
進(jìn)程、線程、協(xié)程之間的關(guān)系可以如下圖詮釋
1.2 進(jìn)程、線程、協(xié)程的差異
線程進(jìn)程的區(qū)別:
- 根本區(qū)別:進(jìn)程是操作系統(tǒng)資源分配的基本單位,而線程是處理器任務(wù)調(diào)度和執(zhí)行的基本單位,cpu運(yùn)行任務(wù)是運(yùn)行線程
- 資源開銷:每個(gè)進(jìn)程都有獨(dú)立的代碼和數(shù)據(jù)空間,程序之間的切換會(huì)有較大的開銷;線程可以看做輕量級(jí)的進(jìn)程,同一進(jìn)程的線程共享代碼和數(shù)據(jù)空間,每個(gè)線程都有自己獨(dú)立的運(yùn)行棧和程序計(jì)數(shù)器,線程之間切換的開銷小。
- 包含關(guān)系:如果一個(gè)進(jìn)程內(nèi)有多個(gè)線程,則執(zhí)行過程不是一條線的,而是多條線(線程)共同完成的。
- 內(nèi)存分配:同一進(jìn)程的線程共享本進(jìn)程的地址空間和資源,而進(jìn)程之間的地址空間和資源是相互獨(dú)立的。
- 影響關(guān)系:一個(gè)進(jìn)程崩潰后,在保護(hù)模式下不會(huì)對(duì)其他進(jìn)程產(chǎn)生影響,但是一個(gè)線程崩潰整個(gè)進(jìn)程都死掉。所以多進(jìn)程要比多線程健壯。
- 執(zhí)行過程:每個(gè)獨(dú)立的進(jìn)程有程序運(yùn)行的入口、順序執(zhí)行序列和程序出口。但是線程不能獨(dú)立執(zhí)行,必須依存在應(yīng)用程序中,由應(yīng)用程序提供多個(gè)線程執(zhí)行控制。兩者均可并發(fā)執(zhí)行。
協(xié)程與線程的區(qū)別:
- 一個(gè)線程可以有多個(gè)協(xié)程。
- 大多數(shù)業(yè)務(wù)場景下,線程進(jìn)程可以看做是同步機(jī)制,而協(xié)程則是異步。
- 線程是搶占式,而協(xié)程是非搶占式的,所以需要用戶代碼釋放使用權(quán)來切換到其他協(xié)程,因此同一時(shí)間其實(shí)只有一個(gè)協(xié)程擁有運(yùn)行權(quán),相當(dāng)于單線程的能力。
- 協(xié)程并不是取代線程,而且抽象于線程之上。線程是被分割的CPU資源, 協(xié)程是組織好的代碼流程, 協(xié)程需要線程來承載運(yùn)行。
1.3 進(jìn)程、線程、協(xié)程的內(nèi)存成本
進(jìn)程占用內(nèi)存
- 32 位操作系統(tǒng)只支持 4G 內(nèi)存的內(nèi)存條,這是因?yàn)檫M(jìn)程在 32 位操作系統(tǒng)中最多只能占用 4G 內(nèi)存
- 在 64 位操作系統(tǒng)中可以占用更多內(nèi)存。
線程占用內(nèi)存
- 一般是 10MB,不同的操作系統(tǒng)版本之間有些差異,區(qū)間在 4M - 64M。
協(xié)程占用內(nèi)
- 一個(gè)協(xié)程占用 2KB 左右的內(nèi)存
內(nèi)存占用: 進(jìn)程 >> 線程 >> 協(xié)程
更低的內(nèi)存占用代表著更低的資源切換成本和可以提供更高的并發(fā)。
1.4 進(jìn)程、線程、協(xié)程的切換成本
不同的進(jìn)程享有獨(dú)立的資源,因此進(jìn)程切換,需要執(zhí)行如下2個(gè)步驟
- 切換頁目錄以使用新的地址空間(切換虛擬內(nèi)存空間)
- 切換內(nèi)核棧和硬件上下文(切換cpu資源)
相同進(jìn)程的線程共享相同的內(nèi)存,因此切換線程
- 使用的是進(jìn)程的內(nèi)存資源,不需要切換虛擬內(nèi)存空間
- 切CPU換上下文時(shí),需要耗費(fèi) CPU 時(shí)間,但是進(jìn)程切換的開銷相差不大(幾微秒)。
相同線程的協(xié)程使用相同的內(nèi)存和cpu資源,因此協(xié)程切換
- 在用戶空間發(fā)生,不需要切換cpu,只需要切換簡單CPU寄存器狀態(tài)
- 一次協(xié)程的上下文切換最多需要幾十納秒的時(shí)間。
切換成本: 進(jìn)程切換 > 線程切換 > 協(xié)程切換
2. 線程、協(xié)程之間的通信和協(xié)作方式
線程、協(xié)程之間的通信主要用于2個(gè)目的
- 控制線程、協(xié)程的執(zhí)行順序(觸發(fā)條件、邏輯啟停等)
- 線程、協(xié)程之間傳遞信息,用于在不同線程、協(xié)程之間實(shí)現(xiàn)業(yè)務(wù)邏輯
- 感知子線程、協(xié)程是否已經(jīng)執(zhí)行完成
注意,
如果不同的線程進(jìn)行在寫操作時(shí),需要注意變量的線程安全問題
- 如果使用的的對(duì)象是線程安全的,不需要加鎖保護(hù),但是需要注意多個(gè)線程使用相同的對(duì)象以及相關(guān)對(duì)象的性能問題
- 如果使用的對(duì)象不是線程安全的,注意進(jìn)行鎖保護(hù)。
2.1 python如何實(shí)現(xiàn)線程通信?
通常使用如下方法進(jìn)行線程同步,可以根據(jù)實(shí)際情況調(diào)整
- 共享變量
- queue隊(duì)列
更多可以參考 python的多線程及線程間的通信方式
2.2 java如何實(shí)現(xiàn)線程通信?
通常使用如下方法進(jìn)行線程同步,可以根據(jù)實(shí)際情況調(diào)整
- 鎖與同步
- 等待/通知機(jī)制
- 信號(hào)量
- 管道
更多可以參考 Java線程間的通信
2.3 go如何實(shí)現(xiàn)線程通信?
在go中,常用的是協(xié)程(goroutine)進(jìn)行多并發(fā),因此探討的通信方式都是以協(xié)程(goroutine)進(jìn)行討論。
實(shí)現(xiàn)多個(gè)goroutine間的同步與通信大致有:
- 全局共享變量
- channel通信(CSP模型)
- Context包
這3種方法具體實(shí)現(xiàn)可以參考文檔 深入golang之—goroutine并發(fā)控制與通信
3. 常用線程池的實(shí)現(xiàn)和使用方式
3.1 python常用線程池
線程池的基類是 concurrent.futures 模塊中的 Executor,Executor 提供了兩個(gè)子類,即 ThreadPoolExecutor 和 ProcessPoolExecutor,其中 ThreadPoolExecutor 用于創(chuàng)建線程池,而 ProcessPoolExecutor 用于創(chuàng)建進(jìn)程池。
由于全局GIL鎖存在,python多線程本質(zhì)上同一時(shí)間只能1個(gè)線程在執(zhí)行,并不能高效的利用所有的CPU核心。
1, 如果使用多線程,線程的類型基本都是IO密集型,線程進(jìn)入IO等到時(shí)會(huì)自動(dòng)釋放GIL索引,因此GIL鎖的存在對(duì)于這種類型的計(jì)算性能影響不算大
2,如果使用多線程,線程的類型基本都是CPU密集型,只能等待解釋器不間斷運(yùn)行了1000字節(jié)碼(Py2)或運(yùn)行15毫秒(Py3)后,該線程也會(huì)放棄GIL,切換到其他的線程執(zhí)行。
使用線程池來執(zhí)行線程任務(wù)的步驟如下:
- 調(diào)用 ThreadPoolExecutor 類的構(gòu)造器創(chuàng)建一個(gè)線程池。
- 定義一個(gè)普通函數(shù)作為線程任務(wù)。
- 調(diào)用 ThreadPoolExecutor 對(duì)象的 submit() 方法來提交線程任務(wù)。
- 當(dāng)不想提交任何任務(wù)時(shí),調(diào)用 ThreadPoolExecutor 對(duì)象的 shutdown() 方法來關(guān)閉線程池。
def test(value1, value2=None):
print("%s threading is printed %s, %s"%(threading.current_thread().name, value1, value2))
time.sleep(2)
return 'finished'
def test_result(future):
print(future.result())
if __name__ == "__main__":
import numpy as np
from concurrent.futures import ThreadPoolExecutor
threadPool = ThreadPoolExecutor(max_workers=4, thread_name_prefix="test_")
for i in range(0,10):
future = threadPool.submit(test, i,i+1)
threadPool.shutdown(wait=True)
更多使用參考PYTHON線程池及其原理和使用(超級(jí)詳細(xì))
3.2 java常用線程池
常用4中類型的線程池
- newFixedThreadPool
構(gòu)造函數(shù)
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
從構(gòu)造方法可以看出,它創(chuàng)建了一個(gè)固定大小的線程池,每次提交一個(gè)任務(wù)就創(chuàng)建一個(gè)線程,直到線程達(dá)到線程池的最大值nThreads。線程池的大小一旦達(dá)到最大值后,再有新的任務(wù)提交時(shí)則放入無界阻塞隊(duì)列中,等到有線程空閑時(shí),再從隊(duì)列中取出任務(wù)繼續(xù)執(zhí)行。
- newCachedThreadPool
構(gòu)造函數(shù)
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
從構(gòu)造方法可以看出,它創(chuàng)建了一個(gè)可緩存的線程池。當(dāng)有新的任務(wù)提交時(shí),有空閑線程則直接處理任務(wù),沒有空閑線程則創(chuàng)建新的線程處理任務(wù),隊(duì)列中不儲(chǔ)存任務(wù)。線程池不對(duì)線程池大小做限制,線程池大小完全依賴于操作系統(tǒng)(或者說JVM)能夠創(chuàng)建的最大線程大小。如果線程空閑時(shí)間超過了60秒就會(huì)被回收。(使用方法不是非常推薦)
- newSingleThreadExecutor
構(gòu)造函數(shù)
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
從構(gòu)造方法可以看出,它創(chuàng)建了一個(gè)單線程化的線程池,它只會(huì)用唯一的工作線程來執(zhí)行任務(wù),保證所有任務(wù)按照指定順序執(zhí)行,無法指定最大線程池?cái)?shù)量。(使用方法不是非常推薦)
- newScheduledThreadPool
構(gòu)造函數(shù)
public class OneMoreStudy {
public static void main(String[] args) {
final SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(3);
System.out.println("提交時(shí)間: " + sdf.format(new Date()));
scheduledThreadPool.schedule(new Runnable() {
@Override
public void run() {
System.out.println("運(yùn)行時(shí)間: " + sdf.format(new Date()));
}
}, 3, TimeUnit.SECONDS);
scheduledThreadPool.shutdown();
}
}
這個(gè)方法創(chuàng)建了一個(gè)固定大小的線程池,支持定時(shí)及周期性任務(wù)執(zhí)行。創(chuàng)建并執(zhí)行ScheduledFuture,該ScheduledFuture在指定的延遲后啟用,任務(wù)立即提交給線程池,線程池安排線程在指定時(shí)間后正式開始運(yùn)作,運(yùn)作以后保持正常節(jié)奏(類似調(diào)度任務(wù))
根據(jù)使用習(xí)慣選擇合適的方法類,更多可以參考Java中常用的四種線程池
3.3 go常用線程池
go的基礎(chǔ)方法類中沒有實(shí)現(xiàn)線程池,需要自己實(shí)現(xiàn),或者引入第三方庫進(jìn)行實(shí)現(xiàn)。
4. 疑問和思考
4.1 go語言中,協(xié)程的成本已經(jīng)很低,還有必要使用線程池嗎?
梳理常用的開發(fā)語言中,是有已經(jīng)有了現(xiàn)成的線程池方法(類)提供使用,情況如下:
開發(fā)語言 | 是否支持線程池 | 備注 |
---|---|---|
python | 是 | |
java | 是 | |
go | 否 | 可以引用第三方的庫或者自己實(shí)現(xiàn) |
go的協(xié)程已經(jīng)把單個(gè)協(xié)程的成本降低到足夠低,還有必要設(shè)計(jì)線程池嗎?該問題在Go Forum 中 skillian 做了解答。
我引用回復(fù)文章來源:http://www.zghlxwxcb.cn/news/detail-814416.html
Like lutzhorn said: Need? No.
But for some workloads in some projects, it might make sense to have a general worker pool implementation. The benefit is that the memory consumption can be limited by not allowing the number of goroutines to exceed whatever the pool allows, though I’m unsure of what order of magnitude of goroutines you need before that benefit is manifested.
Francesc Campoy created a fractal with 4 million goroutines (link 55) and it worked and scaled, but not perfectly. The issue wasn’t with the number of goroutines but that the runtime spent more time managing the goroutines than the goroutines actually worked. By giving the goroutines more work, (I think instead of each goroutine processing only one pixel, they processed the whole line?) the solution still scaled and ended up performing better.
翻譯過來就是
1, 通常不需要
2, 除了特殊場景,特殊項(xiàng)目上,線程池是有意義的。這樣做的好處是,可以通過不允許超過池允許的程序的數(shù)量來限制內(nèi)存消耗,盡管我不確定在顯示出這種好處之前需要多少量級(jí)的程序。文章來源地址http://www.zghlxwxcb.cn/news/detail-814416.html
5. 參考文檔
- 一文快速了解進(jìn)程、線程與協(xié)程
- 進(jìn)程、線程以及協(xié)程的區(qū)別
- 深入golang之—goroutine并發(fā)控制與通信
- Java線程間的通信
到了這里,關(guān)于不同開發(fā)語言在進(jìn)程、線程和協(xié)程的設(shè)計(jì)差異的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!