有關單線程想必不用在這裏羅嗦;多線程常常應用在大量密集型事務處理、高併發以及性能亟待提升的敏感區域,好處不言而喻:充分利用CPU資源、提高吞吐、減少用戶等待、同事增強了程序的靈活性,更有利於內存任務調度、數據交互、資源分配等; 但應用多線程,往往帶來額外的複雜性,如:死鎖、線程通訊、線程同步等等,暫且不用理會,具體問題具體分析便可。
不論是.NET還是Java, 都提供了相對成熟的線程模型,只要在使用中控制好:線程的同步性、線程在特定場景的通訊以及線程的安全性就可以了。本文介紹以下幾個場景下應用多線程的示例,以供參考!
§ 場景1
遠程服務數據查詢接口,但礙於網絡數據傳輸包大小的限制,每次只能查詢最多N條數據,所以如果要顯示或 提供100N條記錄給用戶就需要多次調用同一個接口,再進行封裝爲整體數據包給用戶。
public class Triple<K, V>
{
public K First { get; set; }
public V Second { get; set; }
public Triple()
{ }
public Triple(K k, V v)
{
this.First = k;
this.Second = v;
}
}
public static List<Triple<Request, Exception>> TakeActionOn<Request>(this Action<Request> action, List<Request> requests)
where Request : class
{
List<Triple<Request, Exception>> answers = new List<Triple<Request, Exception>>();
using (WorkCountDown wcd = new WorkCountDown(requests.Count))
{
object syncLock = new object();
foreach (Request request in requests)
{
ThreadPool.QueueUserWorkItem(o =>
{
Request req = o as Request;
Triple<Request, Exception> answer = new Triple<Request, Exception>();
try
{
answer.First = req;
action(req);
}
catch (Exception ex)
{
ExceptionSendMgr.SendMonitor(ex);
answer.Second = ex;
}
finally
{
lock (syncLock)
{
answers.Add(answer);
}
wcd.CountDown();
}
}, request);
}
wcd.CountDown();
wcd.WaitAll();
}
return answers;
}
/// <summary>
/// Instead of WaitHandle.WaitAll's max to 64 limit
/// </summary>
class WorkCountDown : IDisposable
{
int totalCount;
private readonly ManualResetEvent mre;
public WorkCountDown(int totalWorkCount)
{
totalCount = totalWorkCount + 1;
mre = new ManualResetEvent(false);
}
public void CountDown()
{
if (Interlocked.Decrement(ref totalCount) <= 0)
{
mre.Set();
}
}
/// <summary>
/// always remeber to call an extra CountDown before waitAll
/// </summary>
public void WaitAll()
{
mre.WaitOne();
}
#region IDisposable 成員
public void Dispose()
{
((IDisposable)mre).Dispose();
}
#endregion
}
同時對於擴展參數:Action<Request> action,內部處理也需要關注線程安全、同步異步操作,當然可通過ThreadPool.SetMaxThreads來控制線程池大小。
§ 場景2
多線程下載,這塊的應用相對複雜一些,總體來說就是:
根據文件大小,對文件進行分塊,同時通過建立對應的多個通道,進行併發下載,大大的提升下載效率,在下載過程主要存在的問題是:
1、 塊下載狀態的保存,存在下載暫停、中斷等狀況
2、 塊合併
3、 各線程執行狀況彙總,以便更新下載進度、下載報告(線程同步)
4、動態增加線程數
§多線程擴展
1、 ManualResetEvent
通常ThreadPool中的工作都完成以後,ManualResetEvent對象將被設置爲有信號,從而通知主線程繼續運行,有關ManualResetEvent:
可以將布爾值傳遞給構造函數來初始化ManualResetEvent 的初始狀態,true爲終止狀態;否則爲 false。
在初始化以後,該對象將保持原來的狀態不變,直到它的Reset()或者Set()方法被調用:
Reset()方法:將其設置爲無信號狀態;
Set()方法:將其設置爲有信號狀態。
WaitOne()方法:使當前線程掛起,直到ManualResetEvent對象處於有信號狀態,此時該線程將被激活。然後,程序將向線程池中添加工作項,這些以函數形式提供的工作項被系統用來初始化自動建立的線程。當所有的線程都運行完了以後,ManualResetEvent.Set()方法被調用,因爲調用了ManualResetEvent.WaitOne()方法而處在等待狀態的主線程將接收到這個信號,於是它接着往下執行,完成後邊的工作
2、InterLocked(線性):提供一些有用的原子操作,跟lock關鍵字在本質上是一樣的,但是Lock爲線性排他鎖,所以不適宜關注性能的多線程併發處理,也算是悲觀類型鎖
3、互斥對象Mutex(線性)
寫過WIN32 App的朋友應該不會陌生的是,在APP啓動時只允許一個實例運行時,比較簡潔的處理方式也是通過Mutex信號量來實現
進程之內或之間的線程需要訪問操作系統的資源的時候,需要一個控制機制來限制資源訪問的衝突都可以採用Mutex,同一時間,只能有一個線程佔用Mutex。訪問資源之前,每個線程都通過發信號,以獲得Mutex的控制權。此後,線程還必須等待資源的控制權。當線程完成操作時,通過ReleaseMutex()發出完成信號( lock和Monitor對於unmanaged 資源是不起作用的)
Mutex objMutex = new Mutex(false, "ThreadLock" );
objMutex.WaitOne();
//代碼
objMutex.ReleaseMutex();
適當的場景如果選擇Mutex,可採用下面的helper來輔助完成:
/// <summary>
/// 對managed/unmanaged資源進行加鎖的類
/// </summary>
public class ThreadLockHelper
{
static ThreadLockHelper mInstance = null;
Mutex mMutex = null;
private ThreadLockHelper() { }
public static ThreadLockHelper GetInstance()
{
if (mInstance == null)
{
mInstance = new ThreadLockHelper();
mInstance.mMutex = new Mutex(false, "ThreadLock");
} return (mInstance);
}
public bool CreateLock()
{
if (mMutex == null)
{ mMutex = new Mutex(false, "ThreadLock"); }
return (mMutex.WaitOne());
}
public void ReleaseLock()
{
mMutex.ReleaseMutex();
}
}
ThreadLockHelper.GetInstance().CreateLock();
objTask.DoTask();
ThreadLockHelper.GetInstance().ReleaseLock()
4、靜態方法屬性[MethodImpl(MethodImplOptions.Synchronized)]
指定同時只能由一個線程執行該方法。
靜態方法鎖定類型,而實例方法鎖定實例。在任何實例函數中只能有一個線程執行,並且在任何類的靜態函數中只能有一個線程執行。
有關Synchronized的應用,可詳細參考:http://www.cnblogs.com/artech/archive/2008/10/17/1313209.html
5、Monitor
鎖定和保護對象可以採用 lock(expression) statement_block expression:保護一個類的實例,一般地可使用this,保護一個靜態變量屬性(如互斥代碼段在一個靜態方法內部),一般使用類名就可以
當多線程公用一個對象時,也會出現和公用代碼類似的問題而多線程過程中不能採用Lock,我們可以採用Monitor鎖,只有得到鎖的線程纔可以操作
Queue oQueue=new Queue();
Monitor.Enter(oQueue);//oQueue對象只能被當前線程操縱,其他對象BLOCK
try
{
}
catch(){}
finally
{
Monitor.Exit(oQueue);//釋放鎖,其他線程可以操作
}
Monitor鎖定後,內存存在預備隊列、等待隊列
Monitor.Pulse()方法通知等待隊列中的第一個線程,於是該線程被轉移到預備隊列,鎖被釋放時,在預備隊列中的線程可以立即獲得對象鎖
Monitor.Wait(); //釋放對象上的鎖並阻止當前線程,直到它重新獲取該鎖
以下爲典型的生產者和消費者示例:(生產者寫入一個,消費者讀取一個,萬變不離其宗,瞭解衝突解決辦法即可):
class Program
{
static void Main(string[] args)
{
int result = 0; //一個標誌位,如果是0表示程序沒有出錯,如果是1表明有錯誤發生
Cell cell = new Cell();
//下面使用cell初始化CellProd和CellCons兩個類,生產和消費次數均爲20次
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));
//生產者線程和消費者線程都已經被創建,但是沒有開始執行
try
{
producer.Start();
consumer.Start();
producer.Join();
consumer.Join();
Console.ReadLine();
}
catch (ThreadStateException e)
{
//當線程因爲所處狀態的原因而不能執行被請求的操作
Console.WriteLine(e);
result = 1;
}
catch (ThreadInterruptedException e)
{
//當線程在等待狀態的時候中止
Console.WriteLine(e);
result = 1;
}
//儘管Main()函數沒有返回值,但下面這條語句可以向父進程返回執行結果
Environment.ExitCode = result;
}
}
public class Cell
{
int cellContents; // Cell對象裏邊的內容
bool readerFlag = false; // 狀態標誌,爲true時可以讀取,爲false則正在寫入
public int ReadFromCell()
{
lock (this) // Lock關鍵字保證了什麼,請大家看前面對lock的介紹
{
if (!readerFlag)//如果現在不可讀取
{
try
{
//等待WriteToCell方法中調用Monitor.Pulse()方法
Monitor.Wait(this);
}
catch (SynchronizationLockException e)
{
Console.WriteLine(e);
}
catch (ThreadInterruptedException e)
{
Console.WriteLine(e);
}
}
Console.WriteLine("Consume: {0}", cellContents);
readerFlag = false;
//重置readerFlag標誌,表示消費行爲已經完成
Monitor.Pulse(this);
//通知WriteToCell()方法(該方法在另外一個線程中執行,等待中)
}
return cellContents;
}
public void WriteToCell(int n)
{
lock (this)
{
if (readerFlag)
{
try
{
Monitor.Wait(this);
}
catch (SynchronizationLockException e)
{
//當同步方法(指Monitor類除Enter之外的方法)在非同步的代碼區被調用
Console.WriteLine(e);
}
catch (ThreadInterruptedException e)
{
//當線程在等待狀態的時候中止
Console.WriteLine(e);
}
}
cellContents = n;
Console.WriteLine("Produce: {0}", cellContents);
readerFlag = true;
Monitor.Pulse(this);
//通知另外一個線程中正在等待的ReadFromCell()方法
}
}
}
public class CellProd
{
Cell cell; // 被操作的Cell對象
int quantity = 1; // 生產者生產次數,初始化爲1
public CellProd(Cell box, int request)
{
//構造函數
cell = box;
quantity = request;
}
public void ThreadRun()
{
for (int looper = 1; looper <= quantity; looper++)
cell.WriteToCell(looper); //生產者向操作對象寫入信息
}
}
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();//消費者從操作對象中讀取信息
}
}
6、Thread相關
(1)IsBackground:true主線程結束,線程就自動結束,false主線程等待線程結束後纔會結束,如果是Windows服務中啓用該選項,
或許你能看到有些事務未處理完時,APP無法停止
7、 其他
(1)5天不再懼怕多線程:http://www.cnblogs.com/huangxincheng/archive/2012/03/14/2395279.html
(2)NET多線程探索-NET線程基礎知識點:http://www.cnblogs.com/hailan2012/archive/2012/03/19/2405161.html
NET多線程探索-線程同步和通信:http://www.cnblogs.com/hailan2012/archive/2012/03/20/2408179.html
NET多線程探索-互斥鎖,信號量,事件:http://www.cnblogs.com/hailan2012/archive/2012/03/22/2411934.html
(3)多線程讀寫鎖:http://www.mysjtu.com/page/M0/S540/540055.html
(4)死鎖:http://www.925lt.com/forum.php?mod=viewthread&tid=103
http://www.cnblogs.com/ols/articles/1179867.html