再談程式多工(III)─執行緒安全與資料集合


.NET提供了很多標準的資料結構,不過MSDN在這些資料結構的文件當中幾乎都會提到:

Public static (Shared in Visual Basic) members of this type are thread safe. Any instance members are not guaranteed to be thread safe.

也就是說,如果多個執行緒同時進行讀寫同一個資料集合,.NET不保證結果正確,所有執行緒同步處理的工作必須自己來。我遇過的狀況是這樣的:一個執行緒用foreach要對集合裡的每一個元素做一些運算,但是另一個執行緒有時候會修改集合增減元素。結果是通常第一個執行緒會丟出InvalidOperationException說集合被修改。MSDN建議的方式是在想辦法鎖住foreach,修改集合時要等這個鎖打開才行。原文如下:

A List<T> can support multiple readers concurrently, as long as the collection is not modified. Enumerating through a collection is intrinsically not a thread-safe procedure. In the rare case where an enumeration contends with one or more write accesses, the only way to ensure thread safety is to lock the collection during the entire enumeration. To allow the collection to be accessed by multiple threads for reading and writing, you must implement your own synchronization.

這時候,ReaderWriterLockSlim就有用了。底下是用generic寫出來的保證執行緒安全的資料集合:

 

  1. class SafeCollection<T, TCollection>: ICollection<T>, IDisposable
  2.     where TCollection: ICollection<T>, new()
  3. {
  4.     private TCollection m_collection = new TCollection();
  5.     private ReaderWriterLockSlim m_lock = new ReaderWriterLockSlim();
  6.     private bool m_disposed = false;
  7.  
  8.     private class Enumerator : IEnumerator<T>
  9.     {
  10.         private SafeCollection<T, TCollection> m_collection;
  11.         private IEnumerator<T> m_enumerator;
  12.         private bool m_locked = false;
  13.  
  14.         public Enumerator(SafeCollection<T, TCollection> collection)
  15.         {
  16.             m_collection = collection;
  17.             m_enumerator = collection.GetEnumerator();
  18.         }
  19.  
  20.         public T Current
  21.         {
  22.             get { return m_enumerator.Current; }
  23.         }
  24.  
  25.         public void Dispose()
  26.         {
  27.             if (m_locked)
  28.                 m_collection.m_lock.ExitReadLock();
  29.             m_enumerator.Dispose();
  30.         }
  31.  
  32.         object System.Collections.IEnumerator.Current
  33.         {
  34.             get { return this.Current; }
  35.         }
  36.  
  37.         public bool MoveNext()
  38.         {
  39.             if (!m_locked)
  40.             {
  41.                 m_locked = true;
  42.                 m_collection.m_lock.EnterReadLock();
  43.             }
  44.  
  45.             if (!m_enumerator.MoveNext())
  46.             {
  47.                 m_locked = false;
  48.                 m_collection.m_lock.ExitReadLock();
  49.             }
  50.             return m_locked;
  51.         }
  52.  
  53.         public void Reset()
  54.         {
  55.             m_enumerator.Reset();
  56.             m_locked = false;
  57.             m_collection.m_lock.ExitReadLock();
  58.         }
  59.     }
  60.  
  61.     public void Add(T item)
  62.     {
  63.         try
  64.         {
  65.             m_lock.EnterWriteLock();
  66.             m_collection.Add(item);
  67.         }
  68.         finally
  69.         {
  70.             m_lock.ExitWriteLock();
  71.         }
  72.     }
  73.  
  74.     public void Clear()
  75.     {
  76.         try
  77.         {
  78.             m_lock.EnterWriteLock();
  79.             m_collection.Clear();
  80.         }
  81.         finally
  82.         {
  83.             m_lock.ExitWriteLock();
  84.         }
  85.     }
  86.  
  87.     public bool Contains(T item)
  88.     {
  89.         try
  90.         {
  91.             m_lock.EnterReadLock();
  92.             return m_collection.Contains(item);
  93.         }
  94.         finally
  95.         {
  96.             m_lock.ExitReadLock();
  97.         }
  98.     }
  99.  
  100.     public void CopyTo(T[] array, int arrayIndex)
  101.     {
  102.         try
  103.         {
  104.             m_lock.EnterReadLock();
  105.             m_collection.CopyTo(array, arrayIndex);
  106.         }
  107.         finally
  108.         {
  109.             m_lock.ExitReadLock();
  110.         }
  111.     }
  112.  
  113.     public int Count
  114.     {
  115.         get
  116.         {
  117.             try
  118.             {
  119.                 m_lock.EnterReadLock();
  120.                 return m_collection.Count;
  121.             }
  122.             finally
  123.             {
  124.                 m_lock.ExitReadLock();
  125.             }
  126.         }
  127.     }
  128.  
  129.     public bool IsReadOnly
  130.     {
  131.         get
  132.         {
  133.             try
  134.             {
  135.                 m_lock.EnterReadLock();
  136.                 return m_collection.IsReadOnly;
  137.             }
  138.             finally
  139.             {
  140.                 m_lock.ExitReadLock();
  141.             }
  142.         }
  143.     }
  144.  
  145.     public bool Remove(T item)
  146.     {
  147.         try
  148.         {
  149.             m_lock.EnterWriteLock();
  150.             return m_collection.Remove(item);
  151.         }
  152.         finally
  153.         {
  154.             m_lock.ExitWriteLock();
  155.         }
  156.     }
  157.  
  158.     public IEnumerator<T> GetEnumerator()
  159.     {
  160.         return new Enumerator(this);
  161.     }
  162.  
  163.     IEnumerator IEnumerable.GetEnumerator()
  164.     {
  165.         return this.GetEnumerator();
  166.     }
  167.  
  168.     ~SafeCollection() { Dispose(false); }
  169.  
  170.     public void Dispose()
  171.     {
  172.         Dispose(true);
  173.         GC.SuppressFinalize(this);
  174.     }
  175.  
  176.     protected virtual void Dispose(bool disposing)
  177.     {
  178.         if (m_disposed)
  179.             return;
  180.         if (disposing)
  181.         {
  182.             m_lock.Dispose();
  183.             IDisposable disposable = m_collection as IDisposable;
  184.             if (disposable != null)
  185.                 disposable.Dispose();
  186.             m_disposed = true;
  187.         }
  188.     }
  189. }

有點長,但是不會很難懂,主要就是利用ReaderWriterLockSlim的特性把ICollection和IEnumerator重新包裝過,要用那種ICollection的實作類別都沒關係。之前提過foreach實際上是IEnumerator的MoveNext和Current的合體,所以把整個Enumerator鎖起來就行了,就因為這樣,我定義了一個新的Enumerator,只要這個新的Enumerator物件的MoveNext第一次被呼叫,就加上ReaderLock,移到集合尾巴的時候,就把鎖解開。這樣集合增減元素的時候就一定要等到所有的Enumerator的ReaderLock都解開才能進行(因為增減元素都是WriterLock)。當然用這篇寫的ReaderWriterLockSlimWithCountdownEvent更理想。

NET 4.0提供了另一個Namespace:System.Collections.Concurrent,顧名思義裡面有一些支援多執行緒的資料集合(也就是說它們是執行緒安全的),不過都和生產者─消費者設計模式(Producer-Consumer design pattern)有關,對於一般的資料結構,執行緒同步和鎖定還是要自己來。生產者─消費者設計模式其實就是我在這篇提到的把程式變成生產線的概念,細節等有時間再慢慢寫吧。

本篇發表於 Computers and Internet。將永久鏈結加入書籤。

3 Responses to 再談程式多工(III)─執行緒安全與資料集合

  1. Clark 說道:

    這篇文章解決的我工作上的問題~ 感謝您 😀

  2. Clark 說道:

    底下是把這篇文章的思路加上一些實作,做的紀錄文章。^^
    http://www.dotblogs.com.tw/clark/archive/2011/12/11/61518.aspx

發表留言