c++内存管理-内存顺序
轉(zhuǎn)載自:https://www.dazhuanlan.com/2019/12/14/5df40323d6adf/
原子操作(atomic)是無(wú)鎖編程(Lock-Free Programming)的基礎(chǔ)。C++內(nèi)存模型規(guī)定了多個(gè)線程訪問(wèn)同一個(gè)內(nèi)存地址時(shí)的語(yǔ)義,以及某個(gè)線程對(duì)內(nèi)存地址的更新何時(shí)能被其它線程看見(jiàn)。
該模型的核心思想就是由程序員用同步原語(yǔ)(例如,鎖或者C++11中新引入的atomic類型的共享變量)來(lái)保證你程序是沒(méi)有數(shù)據(jù)競(jìng)爭(zhēng)的,這樣CPU和編譯器就會(huì)保證程序是按程序員所想的那樣執(zhí)行的(即順序一致性)。
C++編譯器可能會(huì)進(jìn)行一些錯(cuò)誤優(yōu)化從而導(dǎo)致數(shù)據(jù)競(jìng)爭(zhēng)。第二,絕大多數(shù)情況下線程庫(kù)能正確的完成任務(wù),而在極少數(shù)對(duì)性能有更高要求的情況下(尤其是需要利用底層的硬件特性來(lái)實(shí)現(xiàn)高性能Lock Free算法時(shí))需要更精確的內(nèi)存模型以規(guī)定好程序的行為
C++11中的內(nèi)存屏障
松散”內(nèi)存屏障
std::memory_order_relaxed,松散操作,沒(méi)有同步或順序制約,僅對(duì)此操作要求原子性。帶標(biāo)簽?memory_order_relaxed的原子操作無(wú)同步操作;它們不會(huì)在并發(fā)的內(nèi)存訪問(wèn)間強(qiáng)加順序。它們只保證原子性和修改順序一致性。
例如,對(duì)于最初為零的x和y:
| 1 2 3 4 5 6 7 | r1 = y.load(memory_order_relaxed); // A x.store(r1, memory_order_relaxed); // B // 線程 2 : r2 = x.load(memory_order_relaxed); // C y.store(42, memory_order_relaxed); // D |
允許產(chǎn)生結(jié)果r1 == 42 && r2 == 42,因?yàn)榧词咕€程1中A先序于B且線程2中C先序于D ,卻沒(méi)有制約避免y的修改順序中D先出現(xiàn)于A ,而x的修改順序中B先出現(xiàn)于C。D在y上的副效應(yīng),可能可見(jiàn)于線程1中的加載A ,同時(shí)B在x上的副效應(yīng),可能可見(jiàn)于線程2中的加載C。
如上圖所示,假設(shè):a, b, c分別為普通變量,而x為atomic類型的變量,當(dāng)在寫入x時(shí),設(shè)置memory_order_relaxed時(shí),寫入a,寫入b的順序,在另外的線程上看到的,完全可能是相互調(diào)換的,寫入c的位置,完全也有可能由出現(xiàn)在寫入x之前,而在另外一個(gè)線程上看到的,確實(shí)在寫入x之后,同時(shí),讀出b的動(dòng)作,完全有可能從寫入x之后,編程在另外一個(gè)線程上看,是在寫入x之前。也就是,各種亂序, 都是被允許的。
松散內(nèi)存順序的典型使用是計(jì)數(shù)器自增,只要求原子性,但不要求順序或同步
單向的”釋放”內(nèi)存屏障
std::memory_order_release,釋放操作,當(dāng)前線程中的讀或?qū)懖荒鼙恢嘏诺酱舜鎯?chǔ)后。當(dāng)前線程的所有寫入,可見(jiàn)于加載該同一原子變量的其他線程。
如上圖所示,在使用memory_order_release設(shè)置的原子操作寫入的情況下,原子操作之后的讀寫操作,在另外的線程(加載了該原子變量的線程)看來(lái),可以重排到原子操作之前(通俗點(diǎn)說(shuō),就是另外線程可能認(rèn)為當(dāng)前線程是先執(zhí)行的寫入的a操作,再執(zhí)行的寫入x操作)。但是,如下圖所示,原子操作之前的讀、寫操作,計(jì)算機(jī)系統(tǒng)必須保證,在另外一個(gè)線程看來(lái),他是在原子操作寫入之前就被寫入了, 不能是在原子操作之后才寫入(通俗點(diǎn)說(shuō),也就是另外一個(gè)線程,不能認(rèn)為當(dāng)前線程先寫入的x,再寫入的c,他一定要看到的是先寫入c,再寫入的x):
單向的”加載”內(nèi)存屏障
std::memory_order_acquire,加載操作,當(dāng)前線程中讀或?qū)懖荒鼙恢嘏诺酱思虞d前。其他釋放同一原子變量的線程的所有寫入,為當(dāng)前線程所可見(jiàn)。
如上圖所示,在設(shè)置memory_order_acquire的對(duì)x的讀操作前的讀、寫操作,在另外的線程看來(lái)(或者說(shuō)實(shí)際的運(yùn)行順序),可以被重排到對(duì)x的讀操作后,(通俗點(diǎn)說(shuō),就是其他線程可以是認(rèn)為當(dāng)前線程先讀取的x,再寫入的c)。然而如下圖所示,對(duì)x的讀操作后的讀、寫操作,在另外的線程看來(lái)不允許被重排到x的讀操作前(不允許其他線程看到當(dāng)前線程先寫入的a,再讀取的x)。
單向“加載”+單向“釋放”協(xié)議
往往memory_order_acquire和memory_order_release是配合著一起使用的:
所有在線程1上,在寫入x之前的寫入操作,都將在線程2上,在讀出x之后,被看到。使用單向“加載”+單向“釋放”協(xié)議的場(chǎng)景往往是:
如上圖所示,thread_1在release寫入x(值:A)之前,寫入了待發(fā)布的a,b的數(shù)據(jù),而thread_2,將在acquire讀出x且為A之后,將讀到thread_1發(fā)布的a,b的數(shù)據(jù)。同時(shí),我們可以注意到,在thread_2上,在acquire讀出x之前,如果對(duì)a進(jìn)行讀操作,我們是無(wú)法確認(rèn)讀到的a一定會(huì)thread_1在之前最后寫入的a,這里的順序是不會(huì)被保證的,重排是被允許的。同時(shí),在之后,讀取c,讀到的是否為thread_1最后寫入的c,也是不確定的,因?yàn)?#xff0c;在x寫入之后,thread_1上又出現(xiàn)了一次寫入,而如果在此之前,還有一次寫入, 這兩次寫入之間,是不存在限制,可能會(huì)被重排的。
thread_1上有了release_store,對(duì)于a,b的寫入就一定會(huì)在x的改變之前,在thread_2上,就不會(huì)出現(xiàn)類似讀出c,的不確定性。thread_2上有了acquire_load,右側(cè)的讀出a,就不會(huì)被重排讀到左側(cè),而左側(cè)讀出a的不確定性,也不存在。thread_1卡住的是:對(duì)于數(shù)據(jù)a,b的寫入不能排到x的寫入之后,thread_2卡住的,是對(duì)于數(shù)據(jù)a,b的讀取,不能排到讀取x之前,這樣,就保證了數(shù)據(jù)a,b,與”信號(hào)量”x,之間,在thread_1, thread_2上的同步關(guān)系。
雙向的”加載-釋放”內(nèi)存屏障
std::memory_order_acq_rel,加載-釋放操作,帶此內(nèi)存順序的讀-修改-寫操作既是獲得加載又是釋放操作。沒(méi)有操作能夠從此操作之后被重排到此操作之前,也沒(méi)有操作能夠從此操作之前被重排到此操作之后,當(dāng)然,具有這一限制的前提是,觀察讀寫順序的不同線程是使用的同一個(gè)原子變量并基于這一內(nèi)存順序。
最嚴(yán)的”順序一致”內(nèi)存屏障
std::memory_order_seq_cst,順序一致操作,此內(nèi)存順序的操作既是加載操作又是釋放操作,沒(méi)有操作能夠從此操作之后被重排到此操作之前,也沒(méi)有操作能夠從此操作之前被重排到此操作之后。帶標(biāo)簽memory_order_seq_cst的原子操作不僅以與釋放/加載順序相同的方式排序內(nèi)存(在一個(gè)線程中先發(fā)生于存儲(chǔ)的任何結(jié)果都變成做加載的線程中的可見(jiàn)),還對(duì)所有擁有此標(biāo)簽的內(nèi)存操作建立一個(gè)單獨(dú)全序。memory_order_seq_cst比memory_order_acq_rel更強(qiáng),memory_order_acq_rel的順序保障,是要基于同一個(gè)原子變量的,也就是說(shuō),在這個(gè)原子變量之前的讀寫,不能重排到這個(gè)原子變量之后,同時(shí)這個(gè)原子變量之后的讀寫,也不能重排到這個(gè)原子變量之前。但是,如果兩個(gè)線程基于memory_order_acq_rel使用了兩個(gè)不同的原子變量x1, x2,那在x1之前的讀寫,重排到x2之后,是完全可能的,在x1之后的讀寫,重排到x2之前,也是被允許的。然而,如果兩個(gè)原子變量x1,x2,是基于memory_order_seq_cst在操作,那么即使是x1之前的讀寫,也不能被重排到x2之后,x1之后的讀寫,也不能重排到x2之前,也就說(shuō),如果都用memory_order_seq_cst,那么程序代碼順序(Program Order)就將會(huì)是你在多個(gè)線程上都實(shí)際觀察到的順序(Observed Order)
?
?
?
?
?
總結(jié)
以上是生活随笔為你收集整理的c++内存管理-内存顺序的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: c++-内存管理-内存对齐方式
- 下一篇: c++-内存管理-array alloc