混合線程同步核心篇——自定義混合同步鎖,Monitor,lock,ReaderWriterLockSlim・・・


 前兩篇博客,分別介紹了用戶模式和內核模式的同步構造,由於它們各有優勢和劣勢。本文將介紹如何將這兩者的優勢結合在一起,構建一個性能良好的同步機制。

一,實現一個簡單的混合同步鎖

#region hybird lock
/// <summary>
/// 簡單的混合同步鎖
/// </summary>
private sealed class HybirdLock
{
private int m_waiters = 0;
AutoResetEvent m_waitLock = new AutoResetEvent(false);

public void Enter()
{
//如果只有一個線程,直接返回
if (Interlocked.Increment(ref m_waiters) == 1)
return;

//1個以上的線程在這里被阻塞
m_waitLock.WaitOne();
}

public void Leave()
{
//如果只有一個線程,直接返回
if (Interlocked.Decrement(ref m_waiters) == 0)
return;

//如果有多個線程等待,就喚醒一個
m_waitLock.Set();
}
}

優點:只有一個線程的時候僅在用戶模式下運行(速度極快),多於一個線程時才會用到內核模式(AutoRestEvent),這大大的提升了性能。由於線程的並發訪問畢竟是少數,多數情況下都是一個線程在訪問資源,利用用戶模式構造可以保證速度,利用內核模式又可以阻塞其它線程(雖然也有線程切換代價,但比起用戶模式的一直自旋浪費cpu時間可能會更好,況且只有在多線程沖突時才會使用這個內核模式,幾率很低)。

二、實現一個加入自旋,線程所有權,遞歸的混合同步鎖

  • 自旋:使多線程並發時,可以在一定的時間內維持在用戶模式,如果在這個期間獲得了鎖,就不用切換到內核模式,以避免切換的開銷。
  • 線程所有權:只有獲得鎖的線程才能釋放鎖。
  • 遞歸:就是同一線程可以多次調用獲取鎖的方法,然后調用等次數的釋放鎖的操作(mutex就屬於這種類型)。

下面來看看具體的實現:

/// <summary>
/// 加入自旋,線程多有權,遞歸的混合同步鎖
/// </summary>
private sealed class AnotherHybirdLock : IDisposable
{
//等待的線程數
private int m_waiters = 0;
//切換到內核模式是,用於同步
AutoResetEvent m_waitLock = new AutoResetEvent(false);

//用戶模式自旋的次數(可以調整大小)
private int m_spinCount = 4000;

//用於判斷獲取和釋放鎖是不是同一線程
private int m_owningThreadId = 0;

//同一線程循環計數(為0時,代表該線程不擁有鎖了)
private int m_recursion = 0;

private void Enter()
{
int threadId = Thread.CurrentThread.ManagedThreadId;

//同一線程,多次調用的情況
if (m_owningThreadId == threadId)
{
m_recursion++;
return;
}

//先采用用戶模式自旋,這避免了切換
SpinWait spinWait = new SpinWait();//.Net自帶的用於用戶模式等待的類
for (int i = 0; i < m_spinCount; i++)
{
//試圖在用戶模式等待獲得鎖,如果獲得成功,應跳過內核模式的阻塞
if (Interlocked.CompareExchange(ref m_waiters, 1, 0) == 0)
{
//這里用了goto語句,可以用flag等去掉goto
goto GotLock;
}

spinWait.SpinOnce();
}

//內核模式阻塞(在嘗試獲取了一次)
//如果=1,也不用在內核模式阻塞
if (Interlocked.Increment(ref m_waiters) > 1)
{
//多個線程在這里都會被阻塞
m_waitLock.WaitOne();//性能損失
//等這個線程醒來時,它擁有鎖,並記錄一些狀態
}

GotLock:
//線程獲取鎖是記錄線程Id,重置計數為1
m_owningThreadId = threadId;
m_recursion = 1;
}

private void Leave()
{
int threadId = Thread.CurrentThread.ManagedThreadId;
//檢查釋放鎖的線程的一致性
if (threadId != m_owningThreadId)
throw new SynchronizationLockException("Lock not owned by calling thread");

//同一線程,循環計數沒有歸0,不能遞減線程計數
if (--m_recursion > 0) return;

m_owningThreadId = 0;//沒有線程擁有鎖

//么有其它線程被阻塞,直接返回
if (Interlocked.Decrement(ref m_waiters) == 0)
return;

//有其他線程被阻塞,喚醒其中一個
m_waitLock.Set();//這里有性能損失
}

#region IDisposable
[MethodImpl(MethodImplOptions.Synchronized)]
public void Dispose()
{
m_waitLock.Dispose();
}

#endregion
}

注釋中,已經寫的相當詳細了,一定要好好理解它的實現方式,我們最常用的Monitor類和它的實現方式幾乎一樣。

三、細數FCL提供的混合構造:

有了上面的自定義混合同步構造的基礎,再來看看.net為我們都准備了哪些能夠直接使用的混合同步構造。

特別要注意一點:它們的性能都會比單純的內核模式構造(如Mutex,AutoResetEvent等)要好很多,在實際項目中,要酌情使用。

2.1 ManualResetEventSlim,SemaphoreSlim

它們的構造和內核模式的ManualResetEvent,Semaphore完全一致,只是它們都在用戶模式中“自旋”,而且都推遲到發生第一次競爭時,才創建內核模式的構造。另外,可以向wait方法傳入CancellationToken以支持取消。

2.2 Monitor類和同步塊

Monitor類應該是我們我們使用得最頻繁的同步技術。它提供了一個互斥鎖,這個鎖支持自旋,線程所有權和遞歸。和我們上面展示的那個自定義同步類AnotherHybirdLock相似。它是一個靜態類,提供了Enter和Exit方法用於獲取鎖和釋放鎖,會使用到傳遞給Enter和Exit方法對象的同步塊。同步塊的構造和AnotherHybirdLock的字段相似,包含一個內核對象、擁有線程的ID、一個遞歸計數、以及一個等待線程的計數。關於同步塊的概念,可以查閱其它的資料,這里不做太多的講解。

Monitor存在的問題以及使用建議:

  1.  Monitor類如果鎖住了一個業務對象,那么其他線程在該對象上的任何操作都會被阻塞。所以,最好的辦法是提供一個私有的專用字段用於鎖。如:private objectm_lock = new object();如果方法是靜態的,那么這個鎖字段也標注成靜態(static)就可以了。
  2. 不要對string對象加鎖。原因是,字符串可能留用(interning),兩個完全不同的代碼段可能指向同一個string對象。如果加鎖,兩個代碼段在完全不知情的情況下就被同步了。另一個原因是跨界一個AppDomain傳遞一個字符串時,不會復制副本,相反,它傳遞的是一個引用,如果加鎖,也會出現上面的情況。這是CLR在AppDomain隔離中的一個bug。
  3. 不要鎖住一個類型(Type)。如果一個類型對象是以“AppDomain中立”的方式加載,它會被其它AppDomain共享。線程會跨越AppDomain對該類型對象加鎖,這也是CLR的一個已知bug。
  4. 不要對值類型加鎖。每次調用Monitor的Enter方法,都會對這個值類型裝箱,造成每次鎖的對象都不一樣,無法做到線程同步。
  5. 避免向一個方法應用[MethodImpl( MethodImplOptions.Synchronized)]特性。如果方法是一個實例方法,那么JIT編譯器會加入Monitor.Enter(this)和Monitor.Exit(this)來包圍代碼。如果是一個靜態方法,傳給Enter方法的就是這個類的類型。
  6. 調用一個類型的類型構造器(靜態構造函數)時,CLR要獲取類型對象上的一個鎖,確保只有一個線程初始化類型對象及其靜態字段。同樣,如果類型是以“AppDomain中立”的方式加載,也會出現問題。例如,靜態構造函數里出現一個死循環,進程中所有AppDomain都不能使用該類型。所以要盡量保證靜態函數短小簡單,或盡量避免用類型構造器。

2.3 lock關鍵字

lock關鍵字是對Monitor類的一個簡化語法。

public void SomeMethod()
{
lock (this)
{
//對數據的獨占訪問。。。
}
}

//等價於下面這樣
public void SomeMehtodOther()
{
bool lockToken = false;
try
{
//線程可能在這里推出,還沒有執行Enter方法
Monitor.Enter(this, ref lockToken);
//對數據的獨占訪問。。。
}
finally
{
if (lockToken) Monitor.Exit(this);
}
}

lockToken變量的作用:如果一個線程在沒有調用Enter方法時就退出,這時它的值為false,finally塊中就不會調用Exit方法;如果成功獲得鎖,它就為true,這時就可以調用Exit方法。

lock關鍵字存在的問題:

Jeffrey指出,編譯器為lock關鍵字生成的代碼默認加上了try/finally塊,如果在對數據的獨占訪問時發生了異常,當前線程是可以正常退出的。但是,如果有其他的線程正在等待,它們會被喚醒,從而訪問到由於異常而被破壞掉的臟數據,進而引發安全漏洞。與其這樣,還不如讓進程終止。另外,進入一個try塊和finally塊會使代碼的速度變慢。它建議我們杜絕使用lock關鍵字,當然,估計太多的程序員都在使用lock關鍵字,該不該杜絕使用,自己判斷。

2.4 ReaderWriterLockSlim

互斥鎖保證多線程在訪問一個資源時,只有一個線程才會運行,其它的線程都阻塞了,這會降低應用程序的吞吐量。如果所有線程都以只讀的方法訪問資源,我們就沒有必要阻塞它。另一方面,如果一個線程希望修改數據,就需要獨占的訪問。ReaderWriterLockSlim就能解決這個問題。

它的實現方式是這樣的:

  • 一個線程寫入數據時,其它的所有線程都被阻塞。
  • 一個線程讀取數據時,請求讀取的線程繼續執行,請求寫入的線程被阻塞。
  • 一個線程寫入結束后,要么解除一個寫入線程的阻塞,要么解除一個讀取線程的阻塞。如果沒有線程被阻塞,鎖就自由了。
  • 所有讀取線程結束后,一個寫入線程解除阻塞。(可見讀取更優先)

一個簡單的例子:

public class MyResource:IDisposable
{
//LockRecursionPolicy(NoRecursion,SupportsRecursion)
//SupportsRecursion會導致增加遞歸,開銷會變得很大,盡量用NoRecursion
private ReaderWriterLockSlim m_lock = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion);

private object m_source;

public void WriteSource(object source)
{
m_lock.EnterWriteLock();
//寫獨占訪問
m_source = source;
m_lock.ExitWriteLock();
}

public object GetSource()
{
m_lock.EnterReadLock();
//共享訪問
object temp = m_source;
m_lock.ExitReadLock();
return temp;
}

#region IDisposable

public void Dispose()
{
m_lock.Dispose();
}

#endregion
}

.net 1.0提供了一個ReaderWriterLock,少了一個Slim后綴。它存在下面的幾個問題:

  1. 不存在線程競爭,數度也很慢。
  2. 線程所有權和遞歸被它進行了封裝,並且還取消不了。
  3. 相比writer,它更青睞reader,這可能造成writer排很長的對而得不到執行。

2.5 CountDownEvent

不太常用。這個構造阻塞一個線程,直到它的內部計數為0。這和Semaphore恰恰相反。如果它的CurrentCount變為0,就不能再度更改了。再次調用AddCount方法會拋出異常。

2.6 Barrier

不太常用。它可以用於一系列線程並行工作。每個參與者線程完成階段性工作后,都調用SignalAndWait方法阻塞自己,最后一個參與者線程調用SignalAndWait方法后會解除所有線程的阻塞。

如果你覺得本文對你還有一絲絲幫助,支持一下吧,總結提煉也要花很多精力呀,傷不起。。。

主要參考資料:

CLR Via C# 3 edition


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM