前段時間仔細看過些關於多線程方面的資料,項目中用到線程的地方也不少,可是,當看了Jeffrey的一篇關於鎖的文章後,發現自己雖然一直都在使用多線程,但是缺少了做多線程編程需要的思維!所以想從Jeffrey的Optex(鎖)入手,來談談我從其中體會的東西。
在.NET中,我們用的最多的鎖機制就是lock,用起來很簡單,短短幾行程序就可以實現,例如:
Lock 's Code
public class TestThreading
{
private System.Object lockThis = new System.Object();
public void Function()
{
lock (lockThis)
{
// Access thread-sensitive resources.
}
}
}
其實我們也明白,lock並不是鎖,而是MS提供的一個簡便式的寫法,真正實現的是Monitor類中的Enter和Exit方法,既然提到了Monitor類也就說下有個需要注意的地方:
Pulse和PulseAll方法,這兩個方法就是把鎖狀態將要改變的消息通知給等待隊列中的線程,不過這時如果等待隊列中沒有線程,那麼該方法就會一直等待下去,直到有等待的線程進入隊列,也就是說該方法可能造成類試死鎖的情況出現。
上面的lock + 線程(Thread和ThreadPool) = 多線程編程(N%)!?
對於該公式我曾經的N是80,現在是20。其中有很多東西影響我,讓我從80->20,下面的Optex就是一個入口點。
Optex 's Code
public sealed class Optex : IDisposable {
private Int32 m_Waiters = 0;
private Semaphore m_WaiterLock = new Semaphore(0, Int32.MaxValue);
public Optex() { }
public void Dispose() {
if (m_WaiterLock != null)
{
m_WaiterLock.Close();
m_WaiterLock = null;
}
}
public void Enter() {
Thread.BeginCriticalRegion();
// Add ourself to the set of threads interested in the Optex
if (Interlocked.Increment(ref m_Waiters) == 1) {
// If we were the first thread to show interest, we got it.
return;
}
// Another thread has the Optex, we need to wait for it
m_WaiterLock.WaitOne();
// When WaitOne returns, this thread now has the Optex
}
public void Exit() {
// Subtract ourself from the set of threads interested in the Optex
if (Interlocked.Decrement(ref m_Waiters) > 0) {
// Other threads are waiting, wake 1 of them
m_WaiterLock.Release(1);
}
Thread.EndCriticalRegion();
}
}
看完上面的代碼,讓我增加了兩點認識:
1、Thread.BeginCriticalRegion()和Thread.EndCriticalRegion();
因為這段時間正好看了一本多線程編程的書,既然將上面方法認為是進入臨界區和退出臨界區,對於臨界區而言,進入該區的數據,在沒有退出之前,如果臨界區外的程序需要使用它,那麼就必須出於等待。所以覺得已經使用臨界區,為什麼還要使用Semaphore?!
可是,MS只是取了個相同的名字,做的事情完全不同,上面兩個方法完全沒有臨界區的概念,它只是設置一個區域(Begin到End之間),表示該區域內發生線程中斷或未處理的異常會影響整個應用程序域。
2、m_Waiters的作用
一開始以為在Enter的時候,直接寫上:
m_WaiterLock.WaitOne();
Exit的時候,寫上:
m_WaiterLock.Release(1);
這樣就可以了。m_Waiters有什麼意義?!
優化性能,Semaphore是內核對象,我們都知道,要盡量少的進入內核模式,因為這是很消耗性能,所以盡量少的使用內核對象。m_Waiters的意義就在這裡,如果只有一個線程使用該鎖對象的時候,是不需要去獲取和釋放的。 OK,上述的東西都是鋪墊,鋪完了也就進入主題了!
多線程的思維
優化的Optex
namespace ThreadConcurrent.Lock
{
public sealed class Optex : IDisposable
{
/// <summary>
/// 瑣的狀態
/// </summary>
private Int32 m_LockState = c_lsFree;
/// <summary>
/// 自由狀態
/// </summary>
private const Int32 c_lsFree = 0x00000000;
/// <summary>
/// 被擁有狀態
/// </summary>
private const Int32 c_lsOwned = 0x00000001;
/// <summary>
/// 等待的線程數
/// </summary>
private const Int32 c_1Waiter = 0x00000002;
private Semaphore m_WaiterLock = new Semaphore(0, Int32.MaxValue);
#region 構造函數
/// <summary>
///
/// </summary>
public Optex() { }
#endregion
/// <summary>
/// 請求鎖
/// </summary>
public void Enter()
{
Thread.BeginCriticalRegion();
while (true)
{
Int32 ls = InterlockedOr(ref m_LockState, c_lsOwned);
//自由狀態
if ((ls & c_lsOwned) == c_lsFree) return;
// 增加等待的線程數
if (IfThen(ref m_LockState, ls, ls + c_1Waiter))
{
m_WaiterLock.WaitOne();
}
}
}
public void Exit()
{
// 釋放瑣
Int32 ls = InterlockedAnd(ref m_LockState, ~c_lsOwned);
//無等待的線程
if (ls == c_lsOwned)
{
}
else
{
ls &= ~c_lsOwned;
if (IfThen(ref m_LockState, ls & ~c_lsOwned, ls - c_1Waiter))
{
m_WaiterLock.Release(1);
}
else
{
}
}
Thread.EndCriticalRegion();
}
#region 原子化操作
/// <summary>
/// 與操作
/// </summary>
/// <param name="target"></param>
/// <param name="with"></param>
/// <returns></returns>
private static Int32 InterlockedAnd(ref Int32 target, Int32 with)
{
Int32 i, j = target;
do
{
i = j;
j = Interlocked.CompareExchange(ref target, i & with, i);
} while (i != j);
return j;
}
/// <summary>
/// 或操作
/// </summary>
/// <param name="target"></param>
/// <param name="with"></param>
/// <returns></returns>
private static Int32 InterlockedOr(ref Int32 target, Int32 with)
{
Int32 i, j = target;
do
{
i = j;
j = Interlocked.CompareExchange(ref target, i | with, i);
} while (i != j);
return j;
}
#endregion
private static Boolean IfThen(ref Int32 val, Int32 @if, Int32 then)
{
return (Interlocked.CompareExchange(ref val, @then, @if) == @if);
}
private static Boolean IfThen(ref Int32 val, Int32 @if, Int32 then, out Int32 prevVal)
{
prevVal = Interlocked.CompareExchange(ref val, @then, @if);
return (prevVal == @if);
}
/// <summary>
/// 釋放資源
/// </summary>
public void Dispose()
{
if (m_WaiterLock != null)
{
m_WaiterLock.Close();
m_WaiterLock = null;
}
}
}
}
對於上面的這個代碼,我暈眩了好一段時間,不過當我真正理解的時候,從暈眩中學到了做多線程編程應該具備的思維方式。
首先從簡單的理解開始談,
1、原子化操作
對於InterLocked類,曾經也知道,但是卻用的很少,不過從該代碼中知道,在多線程的編程中對共享數據的寫入操作,一定要達到原子性。至於如何做到這點,InterlockedAnd和InterlockedOr做了很好的诠釋:
While循環的目的就是保證target值以最新的值做與操作,如果傳入的值在執行的過程被其他線程改變的話,那麼是不會退出該循環的,並會利用改變後的值重新做次與操作。
2、理解Enter和Exit
這兩個方法很難寫出來解釋,用圖是最清晰的。
曾經的暈眩:
1、Enter方法中為什麼存在循環,為什麼不是執行完waitone就結束,必須m_lockState等於c_IsFree的時候才結束?
線程的執行並不完全按照先前排好的順序去執行,有時會發生一些特殊的情況來使改變線程的調度順序,所以就可能會出現上圖灰色部分的情況,則為了解決該可能發生的問題(概率很小)循環機制就出現了。
2、為什麼在WaitOne和Release之前,除了增加和減少等待者外,還需要判斷m_lockstate是否改變(進入Enter到執行Waitone前的這段時間)?
一般性的思維:
該程序的思維:
這樣做的好處就是盡量少的操作內核對象,提高性能!
多線程編程雖然復雜,但是我覺得很有意思和挑戰性,而且隨著硬件的發展,多線程編程會更加重要,既然已經上路就讓我們走到盡頭!