C#多線程之線程鎖
一、Mutex類
“mutex”是術語“互相排斥(mutually exclusive)”的簡寫形式,也就是互斥量?;コ饬扛R界區(qū)中提到的Monitor很相似,只有擁有互斥對象的線程才具有訪問資源的權(quán)限,由于互斥對象只有一個,因此就決定了任何情況下此共享資源都不會同時被多個線程所訪問。當前占據(jù)資源的線程在任務處理完后應將擁有的互斥對象交出,以便其他線程在獲得后得以訪問資源?;コ饬勘扰R界區(qū)復雜,因為使用互斥不僅僅能夠在同一應用程序不同線程中實現(xiàn)資源的安全共享,而且可以在不同應用程序的線程之間實現(xiàn)對資源的安全共享。.Net中mutex由Mutex類來表示。
二、Mutex的用途
Mutex并不適合于有相互消息通知的同步;另一方面局部Mutex應該被Monitor/lock所取代;而跨應用程序的、相互消息通知的同步由EventWaiteHandle/AutoResetEvent/ManualResetEvent承擔更合適。所以,Mutex在.net中應用的場景似乎不多。不過,Mutex有個最常見的用途:用于控制一個應用程序只能有一個實例運行。系統(tǒng)依靠這個name屬性來標識唯一的Mutex。
private static Mutex mutex = null; //設為Static成員,是為了在整個程序生命周期內(nèi)持有Mutex
static void Main()
{
bool firstInstance;
mutex = new Mutex(true, @"Global\MutexSampleApp", out firstInstance);
try
{
if (firstInstance)
{
Console.WriteLine("我們是第一個實例!");
}
else
{
Console.WriteLine("警告,已有實例運行!");
return;
}
}
finally
{
//只有第一個實例獲得控制權(quán),因此只有在這種情況下才需要ReleaseMutex,否則會引發(fā)異常。
if (firstInstance)
{
mutex.ReleaseMutex();
}
mutex.Close();
mutex = null;
}
}三、Semaphore信號量
1、簡介
Semaphore是操作系統(tǒng)中用于控制線程同步互斥的信號量。在編寫多線程的程序時,可以使用Semaphore信號量來協(xié)調(diào)多線程并行,使各個線程能夠合理地共享資源,保證程序正確運行。
2、初始化
初始化Semaphore可當做開啟了一個線程池,initialCount代表剩余空位,maximumCount代表最大容量。示例如下,當前空位為0,最大容量為1:
Semaphore sem = new Semaphore(0, 1);
3、WaitOne()和Release()
Semaphore常用的方法有兩個WaitOne()和Release()。
使用WaitOne()方法相當于等待出現(xiàn)退出的線程,而使用Release()方法為讓一個線程退出。
假設initialCount和maximumCount都為5,開始的時候線程池有5個空位置,且總共只有5個位置,當需要并行的線程數(shù)量超過5個時,首先使用WaitOne()方法等待,發(fā)現(xiàn)有空位就依次進去,每進去一個空位減1,直到進去5個線程之后,空位(initialCount)為0,這時候后面的線程就一直等待,直到有線程調(diào)用了Release()方法,主動退出線程池,空位加1,在等待的線程才能繼續(xù)進入線程池。
下面的代碼示例創(chuàng)建一個信號量, 其最大計數(shù)為 3, 初始計數(shù)為零。 該示例啟動五個線程, 這會阻止等待信號量。 主線程使用Release(Int32)方法重載將信號量計數(shù)增加到其最大值, 從而允許三個線程進入信號量。 每個線程使用Thread.Sleep方法等待一秒, 以模擬工作, 然后Release()調(diào)用方法重載以釋放信號量。 每次釋放信號燈時, 都將顯示以前的信號量計數(shù)。 控制臺消息跟蹤信號量使用。 每個線程的模擬工作時間間隔略有增加, 使輸出更易于讀取。
private static Semaphore _pool;
private static int _padding;
public static void Main()
{
_pool = new Semaphore(0, 3);
for (int i = 1; i <= 5; i++)//創(chuàng)建并啟動五個線程。
{
Thread t = new Thread(new ParameterizedThreadStart(Worker));
t.Start(i);// 啟動線程,傳遞數(shù)字。
}
Thread.Sleep(500);
Console.WriteLine("Main thread calls Release(3).");
_pool.Release(3);//調(diào)用Release(3)會使信號量計數(shù)回其最大值,并允許等待的線程進入信號量,一次最多三個。
Console.WriteLine("Main thread exits.");
}
private static void Worker(object num)
{
Console.WriteLine("Thread {0} begins " + "and waits for the semaphore.", num);
_pool.WaitOne();
//填充間隔,使輸出更加有序。
int padding = Interlocked.Add(ref _padding, 100);
Console.WriteLine("Thread {0} enters the semaphore.", num);
Thread.Sleep(1000 + padding);
Console.WriteLine("Thread {0} releases the semaphore.", num);
Console.WriteLine("Thread {0} previous semaphore count: {1}", num, _pool.Release());
}四、Monitor類
當多個線程公用一個對象時,也會出現(xiàn)和公用代碼類似的問題,這就需要用到 System.Threading 中的 Monitor 類,我們可以稱之為監(jiān)視器,Monitor 提供了使線程共享資源的方案。
Monitor 類可以鎖定一個對象,一個線程只有得到這把鎖才可以對該對象進行操作。 對象鎖機制保證了在可能引起混亂的情況下,一個時刻只有一個線程可以訪問這個對象。Monitor 必須和一個具體的對象相關聯(lián)。
下面代碼說明了使用 Monitor 鎖定一個對象的情形:
// 表示對象的先進先出集合
Queue oQueue = new Queue();
try
{
// 現(xiàn)在 oQueue 對象只能被當前線程操縱了
Monitor.Enter(oQueue);
// do something......
}
catch
{
}
finally
{
// 釋放鎖
Monitor.Exit(oQueue);
}如上所示, 當一個線程調(diào)用 Monitor.Enter() 方法鎖定一個對象時,這個對象就歸它所有了,其它線程想要訪問這個對象,只有等待它使用Monitor.Exit() 方法釋放鎖。為了保證線程最終都能釋放鎖,你可以把 Monitor.Exit() 方法寫在 try-catch-finally 結(jié)構(gòu)中的 finally 代碼塊里。(lock 關鍵字就是這個步驟的語法糖)
任何一個被 Monitor 鎖定的對象,內(nèi)存中都保存著與它相關的一些信息:
- 現(xiàn)在持有鎖的線程的引用
- 一個預備隊列,隊列中保存了已經(jīng)準備好獲取鎖的線程
- 一個等待隊列,隊列中保存著當前正在等待這個對象狀態(tài)改變的隊列的引用
當擁有對象鎖的線程準備釋放鎖時,它使用 Monitor.Pulse() 方法通知等待隊列中的第一個線程,于是該線程被轉(zhuǎn)移到預備隊列中,當對象鎖被釋放時,在預備隊列中的線程可以立即獲得對象鎖。
典型的生產(chǎn)者與消費者實例
下面是一個展示如何使用 lock 關鍵字和 Monitor 類來實現(xiàn)線程的同步和通訊的例子。在本例中,生產(chǎn)者線程和消費者線程是交替進行的,生產(chǎn)者寫入一個數(shù),消費者立即讀取并且顯示(注釋中介紹了該程序的精要所在)。
/// <summary>
/// 測試類
/// </summary>
public class MonitorSample
{
public static void Main(String[] args)
{
// 一個標志位,如果是 0 表示程序沒有出錯,如果是 1 表明有錯誤發(fā)生
int result = 0;
// 下面使用 cell 初始化 CellProd 和 CellCons 兩個類,生產(chǎn)和消費次數(shù)均為 20 次
Cell cell = new Cell();
CellProd prod = new CellProd(cell, 20);
CellCons cons = new CellCons(cell, 20);
Thread producer = new Thread(new ThreadStart(prod.ThreadRun));
Thread consumer = new Thread(new ThreadStart(cons.ThreadRun));
// 生產(chǎn)者線程和消費者線程都已經(jīng)被創(chuàng)建,但是沒有開始執(zhí)行
try
{
producer.Start();
consumer.Start();
producer.Join();
consumer.Join();//等待這兩個線程結(jié)束才往下執(zhí)行
Console.ReadLine();
}
catch (ThreadStateException e)
{
// 當線程因為所處狀態(tài)的原因而不能執(zhí)行被請求的操作
Console.WriteLine(e);
result = 1;
}
catch (ThreadInterruptedException e)
{
// 當線程在等待狀態(tài)的時候中止
Console.WriteLine(e);
result = 1;
}
// 盡管 Main() 函數(shù)沒有返回值,但下面這條語句可以向父進程返回執(zhí)行結(jié)果
Environment.ExitCode = result;
}
}
/// <summary>
/// 生產(chǎn)者
/// </summary>
public class CellProd
{
/// <summary>
/// 被操作的 Cell 對象
/// </summary>
Cell cell;
/// <summary>
/// 生產(chǎn)者生產(chǎn)次數(shù),初始化為 1
/// </summary>
int quantity = 1;
public CellProd(Cell box, int request)
{
cell = box;
quantity = request;
}
public void ThreadRun()
{
for (int looper = 1; looper <= quantity; looper++)
{
// 生產(chǎn)者向操作對象寫入信息
cell.WriteToCell(looper);
}
}
}
/// <summary>
/// 消費者
/// </summary>
public class CellCons
{
Cell cell;
int quantity = 1;
public CellCons(Cell box, int request)
{
cell = box;
quantity = request;
}
public void ThreadRun()
{
int valReturned;
for (int looper = 1; looper <= quantity; looper++)
{
valReturned = cell.ReadFromCell(); // 消費者從操作對象中讀取信息
}
}
}
/// <summary>
/// 被操作的對象
/// </summary>
public class Cell
{
/// <summary>
/// Cell 對象里的內(nèi)容
/// </summary>
int cellContents;
/// <summary>
/// 狀態(tài)標志: 為 true 時可以讀取,為 false 則正在寫入
/// </summary>
bool readerFlag = false;
public int ReadFromCell()
{
lock (this)
{
if (!readerFlag)
{
try
{
// 等待 WriteToCell 方法中調(diào)用 Monitor.Pulse()方法
Monitor.Wait(this);
}
catch (SynchronizationLockException e)
{
Console.WriteLine(e);
}
catch (ThreadInterruptedException e)
{
Console.WriteLine(e);
}
}
// 開始消費行為
Console.WriteLine("Consume: {0}", cellContents);
Console.WriteLine();
// 重置 readerFlag 標志,表示消費行為已經(jīng)完成
readerFlag = false;
Monitor.Pulse(this);// 通知 WriteToCell()方法(該方法在另外一個線程中執(zhí)行,等待中)
}
return cellContents;
}
public void WriteToCell(int n)
{
lock (this)
{
if (readerFlag)
{
try
{
Monitor.Wait(this);
}
catch (SynchronizationLockException e)
{
// 當同步方法(指Monitor類除Enter之外的方法)在非同步的代碼區(qū)被調(diào)用
Console.WriteLine(e);
}
catch (ThreadInterruptedException e)
{
// 當線程在等待狀態(tài)的時候中止
Console.WriteLine(e);
}
}
cellContents = n;
Console.WriteLine("Produce: {0}", cellContents);
readerFlag = true;
Monitor.Pulse(this); // 通知另外一個線程中正在等待的 ReadFromCell() 方法
}
}
}五、Lock
C# 提供了一個關鍵字 lock,它可以把一段代碼定義為互斥段(critical section),互斥段在一個時刻內(nèi)只允許一個線程進入執(zhí)行,而其他線程必須等待。
在C#中,關鍵字 lock 的定義:
lock(expression)
{statement_block}
expression 代表你希望跟蹤的對象,通常是對象引用。如果你想保護一個類的實例,你可以使用 this;如果你想保護一個靜態(tài)變量(如互斥代碼段在一個靜態(tài)方法內(nèi)部),一般使用鎖定一個私有的static 成員變量就可以了。而 statement_block 就是互斥段的代碼,這段代碼在一個時刻內(nèi)只可能被一個線程執(zhí)行。
NET在一些集合類中(比如ArrayList,HashTable,Queue,Stack)已經(jīng)提供了一個供lock使用的對象SyncRoot,用Reflector工具查看了SyncRoot屬性的代碼,在Array中,該屬性只有一句話:return this,這樣和lock array的當前實例是一樣的。
Lock 語法簡單易用。其本質(zhì)是針對 Monitor.Enter() 和 Monitor.Exit() 的封裝,是一個語法糖!
static internal Thread[] threads = new Thread[10];
public static void Main()
{
Account acc = new Account(100);
for (int i = 0; i < 10; i++)
{
Thread t = new Thread(new ThreadStart(acc.DoTransactions));
threads[i] = t;
threads[i].Name = i.ToString();
threads[i].Start(); //10個線程同時啟動
}
}
internal class Account
{
int balance;internal Account(int initial)
{
balance = initial;
}
internal void DoTransactions()
{
for (int i = 0; i < 100; i++)
{
int amount = new Random().Next(-100, 100);
lock (this)
{
Console.WriteLine("當前線程:" + Thread.CurrentThread.Name + " 余額:" + balance.ToString() + " 數(shù)量:" + amount);
if (balance >= amount)
{
Thread.Sleep(5);
balance = balance - amount;
}
else
{
Console.WriteLine("當前線程:" + Thread.CurrentThread.Name + " 不能交易,余額不足:" + balance.ToString());
return;
}
}
}
}
}六、InterLocked(相當于lock,對整數(shù))
在C#中,賦值和簡單的數(shù)字運算都不是原子型操作。 在多線程環(huán)境下,我們可以通過使用System.Threading.Interlocked類來實現(xiàn)原子型操作當個數(shù)據(jù),使用它比使用Monitor類跟簡單。
使用.NET提供的Interlocked類可以對一些數(shù)據(jù)進行原子操作,看起來似乎跟lock鎖一樣,但它并不是lock鎖,它的原子操作是基于CPU本身的,非阻塞的,所以要比lock的效率高。
1、Interlocked類主要方法
- Read() 安全讀取數(shù)值,相等于int a=b
- Add() 安全相加一個數(shù)值,相當于 a = a + 3
- Increment() 安全遞加1,相當于 i++。返回遞增后的值。
- Decrement()安全遞減1,相當于 i--Exchange() 安全交換數(shù)據(jù),返回遞減后的值。
- CompareExchange() 安全比較兩個值是不是相等。如果相等,將第三個值與其中一個值交換。
2、實例
例一:
void Main()
{
TestIncrementUnSafe();
TestIncrementSafe();
}
private int value1 = 0;
public void TestIncrementUnSafe()
{
for (int i = 0; i < 5; i++)
{
Thread t = new Thread(IncrementValue1);
t.Name = "t1 " + i;
t.Start();
}
Thread.Sleep(2000);
//value maybe 500000
Console.WriteLine("value1 = " + value1);
}
private void IncrementValue1()
{
for (int i = 0; i < 1000000; i++)
{
value1++;
}
}
private int value2 = 0;
public void TestIncrementSafe()
{
for (int i = 0; i < 5; i++)
{
Thread t = new Thread(IncrementValue2);
t.Name = "t2 " + i;
t.Start();
}
Thread.Sleep(2000);
//value should be 500000
Console.WriteLine("value2 = " + value2);
}
private void IncrementValue2()
{
for (int i = 0; i < 1000000; i++)
{
Interlocked.Increment(ref value2);
}
}運行結(jié)果
value1 = 4612592
value2 = 5000000
例二、
void Main()
{
TestExchangeSafe();
TestCompareExchangeSafe();
}
private int value3 = 0;
public void TestExchangeSafe()
{
for (int i = 0; i < 5; i++)
{
Thread t = new Thread(ExchangeValue3);
t.Name = "t2 " + i;
t.Start();
}
Thread.Sleep(2000);
//value should be 83
Console.WriteLine("value3 = " + value3);
}
private void ExchangeValue3()
{
Interlocked.Exchange(ref value3, 83);
}
private int value4 = 0;
public void TestCompareExchangeSafe()
{
for (int i = 0; i < 5; i++)
{
Thread t = new Thread(ExchangeValue3);
t.Name = "t2 " + i;
t.Start();
}
Thread.Sleep(2000);
//value should be 99 or 0
Console.WriteLine("value4 = " + value4);
}
private void ExchangeValue4()
{
//if value4=0, set value4=99
Interlocked.CompareExchange(ref value4, 99, 0);
}運行結(jié)果:
value3 = 83
value4 = 0
到此這篇關于C#多線程之線程鎖的文章就介紹到這了。希望對大家的學習有所幫助,也希望大家多多支持腳本之家。
相關文章
C#實現(xiàn)獲取系統(tǒng)目錄并以Tree樹叉顯示的方法
這篇文章主要介紹了C#實現(xiàn)獲取系統(tǒng)目錄并以Tree樹叉顯示的方法,可以加深讀者對于C#下數(shù)據(jù)結(jié)構(gòu)實現(xiàn)方法的認識,需要的朋友可以參考下2014-07-07

