1. 程式人生 > >.NET進階篇06-async非同步、thread多執行緒4

.NET進階篇06-async非同步、thread多執行緒4

知識需要不斷積累、總結和沉澱,思考和寫作是成長的催化劑

梯子

一、鎖1、lock2、Interlocked3、Monitor4、SpinLock5、Mutex6、Semaphore7、Events1、AutoResetEvent2、ManualResetEvent3、ManualResetEventSlim8、ReaderWriterLock二、執行緒安全集合三、多執行緒模型1、同步程式設計模型SPM2、非同步程式設計模型APM3、基於事件程式設計模型EAP4、基於任務程式設計模型TAP四、End

一、鎖

資料庫中也有鎖概念,行鎖,表鎖,事物鎖等,鎖的作用就是控制併發情況下資料的安全一致

,使一個數據被操作時,其他併發執行緒等待。開發方面多執行緒並行程式設計訪問共享資料時,為保證資料的一致安全,有時需要使用鎖來鎖定物件來達到同步

.NET中提供很多執行緒同步技術。有lock,Interlocked,Monitor等用於程序內同步鎖,Mutex互斥鎖,Semaphore訊號量,Events,ReaderWriterLockSlim讀寫鎖等用於多個程序間的執行緒同步

1、lock

lock語句是設定對鎖定和解除鎖定的一種簡單方式,也是最常用的一種同步方式。lock用於鎖定一個引用型別欄位,當執行緒執行到Lock處,會鎖定該欄位,使之只有一個執行緒進入lock語句塊內,才lock語句結束位置再釋放鎖定,另一個執行緒才可以進入。原理運用同步塊索引,感興趣可以研究下

lock (obj)
{
    //synchronized region
}

因為只有一個執行緒可以進去,沒有併發,所以犧牲了效能,所以要儘量縮小lock的範圍,另一個建議是首選鎖一個私有變數,也就是SyncRoot模式,宣告一個syncRoot的私有object變數來進行鎖定,而不是使用lock(this),因為外面呼叫者也可能鎖定你這個物件的例項,但他並不知道你內部也使用了鎖,所以容易造成死鎖

private object syscRoot = new object();
public void DoThis()
{
    lock (syscRoot)
    {
        //同一個時間只有一個執行緒能到達這裡
    }
}

2、Interlocked

InterLoacked用於將變數的一些簡單操作原子化,也就是執行緒安全同步。我們常寫的i++就不是執行緒安全的,從記憶體中取值然後+1然後放回記憶體中,過程中很可能被其他執行緒打斷,比如在你+1後放回記憶體時,另一個執行緒已經先放回去了,也就不同步了。InerLocked類提供了以執行緒安全的方式遞增、遞減、交換、讀取值的方法
比如以下代替lock的遞增方式

int num = 0;
//lock (syscRoot)
//{
//    num++;
//}
num = Interlocked.Increment(ref num);

3、Monitor

上面lock就是Monitor的語法糖,通過編譯器編譯會生成Monitor的程式碼,像下面這樣

lock (syscRoot)
{
    //synchronized region
}
//上面的lock鎖等同於下面Monitor
Monitor.Enter(syscRoot);
try
{
    //synchronized region
}
finally
{
    Monitor.Exit(syscRoot);
}

Monitor不同於Lock就是它還可以設定超時時間,不會無限制的等待下去。

bool lockTaken = false;
Monitor.TryEnter(syscRoot,500,ref lockTaken);
if (lockTaken)
{
    try
    {
        //synchronized region
    }
    finally
    {
        Monitor.Exit(syscRoot);
    }
}
else
{
}

4、SpinLock

SpinLock自旋鎖是一種使用者模式鎖。對了,插一嘴鎖分為核心模式鎖和使用者模式鎖,核心模式就是在系統級別讓執行緒中斷,收到訊號時再切回來繼續幹活,使用者模式就是通過一些cpu指定或則死迴圈讓執行緒一直執行著直到可用。各有優缺點吧,核心Cpu資源利用率高,但切換損耗,使用者模式就相反,如果鎖定時間較長,就會白白迴圈等待,後面就有混合模式鎖的出現了

如果有大量的鎖定,且鎖定時間非常短,SpinLock就很有用,用法和Monitor類似,Enter或TryEnter獲取鎖,Exit釋放鎖。IsHeld和IsHeldByCurrentThread指定它當前是否鎖定

另外SpinLock是個結構型別,所以注意拷貝賦值時會建立全新副本問題。必要時可按引用來傳遞

5、Mutex

Mutex互斥鎖提供跨多個程序同步一個類,定義互斥鎖的時候可以指定互斥鎖的名稱,這樣系統能夠識別,所以在另一個程序中定義的互斥,其他程序也是可以訪問到的,Mutex.OpenExisting()便可以得到。

bool createdNew = false;
Mutex mutex = new Mutex(false, "ProCharpMutex", out createdNew);
if (mutex.WaitOne())
{
    try
    {
        //synchronized region
    }
    finally
    {
        mutex.ReleaseMutex();
    }
}

介於此我們可以用來禁止一個應用程式啟動兩次,一般我們通過程序的名稱來判斷,這裡我們使用Mutex實現

bool createdNew = false;
Mutex mutex = new Mutex(false, "SingletonWinAppMutex", out createdNew);
if (!createdNew)
{
    MessageBox.Show("應用程式已經啟動過了");
    Application.Exit();
    return;
}

6、Semaphore

Semaphore訊號量和互斥類似,區別是,訊號量可以同時讓多個執行緒使用,是一種計數的互斥鎖定。通過計數允許同時有幾個執行緒訪問受保護的資源。也可以指定訊號量名稱以使在多個程序間共享

Semaphore和上面Mutex都是繼承自WaitHandle基類,WaitHandle用於等待一個訊號的設定,嗲用Wait,執行緒會等待接收一個與等待控制代碼相關的訊號

SemaphoreSlim是對Semaphore的輕量替代版本(它不繼承WaitHandle),SemaphoreSlim(int initialCount, int maxCount)建構函式可指定最大併發個數,然後線上程內通過SemaphoreSlim的Wait等到直到來接收訊號是否可以進去受保護程式碼塊了,最後記得要Release,不然下一個執行緒獲取不到准許進入的訊號

7、Events

Events事件鎖不同於委託中的事件,在System.Threading名稱空間下,用於系統範圍內的事件資源的同步,有AutoResetEvent自動事件鎖、ManualResetEvent手動事件鎖以及輕量版本ManualResetEventSlim

1、AutoResetEvent

AutoResetEvent也是繼承自waitHandle類的,也是通過WaitOne來等待直到有訊號,它有兩種狀態:終止和非終止,可以呼叫set和reset方法使物件進入終止和非終止狀態。通俗點就是set有訊號,另一個執行緒可以進入了,reset非終止無資訊,其他執行緒就阻塞了。自動的意思就是一個執行緒進入了,自動Reset設定無訊號了其他執行緒就進不去了。類似現實中的汽車收費口,一杆一車模式

private AutoResetEvent autoEvent = new AutoResetEvent(false);
public void DoThis()
{
    autoEvent.WaitOne();
    //執行同步程式碼塊
    autoEvent.Set();
}
2、ManualResetEvent

手動事件鎖和自動的區別在於,手動事件鎖沒有訊號時會阻塞一批執行緒的,有訊號時,所有執行緒都執行,同時喚醒多個執行緒,除非手動Reset再阻塞,類似現實場景中火車道路口的柵欄,落杆攔截一批人,起杆則一批人蜂擁通過,用法和上面一樣,WaitOne等待訊號,結束時通過Set來通知有訊號了,可以通過了

3、ManualResetEventSlim

ManualResetEventSlim通過封裝 ManualResetEvent提供了自旋等待和核心等待的混合鎖模式。如果需要跨程序或者跨AppDomain的同步,那麼就必須使用ManualResetEvent。ManualResetEventSlim使用Wait來阻塞執行緒,支援任務的取消。和SemaphoreSlim的Wait一樣,內部先通過使用者模式自旋然後再通過核心模式效率更高

8、ReaderWriterLock

ReaderWriterLock讀寫鎖不是從限定執行緒個數的角度來保護資源,而是按讀寫角度來區分,就是你可以鎖定當某一類執行緒(寫執行緒)中一個進入受保護資源時,另一類執行緒(讀執行緒)全部阻塞。如果沒有寫入執行緒鎖定資源,就允許多個讀取執行緒方法資源,但只能有一個寫入執行緒鎖定該資源

具體用法參考示例

// 建立讀寫鎖
ReaderWriterLock rwLock = new ReaderWriterLock();
// 當前執行緒獲取讀鎖,引數為:超時值(毫秒)
rwLock.AcquireReaderLock(250);
// 判斷當前執行緒是否持有讀鎖
if (!rwLock.IsReaderLockHeld)
{
    return;
}
Console.WriteLine("拿到了讀鎖......");
// 將讀鎖升級為寫鎖,鎖引數為:超時值(毫秒)
LockCookie cookie = rwLock.UpgradeToWriterLock(250);
// 判斷當前執行緒是否持有寫鎖
if (rwLock.IsWriterLockHeld)
{
    Console.WriteLine("升級到了寫鎖......");
    // 將鎖還原到之前所的級別,也就是讀鎖
    rwLock.DowngradeFromWriterLock(ref cookie);
}
// 釋放讀鎖(減少鎖計數,直到計數達到零時,鎖被釋放)
rwLock.ReleaseReaderLock();
Console.WriteLine("順利執行完畢......");

// 當前執行緒獲取寫鎖,引數為:超時值(毫秒)
rwLock.AcquireWriterLock(250);
// 判斷當前執行緒是否持有寫鎖
if (rwLock.IsWriterLockHeld)
{
    Console.WriteLine("拿到了寫鎖......");
    // 釋放寫鎖(將減少寫鎖計數,直到計數變為零,釋放鎖)
    rwLock.ReleaseWriterLock();
}
// 釋放寫鎖(將減少寫鎖計數,直到計數變為零,釋放鎖)
// 當前執行緒不持有鎖,會丟擲異常
rwLock.ReleaseWriterLock();
Console.WriteLine("順利執行完畢......");
Console.ReadLine();

ReaderWriterLockSlim同樣是ReaderWriterLock的輕量優化版本,簡化了遞迴、升級和降級鎖定狀態的規則。
1. EnterWriteLock 進入寫模式鎖定狀態
2. EnterReadLock 進入讀模式鎖定狀態
3. EnterUpgradeableReadLock 進入可升級的讀模式鎖定狀態
並且三種鎖定模式都有超時機制、對應 Try… 方法,退出相應的模式則使用 Exit… 方法,而且所有的方法都必須是成對出現的

二、執行緒安全集合

並行環境下修改共享變數為了保證資源安全,通常使用上面介紹的鎖或訊號量來解決此問題。其實.NET也內建了一些執行緒安全的集合,使用他們就像使用單執行緒集合一樣。

型別描述
BlockingCollection 提供針對實現 IProducerConsumerCollection 的任何型別的限制和阻塞功能。 有關詳細資訊,請參閱BlockingCollection 概述。
ConcurrentDictionary<tkey,tvalue style="font-size: inherit; color: inherit; line-height: inherit; margin: 0px; padding: 0px;"> 鍵/值對字典的執行緒安全實現。
ConcurrentQueue FIFO(先進先出)佇列的執行緒安全實現。
ConcurrentStack LIFO(後進先出)堆疊的執行緒安全實現。
ConcurrentBag 無序的元素集合的執行緒安全實現。
IProducerConsumerCollection 型別必須實現以在 BlockingCollection 中使用的介面。

三、多執行緒模型

1、同步程式設計模型SPM

2、非同步程式設計模型APM

我們常見的XXBegin, XXEnd這兩個經典的配對方法就是非同步的,Begin後會委託給執行緒池呼叫一個執行緒去執行。還有委託的BeginInvoke呼叫

FileStream fs = new FileStream("D:\\test.txt", FileMode.Open);
var bytes = new byte[fs.Length];
fs.BeginRead(bytes, 0, bytes.Length, (aysc) =>
{
    var num = fs.EndRead(aysc);
}, string.Empty);

3、基於事件程式設計模型EAP

WinFrom/WPF開發中的BackgroundWorker類就是非同步事件模式的一種實現方案,RunWorkerAsync方法啟動與DoWork事件非同步關聯的方法,工作完成後,就觸發RunWorkerCompleted事件,也支援CancelAysnc方法取消以及ReportProgress通知進度等。還又一個典型的就是WebClient

WebClient client = new WebClient();
client.DownloadDataCompleted += (sender,e)=> 
{
};
client.DownloadDataAsync(new Uri("https://www.baidu.com/"));

4、基於任務程式設計模型TAP

Task出來後,微軟就大力推廣基於Task的非同步程式設計模型,APM和EAP都被包裝成Task使用。下面示例簡單用Task封裝上面的程式設計模型。WebClient的DownloadDataTaskAsync實現和示例中的類似,利用一個TaskCompletionSource包裝器包裝成Task

FileStream fs = new FileStream("D:\\test.txt", FileMode.Open);
var bytes = new byte[fs.Length];
var task = Task.Factory.FromAsync(fs.BeginRead, fs.EndRead, bytes, 0, bytes.Length, string.Empty);
var nums = task.Result;

Action action = () =>{ };
var task = Task.Factory.FromAsync(action.BeginInvoke, action.EndInvoke, string.Empty);

public static Task<int> GetTaskAsuc(string url)
{
    TaskCompletionSource<int> source = new TaskCompletionSource<int>();//包裝器
    WebClient client = new WebClient();
    client.DownloadDataCompleted += (sender, e) =>
    {
        try
        {
            source.TrySetResult(e.Result.Length);
        }
        catch (Exception ex)
        {
            source.TrySetException(ex);
        }
    };
    client.DownloadDataAsync(new Uri(url));
    return source.Task;
}

四、End

最近幾篇介紹瞭如何編寫多執行緒和多工應用程式。在應用程式開發過程中要仔細規劃,太多的執行緒導致資源問題,太少則起不到大效果。多執行緒程式設計中一箇中肯的建議就是
儘量避免修改共享變數,使同步的要求變低。通過合理規劃可以減少大部分的同步複雜度。

Search the fucking web
Read the fucking maunal

——GoodGoodStudy