1. 程式人生 > >多線程(6)線程同步

多線程(6)線程同步

hand 引用 toa abstract initial button main cross oid

  使用多線程很容易,但是如果多個線程同時訪問一個共享資源時而不加以控制,就會導致數據損壞。所以多線程並發時,必須要考慮線程同步(或稱線程安全)的問題。

什麽是線程同步

多個線程同時訪問共享資源時,使多個線程順序(串行)訪問共享資源的機制。 註意: 1,共享資源,比如全局變量和靜態變量。 2,訪問,一般指寫操作,讀操作無需考慮線程同步。 3,串行,指當一個線程正在訪問共享資源時,其它線程等待,直到該線程釋放鎖。

線程同步帶來哪些問題

如果能保證多個線程不會同時訪問共享資源,那麽就不需要考慮線程同步。 雖然線程同步能保證多線程同時訪問共享數據時線程安全,但是同時也會帶來以下問題: 1,使用起來繁瑣,因為必須找出代碼中所有可能由多個線程同時訪問的共享數據,並且要用額外的代碼將這些代碼包圍起來,獲取和釋放一個線程同步鎖,而一旦有一處忘記用鎖包圍,共享數據就會被損壞。 2,損害性能,因為獲取和釋放一個鎖是需要時間的。 3,可能會造成更多的線程被創建,由於線程同步鎖一次只允許一個線程訪問共享資源,當線程池線程試圖獲取一個暫時無法獲取的鎖時,線程池就會創建一個新的線程。 所以,要從設計上盡可能地避免線程同步,實在不能避免的再考慮線程同步。

線程同步的常用解決方案

1,鎖

包括lock關鍵字和Monitor類型。 使用lock關鍵字實現: 技術分享

技術分享
 1 /// <summary>
 2 /// 線程同步計算器
 3 /// </summary>
 4 public class SyncCounter : CounterBase
 5 {
 6     /// <summary>
 7     /// 全局變量
 8     /// </summary>
 9     public int Result = 0;
10 
11     private static readonly object lockObj = new
object(); 12 13 public override void Increase() 14 { 15 lock (lockObj) 16 { 17 Result++; 18 } 19 } 20 21 public override void Decrease() 22 { 23 lock (lockObj) 24 { 25 Result--; 26 } 27 }
28 }
View Code 需要註意的是: 1,lock鎖定的對象必須是引用類型,不能是值類型。因為值類型傳入會發生裝箱,這樣每次lock的將是一個不同的對象,就沒有辦法實現多線程同步了。 2,避免使用public類型的對象,這樣很容易導致死鎖。因為其它代碼也有可能鎖定該對象。 3,避免鎖定字符串,因為字符串會被CLR暫留(也就是說兩個變量的字符串內容相同,.net會把暫留的字符串對象分配給變量),導致應用程序中鎖定的是同一個對象,造成死鎖。 使用Monitor實現: 技術分享 技術分享
 1 /// <summary>
 2 /// 線程同步計算器
 3 /// </summary>
 4 public class SyncCounter : CounterBase
 5 {
 6     /// <summary>
 7     /// 全局變量
 8     /// </summary>
 9     public int Result = 0;
10 
11     private static readonly object lockObj = new object();
12 
13     public override void Increase()
14     {
15         Monitor.Enter(lockObj);
16         try
17         {
18             Result++;
19         }
20         finally
21         {
22             Monitor.Exit(lockObj);
23         }
24     }
25 
26     public override void Decrease()
27     {
28         Monitor.Enter(lockObj);
29         try
30         {
31             Result--;
32         }
33         finally
34         {
35             Monitor.Exit(lockObj);
36         }
37     }
38 }
View Code

完整代碼:

技術分享
  1 namespace ConsoleApplication28
  2 {
  3     class Program
  4     {
  5         static void Main(string[] args)
  6         {
  7             //同時發起3個異步線程
  8             Console.WriteLine("普通(非線程同步)計算器測試...");
  9             var normalCounter = new NormalCounter();
 10             var tasks = new List<Task>();
 11             var task1 = Task.Factory.StartNew(() =>
 12             {
 13                 TestCounter(normalCounter);
 14             });
 15             tasks.Add(task1);
 16 
 17             var task2 = Task.Factory.StartNew(() =>
 18             {
 19                 TestCounter(normalCounter);
 20             });
 21             tasks.Add(task2);
 22 
 23             var task3 = Task.Factory.StartNew(() =>
 24             {
 25                 TestCounter(normalCounter);
 26             });
 27             tasks.Add(task3);
 28 
 29 
 30             Task.WaitAll(tasks.ToArray());
 31             Console.WriteLine("NormalCounter.Result:" + normalCounter.Result);
 32             Console.WriteLine("*******************************************");
 33 
 34             Console.WriteLine("線程同步計算器測試...");
 35             var syncCounter = new SyncCounter();
 36             var tasks1 = new List<Task>();
 37             task1 = Task.Factory.StartNew(() =>
 38             {
 39                 TestCounter(syncCounter);
 40             });
 41             tasks1.Add(task1);
 42 
 43             task2 = Task.Factory.StartNew(() =>
 44             {
 45                 TestCounter(syncCounter);
 46             });
 47             tasks1.Add(task2);
 48 
 49             task3 = Task.Factory.StartNew(() =>
 50             {
 51                 TestCounter(syncCounter);
 52             });
 53             tasks1.Add(task3);
 54 
 55             Task.WaitAll(tasks1.ToArray());
 56             Console.WriteLine("SyncCounter.Result:" + syncCounter.Result);
 57 
 58             Console.ReadKey();
 59         }
 60 
 61         /// <summary>
 62         /// 
 63         /// </summary>
 64         /// <param name="counter"></param>
 65         static void TestCounter(CounterBase counter)
 66         {
 67             //100000次加減
 68             for (int i = 0; i < 100000; i++)
 69             {
 70                 counter.Increase();
 71                 counter.Decrease();
 72             }
 73         }
 74     }
 75 
 76     /// <summary>
 77     /// 計算器基類
 78     /// </summary>
 79     public abstract class CounterBase
 80     {
 81         /// <summary>
 82         /// 83         /// </summary>
 84         public abstract void Increase();
 85 
 86         /// <summary>
 87         /// 88         /// </summary>
 89         public abstract void Decrease();
 90     }
 91 
 92     /// <summary>
 93     /// 普通計算器
 94     /// </summary>
 95     public class NormalCounter : CounterBase
 96     {
 97         /// <summary>
 98         /// 全局變量
 99         /// </summary>
100         public int Result = 0;
101 
102         public override void Increase()
103         {
104             Result++;
105         }
106 
107         public override void Decrease()
108         {
109             Result--;
110         }
111 
112     }
113 
114     /// <summary>
115     /// 線程同步計算器
116     /// </summary>
117     public class SyncCounter : CounterBase
118     {
119         /// <summary>
120         /// 全局變量
121         /// </summary>
122         public int Result = 0;
123 
124         private static readonly object lockObj = new object();
125 
126         public override void Increase()
127         {
128             lock (lockObj)
129             {
130                 Result++;
131             }
132         }
133 
134         public override void Decrease()
135         {
136             lock (lockObj)
137             {
138                 Result--;
139             }
140         }
141     }
142 }
View Code

lock關鍵字揭密:

通過查看lock關鍵字生成的IL代碼,如下圖: 技術分享

從上圖可以得出以下結論:

lock關鍵字內部就是使用Monitor類(或者說lock關鍵字是Monitor的語法糖),使用lock關鍵字比直接使用Monitor更好,原因有二。

1,lock語法更簡潔。

2,lock確保了即使代碼拋出異常,也可以釋放鎖,因為在finally中調用了Monitor.Exit方法。

2,信號同步

信號同步機制中涉及的類型都繼承自抽象類WaitHandle,這些類型有EventWaitHandle(類型化為AutoResetEvent、ManualResetEvent)和Semaphore以及Mutex。關系如下圖。 技術分享

下面是使用信號同步機制的一個簡單的例子,如下代碼:

技術分享

技術分享
 1 namespace WindowsFormsApplication1
 2 {
 3     public partial class Form1 : Form
 4     {
 5         //信號
 6         AutoResetEvent autoResetEvent = new AutoResetEvent(false);
 7 
 8         public Form1()
 9         {
10             InitializeComponent();
11 
12             CheckForIllegalCrossThreadCalls = false;
13         }
14 
15         /// <summary>
16         /// 開始
17         /// </summary>
18         /// <param name="sender"></param>
19         /// <param name="e"></param>
20         private void button1_Click(object sender, EventArgs e)
21         {
22             Task.Factory.StartNew(() => 
23             {
24                 this.richTextBox1.Text+="線程啟動..." + Environment.NewLine;
25                 this.richTextBox1.Text += "開始處理一些實際的工作" + Environment.NewLine;
26                 Thread.Sleep(3000);
27 
28                 this.richTextBox1.Text += "我開始等待別的線程給我信號,才願意繼續下去" + Environment.NewLine;
29                 autoResetEvent.WaitOne();
30                 this.richTextBox1.Text += "我繼續做一些工作,然後結束了!";
31             });
32         }
33 
34         /// <summary>
35         /// 信號同步
36         /// </summary>
37         /// <param name="sender"></param>
38         /// <param name="e"></param>
39         private void button2_Click(object sender, EventArgs e)
40         {
41             //給在autoResetEvent上等待的線程一個信號
42             autoResetEvent.Set();
43         }
44     }
45 }
View Code

運行效果:

1,線程阻塞,等待信號。

技術分享

2,主線程發送信號,讓線程繼續執行。

技術分享

3,線程安全的集合類

我們也可以通過使用.net提供的線程安全的集合類來保證線程安全。在命名空間:System.Collections.Concurrent下。 主要包括:
  • ConcurrentQueue 線程安全版本的Queue【常用】
  • ConcurrentStack線程安全版本的Stack
  • ConcurrentBag線程安全的對象集合
  • ConcurrentDictionary線程安全的Dictionary【常用】

多線程(6)線程同步