您好,登錄后才能下訂單哦!
這篇文章主要介紹“C#多線程安全怎么理解”,在日常操作中,相信很多人在C#多線程安全怎么理解問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”C#多線程安全怎么理解”的疑惑有所幫助!接下來,請跟著小編一起來學習吧!
一段程序,單線程和多線程執行結果不一致,就表示存在多線程安全問題,即多線程不安全。
假如我們有一個需求,需要輸出5個線程,且線程序號按0-4命名,我們編寫代碼如下:
private void btnTask1_Click(object sender, EventArgs e) { Console.WriteLine("【開始】**************線程不安全示例btnTask1_Click**************"); for (int i = 0; i < 5; i++) { Task.Run(() => { Console.WriteLine($"【BEGIN】**************這是第 {i} 個線程,線程ID={Thread.CurrentThread.ManagedThreadId}**************"); Thread.Sleep(2000); Console.WriteLine($"【 END 】**************這是第 {i} 個線程,線程ID={Thread.CurrentThread.ManagedThreadId}**************"); }); } Console.WriteLine("【結束】**************線程不安全示例btnTask1_Click**************"); }
然后運行示例,如下所示:
通過對以上示例進行分析,得出結論如下:
1.在for循環中,啟動的5個線程,線程序號都是5,并沒有按照我們預期的結果【0,1,2,3,4】進行輸出。
2.經過分析發現,因為for循環中,i是同一個變量,線程啟動是異步進行的,存在延遲,當線程啟動時,for循環已經結束,i的值為5,所以才導致線程序號和預期不一致。
為了解決上述問題,可以通過引入局部變量來解決,即每次循環聲明一個變量,循環5次,存在5個變量,則相互之間不會覆蓋。如下所示:
private void btnTask1_Click(object sender, EventArgs e) { Console.WriteLine("【開始】**************線程不安全示例btnTask1_Click**************"); for (int i = 0; i < 5; i++) { int k = i; Task.Run(() => { Console.WriteLine($"【BEGIN】**************這是第 {k} 個線程,線程ID={Thread.CurrentThread.ManagedThreadId}**************"); Thread.Sleep(2000); Console.WriteLine($"【 END 】**************這是第 {k} 個線程,線程ID={Thread.CurrentThread.ManagedThreadId}**************"); }); } Console.WriteLine("【結束】**************線程不安全示例btnTask1_Click**************"); }
運行優化后的示例,如下所示:
通過運行示例發現,局部變量可以解決相應的問題。
假如我們有一個需求:將0到200增加到一個列表中,采用多線程來實現,如下所示:
private void btnTask2_Click(object sender, EventArgs e) { Console.WriteLine("【開始】**************線程不安全示例btnTask1_Click**************"); List<int> list = new List<int>(); List<Task> tasks = new List<Task>(); for (int i = 0; i < 200; i++) { tasks.Add( Task.Run(() => { list.Add(i); })); } Task.WaitAll(tasks.ToArray()); string res = string.Join(",", list); Console.WriteLine($"列表長度: {list.Count} ,列表內容:{res}"); Console.WriteLine("【結束】**************線程不安全示例btnTask1_Click**************"); }
通過運行示例,如下所示:
通過對以上示例進行分析,得出結論如下:
1.列表的記錄條數不對,會少。
2.列表的元素內容與預期的內容不一致。
針對上述問題,采用中間局部變量的方式,可以解決嗎?不妨一試,修改后的 代碼如下:
private void btnTask2_Click(object sender, EventArgs e) { Console.WriteLine("【開始】**************線程不安全示例btnTask1_Click**************"); List<int> list = new List<int>(); List<Task> tasks = new List<Task>(); for (int i = 0; i < 200; i++) { int k = i; tasks.Add( Task.Run(() => { list.Add(k); })); } Task.WaitAll(tasks.ToArray()); string res = string.Join(",", list); Console.WriteLine($"列表長度: {list.Count} ,列表內容:{res}"); Console.WriteLine("【結束】**************線程不安全示例btnTask1_Click**************"); }
運行優化示例,如下所示:
通過運行上述示例,得出結論如下:
1.列表長度依然不對,會小于實際單一線程的長度。注意:多線程列表長度不是一定會小于單一線程運行時列表長度,只是存在概率,即多個線程存在同時寫入一個位置的概率。
2.列表內容,采用局部變量,可以解決部分問題。
由此可以得出List不是線程安全的數據類型。
針對多線程的不安全問題,可以通過加鎖進行解決,加鎖的目的:在任意時刻,加鎖塊都之允許一個線程訪問。
lock實際是一個語法糖,實際效果等同于Monitor。鎖定的是引用對象的一個內存地址引用。所以鎖定對象不可以是值類型,也不可以是null,只能是引用類型。
lock對象的標準寫法:默認情況下,鎖對象是私有,靜態,只讀,引用對象。如下所示:
/// <summary> /// 定義一個鎖對象 /// </summary> private static readonly object obj = new object();
然后優化程序,如下所示:
private void btnTask2_Click(object sender, EventArgs e) { Console.WriteLine("【開始】**************線程不安全示例btnTask1_Click**************"); List<int> list = new List<int>(); List<Task> tasks = new List<Task>(); for (int i = 0; i < 200; i++) { int k = i; tasks.Add( Task.Run(() => { lock (obj) { list.Add(k); } })); } Task.WaitAll(tasks.ToArray()); string res = string.Join(",", list); Console.WriteLine($"列表長度: {list.Count} ,列表內容:{res}"); Console.WriteLine("【結束】**************線程不安全示例btnTask1_Click**************"); }
運行優化后的示例,如下所示:
通過對上述示例進行分析,得出結論如下:
1.加鎖后,列表在多線程下也變成安全,符合預期的要求。
2.但是由于加鎖的原因,同一時刻,只能由一個線程進入,其他線程就會等待,所以多線程也變成了單線程。
標準寫法,鎖對象是私有類型,目的是為了避免鎖對象被其他線程使用,如果被使用,則會相互阻塞,如下所示:
假如,現在有一個鎖對象,在TestLock中使用,如下所示:
public class TestLock { public static readonly object Obj = new object(); public void Show() { Console.WriteLine("【開始】**************線程示例Show**************"); for (int i = 0; i < 5; i++) { int k = i; Task.Run(() => { lock (Obj) { Console.WriteLine($"【BEGIN】*********T*****這是第 {k} 個線程,線程ID={Thread.CurrentThread.ManagedThreadId}**************"); Thread.Sleep(2000); Console.WriteLine($"【 END 】*********T*****這是第 {k} 個線程,線程ID={Thread.CurrentThread.ManagedThreadId}**************"); } }); } Console.WriteLine("【結束】**************線程示例Show**************"); } }
同時在FrmMain中使用,如下所示:
private void btnTask3_Click(object sender, EventArgs e) { Console.WriteLine("【開始】**************線程示例btnTask3_Click**************"); //類對象中多線程 TestLock.Show(); //主方法中多線程 for (int i = 0; i < 5; i++) { int k = i; Task.Run(() => { lock (TestLock.Obj) { Console.WriteLine($"【BEGIN】*********M*****這是第 {k} 個線程,線程ID={Thread.CurrentThread.ManagedThreadId}**************"); Thread.Sleep(2000); Console.WriteLine($"【 END 】*********M*****這是第 {k} 個線程,線程ID={Thread.CurrentThread.ManagedThreadId}**************"); } }); } Console.WriteLine("【結束】**************線程示例btnTask3_Click**************"); }
運行上述示例,如下所示:
通過上述示例,得出結論如下:
1.T和M是成對相鄰,且各代碼塊交互出現。
2.多個代碼塊,共用一把鎖,是會相互阻塞的。這也是為啥不建議使用public修飾符的原因,避免被不恰當的加鎖。
如果使用不同的鎖對象,多個代碼塊之間是可以并發的【T和M是不成對,且不相鄰出現,但是有同一代碼塊的內部順序】,效果如下:
假如對象不是static類型,那么鎖對象就是對象屬性,不同的對象之間是相互獨立的,所以不同通對象調用相同的方法,就會存在并發的問題,如下所示:
修改TestLock代碼【去掉static】,如下所示:
public class TestLock { public readonly object Obj = new object(); public void Show(string name) { Console.WriteLine("【開始】**************線程示例Show--{0}**************",name); for (int i = 0; i < 5; i++) { int k = i; Task.Run(() => { lock (Obj) { Console.WriteLine($"【BEGIN】*********T*****這是第 {k}--{name} 個線程,線程ID={Thread.CurrentThread.ManagedThreadId}**************"); Thread.Sleep(2000); Console.WriteLine($"【 END 】*********T*****這是第 {k}--{name} 個線程,線程ID={Thread.CurrentThread.ManagedThreadId}**************"); } }); } Console.WriteLine("【結束】**************線程示例Show--{0}**************",name); } }
聲明兩個對象,分別調用Show方法,如下所示:
private void btnTask4_Click(object sender, EventArgs e) { Console.WriteLine("【開始】**************線程示例btnTask3_Click**************"); TestLock testLock1 = new TestLock(); testLock1.Show("first"); TestLock testLock2 = new TestLock(); testLock2.Show("second"); Console.WriteLine("【結束】**************線程示例btnTask3_Click**************"); }
測試示例,如下所示:
通過以上示例,得出結論如下:
非靜態鎖對象,只在當前對象內部進行允許同一時刻只有一個線程進入,但是多個對象之間,是相互并發,相互獨立的。所以建議鎖對象為static對象。
在lock模式下,鎖定的是內存引用地址,而不是鎖定的對象的值。假如將Form的鎖對象的類型改為字符串,如下所示:
/// <summary> /// 定義一個鎖對象 /// </summary> private static readonly string obj = "花無缺";
同時TestLock類的鎖對象也改為字符串,如下所示:
public class TestLock { private static readonly string obj = "花無缺"; public static void Show(string name) { Console.WriteLine("【開始】**************線程示例Show--{0}**************",name); for (int i = 0; i < 5; i++) { int k = i; Task.Run(() => { lock (obj) { Console.WriteLine($"【BEGIN】*********T*****這是第 {k}--{name} 個線程,線程ID={Thread.CurrentThread.ManagedThreadId}**************"); Thread.Sleep(2000); Console.WriteLine($"【 END 】*********T*****這是第 {k}--{name} 個線程,線程ID={Thread.CurrentThread.ManagedThreadId}**************"); } }); } Console.WriteLine("【結束】**************線程示例Show--{0}**************",name); } }
運行上述示例,結果如下:
通過上述示例,得出結論如下:
1.字符串是一種特殊的鎖類型,如果字符串的值一致,則認為是同一個鎖對象,不同對象之間會進行阻塞。因為string類型是享元的,在內存堆里面只有一個花無缺。
2.如果是其他類型,則是不同的鎖對象,是可以相互并發的。
3.說明鎖定的是內存引用地址,而非鎖定對象的值。
如果TestLock為泛型類,如下所示:
1 public class TestLock<T> 2 { 3 private static readonly object obj = new object(); 4 5 public static void Show(string name) 6 { 7 8 Console.WriteLine("【開始】**************線程示例Show--{0}**************",name); 9 10 for (int i = 0; i < 5; i++) 11 { 12 int k = i; 13 Task.Run(() => 14 { 15 lock (obj) 16 { 17 Console.WriteLine($"【BEGIN】*********T*****這是第 {k}--{name} 個線程,線程ID={Thread.CurrentThread.ManagedThreadId}**************"); 18 Thread.Sleep(2000); 19 Console.WriteLine($"【 END 】*********T*****這是第 {k}--{name} 個線程,線程ID={Thread.CurrentThread.ManagedThreadId}**************"); 20 } 21 }); 22 } 23 24 Console.WriteLine("【結束】**************線程示例Show--{0}**************",name); 25 } 26 }
那么在調用時,會相互阻塞嗎?調用代碼如下:
private void btnTask5_Click(object sender, EventArgs e) { Console.WriteLine("【開始】**************線程示例btnTask5_Click**************"); TestLock<int>.Show("AA"); TestLock<string>.Show("BB"); Console.WriteLine("【結束】**************線程示例btnTask5_Click**************"); }
運行上述示例,如下所示:
通過分析上述示例,得出結論如下所示:
1.對于泛型類,不同類型參數之間是可以相互并發的,因為泛型類針對不同類型參數會編譯成不同的類,那對應的鎖對象,會變成不同的引用類型。
2.如果鎖對象為字符串類型,則也是會相互阻塞的,只是因為字符串是享元模式。
3.泛型T的不同,會編譯成不同的副本。
如果在遞歸函數中進行加鎖,會造成死鎖嗎?示例代碼如下:
private void btnTask6_Click(object sender, EventArgs e) { Console.WriteLine("【開始】**************線程示例btnTask6_Click**************"); this.add(1); Console.WriteLine("【結束】**************線程示例btnTask6_Click**************"); } private int num = 0; private void add(int index) { this.num++; Task.Run(()=> { lock (obj) { Console.WriteLine($"【BEGIN】**************這是第 {num} 個線程,線程ID={Thread.CurrentThread.ManagedThreadId}**************"); Thread.Sleep(2000); Console.WriteLine($"【 END 】**************這是第 {num} 個線程,線程ID={Thread.CurrentThread.ManagedThreadId}**************"); if (num < 5) { this.add(index); } } }); }
運行上述示例,如下所示:
通過運行上述示例,得出結論如下:
在遞歸函數中進行加鎖,會進行阻塞等待,但是不會造成死鎖。
到此,關于“C#多線程安全怎么理解”的學習就結束了,希望能夠解決大家的疑惑。理論與實踐的搭配能更好的幫助大家學習,快去試試吧!若想繼續學習更多相關知識,請繼續關注億速云網站,小編會繼續努力為大家帶來更多實用的文章!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。