1. 程式人生 > 程式設計 >詳解c# 執行緒同步

詳解c# 執行緒同步

一、執行緒同步概述

前面的文章都是講建立多執行緒來實現讓我們能夠更好的響應應用程式,然而當我們建立了多個執行緒時,就存在多個執行緒同時訪問一個共享的資源的情況,在這種情況下,就需要我們用到執行緒同步,執行緒同步可以防止資料(共享資源)的損壞。

然而我們在設計應用程式還是要儘量避免使用執行緒同步, 因為執行緒同步會產生一些問題:

1. 它的使用比較繁瑣。因為我們要用額外的程式碼把多個執行緒同時訪問的資料包圍起來,並獲取和釋放一個執行緒同步鎖,如果我們在一個程式碼塊忘記獲取鎖,就有可能造成資料損壞。

2. 使用執行緒同步會影響效能,獲取和釋放一個鎖肯定是需要時間的吧,因為我們在決定哪個執行緒先獲取鎖時候, CPU必須進行協調,進行這些額外的工作就會對效能造成影響

3. 因為執行緒同步一次只允許一個執行緒訪問資源,這樣就會阻塞執行緒,阻塞執行緒會造成更多的執行緒被建立,這樣CPU就有可能要排程更多的執行緒,同樣也對效能造成了影響。

所以在實際的設計中還是要儘量避免使用執行緒同步,因此我們要避免使用一些共享資料,例如靜態欄位。

二、執行緒同步的使用

2.1 對於使用鎖效能的影響

上面已經說過使用鎖將會對效能產生影響, 下面通過比較使用鎖和不使用鎖時消耗的時間來說明這點

using System;
using System.Diagnostics;
using System.Threading;

namespace InterlockedSample
{
  // 比較使用鎖和不使用鎖鎖消耗的時間
  // 通過時間來說明使用鎖效能的影響
  class Program
  {
    static void Main(string[] args)
    {
      int x = 0;
      // 迭代次數為500萬
      const int iterationNumber = 5000000; 
      // 不採用鎖的情況
      // StartNew方法 對新的 Stopwatch 例項進行初始化,將執行時間屬性設定為零,然後開始測量執行時間。
      Stopwatch sw = Stopwatch.StartNew();
      for (int i = 0; i < iterationNumber; i++)
      {
        x++;
      }

      Console.WriteLine("Use the all time is :{0} ms",sw.ElapsedMilliseconds);

      sw.Restart();
      // 使用鎖的情況
      for (int i = 0; i < iterationNumber; i++)
      {
        Interlocked.Increment(ref x);
      }

      Console.WriteLine("Use the all time is :{0} ms",sw.ElapsedMilliseconds);
      Console.Read();
    }
  }
}

執行結果(這是在我電腦上執行的結果)從結果中可以看出加了鎖的執行速度慢了好多(慢了11倍 197/18 ):

詳解c# 執行緒同步

2.2 Interlocked實現執行緒同步

Interlocked類提供了為多個執行緒共享的變數提供原子操作,當我們在多執行緒中對一個整數進行遞增操作時,就需要實現執行緒同步。

因為增加變數操作(++運算子)不是一個原子操作,需要執行下列步驟:

1)將例項變數中的值載入到暫存器中。

2)增加或減少該值。

3)在例項變數中儲存該值。

如果不使用 Interlocked.Increment方法,執行緒可能會在執行完前兩個步驟後被搶先。然後由另一個執行緒執行所有三個步驟,此時第一個執行緒還沒有把變數的值儲存到例項變數中去,而另一個執行緒就可以把例項變數載入到暫存器裡面讀取了(此時載入的值並沒有改變),所以會導致出現的結果不是我們預期的,相信這樣的解釋可以幫助大家更好的理解Interlocked.Increment方法和 原子性操作,

下面通過一段程式碼來演示下加鎖和不加鎖的區別(開始講過加鎖會對效能產生影響,這裡將介紹加鎖來解決執行緒同步的問題,得到我們預期的結果):

不加鎖的情況:

class Program
  {
    static void Main(string[] args)
    {   for (int i = 0; i < 10; i++)
      {
        Thread testthread = new Thread(Add);
        testthread.Start();
      }

      Console.Read();
    }

    // 共享資源
    public static int number = 1;

    public static void Add()
    {
      Thread.Sleep(1000);
      Console.WriteLine("the current value of number is:{0}",++number);
    }
}

執行結果(不同電腦上可能執行的結果和我的不一樣,但是都是得到不是預期的結果的):

詳解c# 執行緒同步

為了解決這樣的問題,我們可以通過使用 Interlocked.Increment方法來實現原子的自增操作。

程式碼很簡單,只需要把++number改成Interlocked.Increment(ref number)就可以得到我們預期的結果了,在這裡程式碼和執行結果就不貼了。

總之Interlocked類中的方法都是執行一次原子讀取以及寫入的操作的。

2.3 Monitor實現執行緒同步

對於上面那個情況也可以通過Monitor.Enter和Monitor.Exit方法來實現執行緒同步。C#中通過lock關鍵字來提供簡化的語法(lock可以理解為Monitor.Enter和Monitor.Exit方法的語法糖),程式碼也很簡單:

using System;
using System.Threading;

namespace MonitorSample
{
  class Program
  {
    static void Main(string[] args)
    {
      for (int i = 0; i < 10; i++)
      {
        Thread testthread = new Thread(Add);
        testthread.Start();
      }

      Console.Read();
    }

    // 共享資源
    public static int number = 1;

    public static void Add()
    {
      Thread.Sleep(1000);
      //獲得排他鎖
      Monitor.Enter(number);

      Console.WriteLine("the current value of number is:{0}",number++);

      // 釋放指定物件上的排他鎖。
      Monitor.Exit(number);
    }
  }
}

執行結果當然是我們所期望的:

詳解c# 執行緒同步

在 Monitor類中還有其他幾個方法在這裡也介紹,只是讓大家引起注意下,一個Wait方法,很明顯Wait方法的作用是:釋放某個物件上的鎖以便允許其他執行緒鎖定和訪問這個物件。第二個就是TryEnter方法,這個方法與Enter方法主要的區別在於是否阻塞當前執行緒,當一個物件通過Enter方法獲取鎖,而沒有執行Exit方法釋放鎖,當另一個執行緒想通過Enter獲得鎖時,此時該執行緒將會阻塞,直到另一個執行緒釋放鎖為止,而TryEnter不會阻塞執行緒。具體程式碼就不不寫出來了。

2.4 ReaderWriterLock實現執行緒同步

如果我們需要對一個共享資源執行多次讀取時,然而用前面所講的類實現的同步鎖都只允許一個執行緒允許,所有執行緒將阻塞,但是這種情況下肯本沒必要堵塞其他執行緒, 應該讓它們併發的執行,因為我們此時只是進行讀取操作,此時通過ReaderWriterLock類可以很好的實現讀取並行。

演示程式碼為:

using System;
using System.Collections.Generic;
using System.Threading;

namespace ReaderWriterLockSample
{
  class Program
  {
    public static List<int> lists = new List<int>();

    // 建立一個物件
    public static ReaderWriterLock readerwritelock = new ReaderWriterLock();
    static void Main(string[] args)
    {
      //建立一個執行緒讀取資料
      Thread t1 = new Thread(Write);
      t1.Start();
      // 建立10個執行緒讀取資料
      for (int i = 0; i < 10; i++)
      {
        Thread t = new Thread(Read);
        t.Start();
      }

      Console.Read();

    }

    // 寫入方法
    public static void Write()
    {
      // 獲取寫入鎖,以10毫秒為超時。
      readerwritelock.AcquireWriterLock(10);
      Random ran = new Random();
      int count = ran.Next(1,10);
      lists.Add(count);
      Console.WriteLine("Write the data is:" + count);
      // 釋放寫入鎖
      readerwritelock.ReleaseWriterLock();
    }

    // 讀取方法
    public static void Read()
    {
      // 獲取讀取鎖
      readerwritelock.AcquireReaderLock(10);

      foreach (int li in lists)
      {
        // 輸出讀取的資料
        Console.WriteLine(li);
      }

      // 釋放讀取鎖
      readerwritelock.ReleaseReaderLock();
    }
  }
}

執行結果:

詳解c# 執行緒同步

三、總結

本文中主要介紹如何實現多執行緒同步的問題, 通過執行緒同步可以防止共享資料的損壞,但是由於獲取鎖的過程會有效能損失,所以在設計應用過程中儘量減少執行緒同步的使用。本來還要介紹互斥(Mutex),訊號量(Semaphore),事件構造的, 由於篇幅的原因怕影響大家的閱讀,所以這剩下的內容放在後面介紹的。

以上就是詳解c# 執行緒同步的詳細內容,更多關於c# 執行緒同步的資料請關注我們其它相關文章!