拋磚引玉睬隶,同大家一起交流汗洒;可能會(huì)從兩個(gè)大方向上來(lái)開(kāi)始:一是來(lái)看看為什么并發(fā)編程難,包括涉及的一些基礎(chǔ)硬件和操作系統(tǒng)相關(guān)的一點(diǎn)知識(shí)晚缩;二是一起來(lái)分析一下常同的并發(fā)模型,我們?cè)趺赐ㄒ粋€(gè)簡(jiǎn)單高效的并發(fā)模型來(lái)寫出簡(jiǎn)單的并發(fā)實(shí)現(xiàn)(如線程與鎖/actor/csp/)媳危;
并發(fā)與并行
并發(fā):是邏輯上發(fā)生的同時(shí)荞彼;如一個(gè)處理器同時(shí)處理多個(gè)任務(wù),CPU時(shí)間片發(fā)生切換
并行:是物理上發(fā)生的同時(shí)待笑;是多核處理器同時(shí)處理多個(gè)任務(wù)鸣皂;
系統(tǒng)硬件體系架構(gòu)
這里做一個(gè)假設(shè),如果CPU1在對(duì)一個(gè)變量執(zhí)行一個(gè)CAS的操作,而該變量的緩存行是在CPU7的高速緩存里面签夭,那么可能的執(zhí)行順序可能是:
CPU1檢查本地高速緩存齐邦,沒(méi)有找到緩存行椎侠;
請(qǐng)求轉(zhuǎn)發(fā)到CPU0與CPU1的互聯(lián)模塊第租,檢查CPU0的本地高速緩存,沒(méi)有找到緩存行我纪;
請(qǐng)求轉(zhuǎn)發(fā)到系統(tǒng)互聯(lián)模塊慎宾,檢查其它三個(gè)芯片,得知緩存行被CPU6與CPU7所在的芯片持有浅悉;
請(qǐng)求被轉(zhuǎn)發(fā)到CPU6和CPU7的互聯(lián)模塊趟据,檢查這兩個(gè)CPU的高速緩存,在CPU7的高速緩存中找到緩存行术健;
CPU6將緩存行發(fā)送給所屬的互聯(lián)模塊汹碱,并刷新自己高速緩存中的緩存行;
CPU6和CPU7的互聯(lián)模塊將緩存行發(fā)送給系統(tǒng)互聯(lián)模塊荞估;
系統(tǒng)互聯(lián)模塊將緩存行發(fā)送給CPU0和CPU1的互聯(lián)模塊咳促;
CPU0和CPU1的互聯(lián)模塊將緩存行發(fā)送給CPU1的高速緩存;
CPU1對(duì)高速緩存中的變量執(zhí)行CAS操作勘伺;
這是一個(gè)簡(jiǎn)化并忽略了某些復(fù)雜的事件序列跪腹,因?yàn)?
其它CPU可能試圖在相同的緩存行上執(zhí)行并發(fā)的CAS操作;
緩存行可能被只讀復(fù)制到其它的CPU高速緩存中飞醉,這種情況下有必要刷新它們的緩存冲茸;
當(dāng)請(qǐng)求到達(dá)時(shí),CPU7可能已經(jīng)在緩存上操作缅帘,這種情況下CPU7必須你保留這個(gè)請(qǐng)求轴术,直到請(qǐng)求完成、
CPU7可能已經(jīng)從緩存中排出它的緩存行钦无,這樣當(dāng)請(qǐng)求到達(dá)時(shí)逗栽,緩存行已經(jīng)寫入內(nèi)存中了;
在緩存行中可能發(fā)生一個(gè)可糾正的錯(cuò)誤铃诬,因此需要在使用數(shù)據(jù)前糾正它祭陷;
...
CPU的緩存一致性極其復(fù)雜,所以高效率(榨干最后一滴CPU資源趣席,每瓦特性能)且可靠的并行編程總是太不容易!
除了CPU高速緩存還有許多其它因素兵志,如內(nèi)存引用、原子操作宣肚、內(nèi)存屏障想罕、I/O操作等等。
內(nèi)存引用
微處理器從內(nèi)存里讀一個(gè)值的時(shí)間,微處理器可以用這段時(shí)間執(zhí)行成成百甚至上行知指令按价,雖然一直在極大的減少內(nèi)存訪問(wèn)的延遲惭适,但是仍只有高度可以預(yù)測(cè)的數(shù)據(jù)訪問(wèn)模式才能讓緩存發(fā)揮最大效果;
原子操作
CPU會(huì)通過(guò)一條『流水線』來(lái)控制CPU內(nèi)部的指令流(現(xiàn)在微處理器都可以支持多條流水線并行)楼镐,這種架構(gòu)使得CPU流水線的可以一次執(zhí)行多個(gè)操作癞志,而原子操作正與這種特性有沖突;比如一種常見(jiàn)的技巧是標(biāo)出所有包含原子操作所需數(shù)據(jù)的流水線框产,保證CPU在操作時(shí)凄杯,這些流水線都屬于正在執(zhí)行原子操作的CPU;如果我們按CPU一個(gè)時(shí)鐘周期執(zhí)行一條指指令(約為0.6ns)秉宿,一個(gè)最好情況下的CAS操作也需要40個(gè)時(shí)鐘周期戒突;
內(nèi)存屏障
這個(gè)比較好理解,一般有Load Barrier 和 Store Barrier即讀屏障和寫屏障描睦,如JAVA中的volatile關(guān)鍵字
寄存器在執(zhí)行前膊存,為了提高性能,會(huì)對(duì)指令重排忱叭,而內(nèi)存屏障會(huì)禁止指令重排隔崎;
強(qiáng)制將CPU高速存中的數(shù)據(jù)寫回內(nèi)存,讓緩存中相關(guān)的數(shù)據(jù)失效窑多,這又涉及到內(nèi)存總線 CPU與內(nèi)存的讀寫等等的性能損耗仍稀;
JVM對(duì)內(nèi)存屏障的詳細(xì)內(nèi)容可以參考一下<>,作者是阿里大神方騰飛埂息,也是?并發(fā)編程網(wǎng)?博主技潘,總之很牛逼。
I/O操作
如高速緩存未命中(CPU之間的I/O)千康,如果涉及到網(wǎng)絡(luò)享幽,大容量存儲(chǔ)(磁盤),這類操作對(duì)于性能的影響更是遠(yuǎn)遠(yuǎn)大于上面提到幾種的開(kāi)銷;
所以并行編程變得復(fù)雜拾弃,除了與硬件的交互值桩、還有任務(wù)分割、并行訪問(wèn)控制等豪椿;這些如線程奔坟、鎖、屏障等搭盾,我們?cè)趯?shí)際工作中應(yīng)盡量避免直接控制它們咳秉,因?yàn)樗鼈兌加泄制猓幪幨窍菥煊纾粦c辛隨著為并發(fā)設(shè)計(jì)編程語(yǔ)言的興起(Elang澜建,Scala,Golang等),在釋放多核威力的同時(shí)炕舵,也一定程度降低了并發(fā)程序的難度何之。特別Golang也是Docker這類明星產(chǎn)品的實(shí)現(xiàn)語(yǔ)言。
Golang并發(fā)模式(CSP)并發(fā)內(nèi)核
(注咽筋,該圖引自網(wǎng)絡(luò))
其中M是一個(gè)內(nèi)核線程溶推,P是調(diào)度器,G是一個(gè)協(xié)程晤硕,灰色的G為掛起的協(xié)程悼潭;Go通過(guò)協(xié)程goroutine提供語(yǔ)言層面的調(diào)度器庇忌,實(shí)現(xiàn)高效的M:N(M個(gè)用戶線程對(duì)應(yīng)N個(gè)os內(nèi)核線程)對(duì)應(yīng)關(guān)系舞箍,使用goroutine做為并發(fā)實(shí)體,非常的輕量級(jí)皆疹,理論可以很輕松的創(chuàng)建上十萬(wàn)個(gè)goroutine疏橄。
P作為調(diào)度器,作用類似于CPU的核略就,每個(gè)工作線程都必須綁定一個(gè)有效的P才被允許執(zhí)行捎迫,否則只能休眠等到有空閑的P時(shí)被喚醒;P還要為線程提供執(zhí)行資源表牢,如為對(duì)象分配內(nèi)存 本地任務(wù)隊(duì)更等窄绒;
而實(shí)際的執(zhí)行體是M(OS內(nèi)核線程),和P綁定 不停的獲取執(zhí)行G的并發(fā)任務(wù)崔兴。M通過(guò)修改寄存器彰导,將執(zhí)行棧指向G自帶的棧內(nèi)存;
P/M是執(zhí)行的組合體敲茄,但是兩者數(shù)量并非是一一對(duì)應(yīng)位谋,M由調(diào)度器按需創(chuàng)建,比如當(dāng)M陷于一個(gè)IO操作長(zhǎng)時(shí)間阻塞堰燎,P就會(huì)被監(jiān)控線程搶回去去創(chuàng)建或喚醒一個(gè)M執(zhí)行其它任務(wù)掏父。
如果想深入了解,建議可以看看雨痕寫的 <<源碼解析>> 郁悶的點(diǎn)是有匯編和C的代碼...
從一段簡(jiǎn)單的代碼來(lái)看
主協(xié)程調(diào)用producer秆剪,創(chuàng)建了個(gè)生產(chǎn)者協(xié)程赊淑,并返回了一個(gè)通道,這里也可以理解為一個(gè)服務(wù)仅讽。
主協(xié)程的do_something執(zhí)行業(yè)務(wù)邏輯陶缺。
JAVA的同學(xué)從簡(jiǎn)單理解就當(dāng)chan就是JAVA中的隊(duì)列(用通訊的方式共享內(nèi)存)
先做拋磚引玉,后續(xù)再專門針對(duì)Go 和Actor做一些分享何什,交流组哩。