關(guān)于C#執(zhí)行順序帶來的一些潛在問題
前言
編寫程序的時(shí)候,人們的直觀感覺通常認(rèn)為,程序的執(zhí)行順序是按照語(yǔ)句的順序進(jìn)行的。然而,許多編程語(yǔ)言的規(guī)范是允許實(shí)際執(zhí)行順序與語(yǔ)句編寫順序不符的。實(shí)際上,編譯器為了完成某種優(yōu)化,常常會(huì)對(duì)一些操作進(jìn)行適當(dāng)?shù)捻樞蛘{(diào)整,導(dǎo)致一些預(yù)料之外的現(xiàn)象。
實(shí)驗(yàn)現(xiàn)象
首先,通過一個(gè)例子來展示這個(gè)現(xiàn)象。在一個(gè)C# .NET Core 3.1命令行程序中,定義兩個(gè)全局變量a和b,在線程1中,依次對(duì)b和a進(jìn)行遞增。這樣,在任何時(shí)刻b應(yīng)當(dāng)?shù)扔赼或a+1。
static int a = 0; static int b = 0; static void Thread1() { while (true) { ++b; ++a; } }
在線程2中,先讀取a的值,然后執(zhí)行一些其他操作,再讀取b的值。如果語(yǔ)句一定是按順序執(zhí)行的,那么讀取到的b的值應(yīng)當(dāng)比讀取到的a的值更新,從而b必然大于或等于a(除非b發(fā)生了溢出)。編寫程序,當(dāng)b < a時(shí)輸出它們的值。
static int c = 0; static void Thread2() { while (true) { c += b; var localA = a; c += b; var localB = b; if (localA > localB) { Console.WriteLine($"a={localA} b={localB}"); } } }
再編寫主程序,啟動(dòng)上述的兩個(gè)線程。
static void Main(string[] args) { Task.Run(Thread1); Task.Run(Thread2); Console.ReadKey(); }
使用Debug配置,編譯并運(yùn)行該程序,命令行是沒有輸出的,符合我們的預(yù)期。但是使用Release配置的話,就會(huì)出現(xiàn)大量輸出,其中a的值比b大1到5不等。
查看反匯編可以看到,在第1個(gè)c += b語(yǔ)句處,程序?qū)的值放到了寄存器中,而后面的語(yǔ)句均使用了該寄存器內(nèi)存放的值。所以,編譯器實(shí)際上將對(duì)b的讀取操作合并并且前置了。以下為反匯編結(jié)果片段。
00007FFB628A394D mov rcx,7FFB6292FBD0h 00007FFB628A3957 mov edx,1 00007FFB628A395C call 00007FFBC2387B10 00007FFB628A3961 mov esi,dword ptr [7FFB6292FC08h] 00007FFB628A3967 mov ecx,esi 00007FFB628A3969 add ecx,dword ptr [7FFB6292FC0Ch] 00007FFB628A396F mov dword ptr [7FFB6292FC0Ch],ecx var localA = a; 00007FFB628A3975 mov edi,dword ptr [7FFB6292FC04h] c += b; 00007FFB628A397B add ecx,esi c += b; 00007FFB628A397D mov dword ptr [7FFB6292FC0Ch],ecx if (localA > localB) 00007FFB628A3983 cmp edi,esi 00007FFB628A3985 jle 00007FFB628A394D
理論分析
在C#語(yǔ)言標(biāo)準(zhǔn)的Basic concepts一章Execution order一節(jié)(參見:Basic concepts – C# language specification)中,提到了C#的執(zhí)行順序規(guī)范。C#程序的副作用在以下關(guān)鍵點(diǎn)處的順序是被保留的:
- 對(duì)volatile字段的讀寫
- lock語(yǔ)句
- 線程的創(chuàng)建和終結(jié)
C#程序的執(zhí)行順序在滿足以下條件的情況下,可以由執(zhí)行環(huán)境任意調(diào)整的:
- 在同一線程內(nèi),數(shù)據(jù)的的依賴關(guān)系是被保留的。即,結(jié)果與語(yǔ)句按照順序執(zhí)行的情況一致。
- 初始化順序的規(guī)則是被保留的。
- 相對(duì)于volatile字段的讀寫,副作用的順序是被保留的。
而上述的副作用包括:
- 讀取或?qū)懭雟olatile字段
- 寫入非volatile變量
- 寫入外部資源
- 拋出異常
由此可以推出,C#程序中對(duì)非volatile變量的讀取順序可能會(huì)被調(diào)整。在只有一個(gè)線程對(duì)該變量進(jìn)行操作時(shí),這個(gè)順序的調(diào)整是保證不會(huì)影響結(jié)果的;但如果同時(shí)有其他的線程正在對(duì)變量進(jìn)行修改,則讀取的順序是無法確定的。
因此,如果有多個(gè)線程同時(shí)訪問的,對(duì)值的實(shí)時(shí)性有要求的變量,應(yīng)當(dāng)設(shè)置為volatile變量。將上述實(shí)驗(yàn)中的靜態(tài)變量a和b改為volatile變量后,即使是Release配置下,也不會(huì)出現(xiàn)命令行的輸出,即兩個(gè)變量的讀取順序符合原始的語(yǔ)句順序。
結(jié)論
在C#程序中,讀取非volatile變量的順序可能被執(zhí)行環(huán)境任意調(diào)整。如果某個(gè)變量在被讀取的時(shí)候會(huì)被其他線程寫入,為了該讀取結(jié)果的實(shí)時(shí)性,應(yīng)當(dāng)將該變量設(shè)置為volatile變量。
總結(jié)
到此這篇關(guān)于關(guān)于C#執(zhí)行順序帶來的一些潛在問題就介紹到這了,更多相關(guān)C#執(zhí)行順序潛在問題內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
C#導(dǎo)航器Xpath與XPathNavigator類
這篇文章介紹了C#導(dǎo)航器Xpath與XPathNavigator類,文中通過示例代碼介紹的非常詳細(xì)。對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-06-06淺析C#中數(shù)組,ArrayList與List對(duì)象的區(qū)別
在C#中,當(dāng)我們想要存儲(chǔ)一組對(duì)象的時(shí)候,就會(huì)想到用數(shù)組,ArrayList,List這三個(gè)對(duì)象了。那么這三者到底有什么樣的區(qū)別呢2013-07-07C#?CefSharp?根據(jù)輸入日期段自動(dòng)選擇日期的操作代碼
這篇文章主要介紹了C#?CefSharp?根據(jù)輸入日期段自動(dòng)選擇日期的操作代碼,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友參考下吧2024-01-01