dotnet C# 給結構體欄位賦值非執行緒安全

lindexi發表於2021-09-06

在 dotnet 執行時中,給引用物件進行賦值替換的時候,是執行緒安全的。給結構體物件賦值,如果此結構體是某個類的成員欄位,那麼此賦值不一定是執行緒安全的。是否執行緒安全,取決於結構體的大小,取決於此結構體能否在一次原子賦值內完成

大家都知道,某個執行邏輯如果是原子邏輯,那麼此邏輯是執行緒安全的。原子邏輯就是一個非 A 即 B 的狀態的變更,絕對不會存在處於 A 和 B 中間的狀態。滿足於此即可稱為執行緒安全,因為執行緒不會讀取到中間狀態。在 dotnet 執行時裡面,特別對了引用物件,也就是類物件的賦值過程進行了優化,可以讓物件的賦值是原子的

從執行時的邏輯上,可以瞭解到的是引用物件的賦值本質上就是將新物件的引用地址賦值,物件引用地址可以認為是指標。在單次 CPU 運算中可以一次性完成,不會存在只寫入某幾位而還有某幾位沒有寫入的情況

大概可以認為在 x86 上,單次的原子賦值長度就是 32 位。這也就是為什麼 dotnet 裡面的物件地址設計為 32 位的原因

但是對於結構體來說,需要分為兩個情況,定義在棧上的結構體,如某個方法的區域性變數或引數是結構體,此時的結構體是存放在棧上的,而在 dotnet 裡面,每個執行緒都有自己獨立的棧,因此放在棧上的結構體線上程上是獨立的,相互之間沒有影響,也就是執行緒安全的

如果是放在堆上面的結構體,如作為某個類物件的欄位,此時的結構體將會佔用此類物件的記憶體空間,如對以下程式碼的記憶體示意圖

    class Foo
    {
        public int X; // 沒有任何專案或理由可以公開欄位,本文這裡不規範的寫法僅僅只是為了做演示而已 (Unity除外)
        public FooStruct FooStruct;
        public int Y;
    }

    struct FooStruct
    {
        public int A { set; get; }
        public int B { set; get; }
        public int C { set; get; }
        public int D { set; get; }
    }

此時的 Foo 物件在記憶體上的佈局示意圖大概如下

如上面示意圖,在記憶體佈局上,將會在類記憶體佈局上將結構體展開,佔用類的一段記憶體空間。也就是說本質上結構體如命名,就是多個基礎型別的組合,實際上是執行的概念。也就是說在給類物件的欄位是結構體進行賦值的時候,每次賦值的內容僅僅是取決於原子長度,如 x86 下使用 32 位進行賦值,相當於先給 FooStruct 的 A 進行賦值,再給 FooStruct 的 B 進行賦值等等。此時如果有某個執行緒在進行賦值,某個執行緒在進行讀取 Foo 物件的 FooStruct 欄位,那麼也許讀取的執行緒會讀取到正在賦值到一半的 FooStruct 結構體

如以下的測試程式碼

    class Program
    {
        static void Main(string[] args)
        {
            var taskList = new List<Task>();

            for (int i = 0; i < 100; i++)
            {
                var n = i;
                taskList.Add(Task.Run(() =>
                {
                    while (Foo != null)
                    {
                        var fooStruct = new FooStruct()
                        {
                            A = n,
                            B = n,
                            C = n,
                            D = n
                        };

                        Foo.FooStruct = fooStruct;

                        fooStruct = Foo.FooStruct;
                        var value = fooStruct.A;
                        if (fooStruct.B != value)
                        {
                            throw new Exception();
                        }

                        if (fooStruct.C != value)
                        {
                            throw new Exception();
                        }

                        if (fooStruct.D != value)
                        {
                            throw new Exception();
                        }
                    }
                }));
            }

            Task.WaitAll(taskList.ToArray());
        }

        private static Foo Foo { get; } = new Foo();
    }

以上程式碼開啟了很多執行緒,每個執行緒都在嘗試讀寫此結構體。每次寫入的賦值都是在 A B C D 給定相同的一個數值,在讀取的時候判斷是否讀取到的每一個屬性是否都是相同的數值,如果存在不同的,那麼證明給結構體賦值是執行緒不安全的

執行以上程式碼,可以看到,在結構體中,存在屬性的數值是不相同的。通過以上程式碼可以看到,放在類物件的欄位的結構體,進行賦值是執行緒不安全的

本文所有程式碼放在githubgitee 歡迎訪問

可以通過如下方式獲取本文的原始碼,先建立一個空資料夾,接著使用命令列 cd 命令進入此空資料夾,在命令列裡面輸入以下程式碼,即可獲取到本文的程式碼

git init
git remote add origin https://gitee.com/lindexi/lindexi_gd.git
git pull origin 01a988dd6efdd0550ce0302ecbb93755f1720e85

以上使用的是 gitee 的源,如果 gitee 不能訪問,請替換為 github 的源

git remote remove origin
git remote add origin https://github.com/lindexi/lindexi_gd.git

獲取程式碼之後,進入 YanibeyeNelahallfaihair 資料夾

相關文章