在 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 給定相同的一個數值,在讀取的時候判斷是否讀取到的每一個屬性是否都是相同的數值,如果存在不同的,那么證明給結構體賦值是線程不安全的
運行以上代碼,可以看到,在結構體中,存在屬性的數值是不相同的。通過以上代碼可以看到,放在類對象的字段的結構體,進行賦值是線程不安全的
可以通過如下方式獲取本文的源代碼,先創建一個空文件夾,接着使用命令行 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 文件夾