------- Windows Phone 7手機開發、.Net培訓、期待與您交流! -------
在黑馬論壇上提出了一個關於多線程的定時任務的問題,可能是我們Asp.Net板塊論壇太冷清了,回答的人很少,也有可能自己提出的問題too easy,別人不屑回答。現將自己關於多線程學到整理成筆記,供大家參考。
一、線程處理概述
進程是操作系統中正在執行的不同應用程序的一個實例,操作系統把不同的進程分離開來。
線程是操作系統分配處理器時間的基本單元,每個線程都維護異常處理程序、調度優先級和一組系統用於在調度該線程前保存線程上下文的結構。
每個應用程序域都是用單個線程啓動的(應用程序的入口點Main方法),應用程序域中的代碼可以創建附加應用程序域和附加線程。
線程的優缺點
線程處理使程序能夠執行併發處理,因而特別適合需要同時執行多個操作的場合。
多線程處理可解決用戶響應性能和多任務的問題,但同時引入了資源共享和同步問題等問題。
二、創建多線程應用程序
應用程序運行時,將創建新的應用程序域。當運行環境調用應用程序的入口點(Main方法)時,將創建應用程序主線程。下面給出一段主線程示例。
using System;
using System.Threading;
namespace CSharpPractice.Threading
{
public class WorkerThreadExample
{
static void Main()
{
Console.WriteLine("主線程:開始……");
// 主線程睡眠5秒鐘
Thread.Sleep(5000);
Console.WriteLine("主線程:結束。");
Console.ReadLine();
}
}
}
2.創建和啓動新線程
主線程以外的線程一般稱之爲工作線程。創建新線程的大致步驟如下:
創建一個將在主線程外執行的函數,即類的方法,用於執行新線程要執行的邏輯操作
在主線程(Main方法)中創建一個Thread的實例,指向步驟1中的函數。例如:Thread newThread = new Thread(anObject.AMethod)
調用創建的Thread的實例的Start()方法,以啓動新線程。例如:newThread.Start()。創建和啓動新線程示例。
using System;
using System.Threading;
namespace CSharpPractice.Threading
{
public class Worker
{ // 工作線程執行邏輯的實現方法.
public void DoWork()
{
while (!_shouldStop)
{
Console.WriteLine("工作線程:working...");
}
Console.WriteLine("工作線程:terminating gracefully.");
}
public void RequestStop()
{
_shouldStop = true;
}
// 使用Volatile以啓發編譯器,本數據成員將被多線程訪問.
private volatile bool _shouldStop;
}
public class WorkerThreadExample
{
static void Main()
{
Console.WriteLine("主線程:Starting worker thread...");
// 創建工作線程對象。但不啓動線程.
Worker workerObject = new Worker();
Thread workerThread = new Thread(workerObject.DoWork);
// 啓動工作線程.
workerThread.Start();
// 循環直至激活工作線程.
while (!workerThread.IsAlive) ;
// 讓主線程睡眠1毫秒,以允許工作線程完成自己的工作:
Thread.Sleep(1);
// 要求工作線程停止自己:
workerObject.RequestStop();
// 使用Join方法阻止當前線程,直至對象線程終止.
workerThread.Join();
Console.WriteLine("主線程:Worker thread has terminated.");
Console.ReadLine();
}
}
}
3.暫停、中斷和銷燬線程
調用Thread.Sleep方法會導致當前線程立即阻止,阻止時間的長度等於傳遞給Thread.Sleep 的毫秒數。注意:一個線程不能針對另一個線程調用Thread.Sleep。
暫停線程示例
using System;
using System.Threading;
namespace CSharpPractice.Threading
{
class PauseThreadTest
{
static void Main()
{
Console.WriteLine("主線程啓動...");
//創建並啓動工作線程
Thread newThread = new Thread(ThreadMethod);
//newThread.SetApartmentState(ApartmentState.MTA);
newThread.Start();
Console.WriteLine("主線程睡眠300ms...");
Thread.Sleep(300);
Console.WriteLine("主線程終止...");
Console.ReadKey();
}
static void ThreadMethod()
{
Console.WriteLine("工作線程啓動...");
Console.WriteLine("工作線程睡眠1000ms...");
Thread.Sleep(1000);
Console.WriteLine("工作線程終止...");
}
}
}
通過對被阻止的線程調用Thread.Interrupt,可以中斷正在等待的線程並引發ThreadInterruptedException,從而使該線程脫離造成阻止的調用。
中斷線程示例
using System;
using System.Security.Permissions;
using System.Threading;
namespace CSharpPractice.Threading
{
class ThreadInterrupt
{
static void Main()
{
StayAwake stayAwake = new StayAwake();
Thread newThread = new Thread(stayAwake.ThreadMethod);
newThread.Start();
// 如果newThread 當前正被阻止或者將被阻止
// 下面的代碼將在ThreadMethod中產生一個異常
Console.WriteLine("主線程調用newThread的Interrupt方法.");
newThread.Interrupt();
// 等待newThread結束.
newThread.Join();
Console.ReadKey();
}
}
class StayAwake
{
public void ThreadMethod()
{
Console.WriteLine("newThread正在執行ThreadMethod……");
try
{
Console.WriteLine("newThread 將進入睡眠……");
Thread.Sleep(Timeout.Infinite);
}
catch (ThreadInterruptedException e)
{
Console.WriteLine("newThread 被主線程中斷.");
}
}
}
}
Abort 方法用於永久地停止也即銷燬託管線程。調用Abort時,公共語言運行庫在目標線程中引發ThreadAbortException,目標線程可捕捉此異常。
銷燬線程示例
using System;
using System.Threading;
namespace CSharpPractice.Threading
{
class Test
{
public static void Main()
{
Thread newThread = new Thread(new ThreadStart(TestMethod));
newThread.Start();
Thread.Sleep(1000);
// 銷燬newThread.
Console.WriteLine("主程序銷燬新線程……");
newThread.Abort("來自於主程序的信息.");
// 等待線程終止.
newThread.Join();
Console.WriteLine("新線程終止 – 退出主程序.");
Console.ReadLine();
}
static void TestMethod()
{
try
{
while (true)
{
Console.WriteLine("新線程運行中……");
Thread.Sleep(1000);
}
}
catch (ThreadAbortException abortException)
{
Console.WriteLine((string)abortException.ExceptionState);
}
}
}
}
三、線程優先級和線程調度
每個線程都有一個分配的優先級,在運行庫內創建的線程最初被分配 Normal 優先級。通過線程的Priority屬性可以獲取和設置其優先級。下面是線程優先級和線程調度示例。
using System;
using System.Threading;
namespace CSharpPractice.Threading
{
class PriorityTest
{
bool loopSwitch;
public PriorityTest()
{
loopSwitch = true;
}
public bool LoopSwitch
{
set { loopSwitch = value; }
}
public void ThreadMethod()
{
long threadCount = 0;
while (loopSwitch)
{
threadCount++;
}
Console.WriteLine("{0} with {1,11} priority " +
"has a count = {2,13}", Thread.CurrentThread.Name,
Thread.CurrentThread.Priority.ToString(),
threadCount.ToString("N0"));
}
}
class Test
{
static void Main()
{
PriorityTest priorityTest = new PriorityTest();
ThreadStart startDelegate =
new ThreadStart(priorityTest.ThreadMethod);
Thread threadOne = new Thread(startDelegate);
threadOne.Name = "ThreadOne";
Thread threadTwo = new Thread(startDelegate);
threadTwo.Name = "ThreadTwo";
threadTwo.Priority = ThreadPriority.BelowNormal;
Console.WriteLine("啓動線程1(優先級默認爲Normal)......");
threadOne.Start();
Console.WriteLine("啓動線程2(優先級設置爲BelowNormal)......");
threadTwo.Start();
// 允許數10 seconds.
Console.WriteLine("請耐心等待5秒......");
Thread.Sleep(5000);
priorityTest.LoopSwitch = false;
Console.ReadLine();
}
}
}
四、線程同步
當多個線程可以調用單個對象的屬性和方法時,一個線程可能會中斷另一個線程正在執行的任務,使該對象處於一種無效狀態。因此必須針對這些調用進行同步處理。可以使用lock語句同步代碼塊,或使用監視器同步代碼塊。
2.使用lock語句同步代碼塊
lock語句使用lock關鍵字將語句塊標記爲臨界區,方法是獲取給定對象的互斥鎖,執行語句,然後釋放該鎖。lock關鍵字可以確保當一個線程位於代碼的臨界區時,另一個線程不會進入該臨界區。如果其他線程試圖進入鎖定的代碼,則它將一直等待(即被阻止),直到該對象被釋放。代碼塊完成運行,而不會被其他線程中斷。
lock語句以關鍵字lock開頭,並以一個對象作爲參數,在該參數的後面爲線程互斥的代碼塊。
使用lock語句同步代碼塊示例
using System;
using System.Threading;
namespace CSharpPractice.Threading
{
class Account //賬戶類
{
private Object thisLock = new Object();
int balance;
Random r = new Random(); //準備生成隨機數
public Account(int initial) //賬戶構造函數
{
balance = initial;
}
int Withdraw(int amount) //從賬戶中取款
{
// 本條件永遠不會爲True,除非註釋掉lock語句
if (balance < 0) //賬戶餘額不足,<=0
{
throw new Exception("賬戶餘額不足(<=0)!");
}
// 註釋掉下面的lock語句,測試lock關鍵字的效果
// lock (thisLock)
{
if (balance >= amount) //賬戶餘額>取款額
{
Console.WriteLine("取款前賬戶餘額: " + balance); //取款前賬戶餘額
Console.WriteLine("取款額(-) : -" + amount); //取款額
balance = balance - amount;
Console.WriteLine("取款後賬戶餘額: " + balance); //取款後賬戶餘額
return amount;
}
else
{
return 0; // 拒絕交易
}
}
}
public void DoTransactions() // 執行交易DoTransactions()
{ // 從賬戶中取100次錢款,每次取款額爲1~100中的隨機數
for (int i = 0; i < 100; i++)
{
Withdraw(r.Next(1, 100));
}
}
}
class Test
{
static void Main()
{
Thread[] threads = new Thread[10]; //線程數組(10個元素)
Account acc = new Account(1000); //新建賬戶對象,初始存款額爲1000
for (int i = 0; i < 10; i++) //循環10次
{ //每次開始新的線程,執行賬戶交易DoTransactions()
Thread t = new Thread(new ThreadStart(acc.DoTransactions));
threads[i] = t;
}
for (int i = 0; i < 10; i++) //循環10次
{
threads[i].Start(); //開始線程
}
Console.ReadLine();
}
}
}
3.使用監視器同步代碼塊
使用監視器(Monitor)也可以防止多個線程同時執行代碼塊。調用Monitor.Enter方法,允許一個且僅一個線程繼續執行後面的語句;其他所有線程都將被阻止,直到執行語句的線程調用Exit。
4.同步事件和等待句柄
同步事件允許線程通過發信號互相通信,從而實現線程需要獨佔訪問的資源的同步處理控制
同步事件有兩種:AutoResetEvent(自動重置的本地事件)和 ManualResetEvent(手動重置的本地事件)
每種事件又包括兩種狀態:收到信號狀態(signaled)和未收到信號狀態(unsignaled)。下面是同步事件和等待句柄示例。
using System;
using System.Threading;
using System.Collections;
using System.Collections.Generic;
namespace CSharpPractice.Threading
{
public class SyncEvents
{
public SyncEvents()
{
_newItemEvent = new AutoResetEvent(false);
_exitThreadEvent = new ManualResetEvent(false);
_eventArray = new WaitHandle[2];
_eventArray[0] = _newItemEvent;
_eventArray[1] = _exitThreadEvent;
}
public EventWaitHandle ExitThreadEvent
{
get { return _exitThreadEvent; }
}
public EventWaitHandle NewItemEvent
{
get { return _newItemEvent; }
}
public WaitHandle[] EventArray
{
get { return _eventArray; }
}
private EventWaitHandle _newItemEvent;
private EventWaitHandle _exitThreadEvent;
private WaitHandle[] _eventArray;
}
public class Producer //製造者線程
{
public Producer(Queue<int> q, SyncEvents e) //構造函數
{
_queue = q;
_syncEvents = e;
}
// Producer.ThreadRun方法一直循環,直到“退出線程”事件變爲終止狀態。
// 此事件的狀態由 WaitOne 方法使用 SyncEvents 類定義的 ExitThreadEvent 屬性進行測試。
// 在這種情況下,檢查該事件的狀態不會阻止當前線程,
// 因爲 WaitOne 使用的第一個參數爲零,這表示該方法應立即返回。
// 如果 WaitOne 返回 true,則說明該事件當前處於終止狀態。
// ThreadRun 方法將返回,其效果相當於終止執行此方法的輔助線程。
public void ThreadRun()
{
int count = 0;
Random r = new Random();
// 在“退出線程”事件終止前,Producer.ThreadStart 方法將嘗試在隊列中保留 20 項。
// 項只是 0 到 100 之間的一個整數。在添加新項前,必須鎖定該集合,
// 以防止使用者線程和主線程同時訪問該集合。這一點是使用 lock 關鍵字完成的。
// 傳遞給 lock 的參數是通過 ICollection 接口公開的 SyncRoot 字段。
// 此字段專門爲同步線程訪問而提供。對該集合的獨佔訪問權限
// 被授予 lock 後面的代碼塊中包含的所有指令。對於製造者添加
// 到隊列中的每個新項,都將調用“新項”事件的 Set 方法。
// 這將通知使用者線程離開掛起狀態並開始處理新項。
while (!_syncEvents.ExitThreadEvent.WaitOne(0, false))
{
lock (((ICollection)_queue).SyncRoot)
{
while (_queue.Count < 20)
{
_queue.Enqueue(r.Next(0, 100));
_syncEvents.NewItemEvent.Set();
count++;
}
}
}
Console.WriteLine("製造者線程: produced {0} items", count);
}
private Queue<int> _queue;
private SyncEvents _syncEvents;
}
public class Consumer //使用者線程
{
public Consumer(Queue<int> q, SyncEvents e)
{
_queue = q;
_syncEvents = e;
}
// Consumer.ThreadRun
// Consumer 對象還定義名爲 ThreadRun 的方法。
// 與製造者的 ThreadRun 類似,此方法由 Main 方法創建的輔助線程執行。
// 然而,使用者的 ThreadStart 必須響應兩個事件。
public void ThreadRun()
{
int count = 0;
// 使用 WaitAny 阻止使用者線程,直到所提供的數組中的任意一個
// 等待句柄變爲終止狀態。在這種情況下,數組中有兩個句柄,
// 一個用來終止輔助線程,另一個用來指示有新項添加到集合中。
// WaitAny 返回變爲終止狀態的事件的索引。“新項”事件是數組中的
// 第一個事件,因此索引零表示新項。在這種情況下,檢查索引 1
//(它指示“退出線程”事件),並使用它來確定此方法是否繼續使用項。
// 如果“新項”事件處於終止狀態,將通過 lock 獲得對集合的
// 獨佔訪問權限並使用新項。因爲此示例生成並使用數千個項,
// 所以不顯示使用的每個項,而是使用 Main 定期顯示隊列中的內容
while (WaitHandle.WaitAny(_syncEvents.EventArray) != 1)
{
lock (((ICollection)_queue).SyncRoot)
{
int item = _queue.Dequeue();
}
count++;
}
Console.WriteLine("使用者線程: consumed {0} items", count);
}
private Queue<int> _queue;
private SyncEvents _syncEvents;
}
public class ThreadSyncSample
{ // 線程同步示例:使用 lock 獲得對隊列的獨佔訪問權限
private static void ShowQueueContents(Queue<int> q) //此方法由主線程執行的(被Main調用)
{
lock (((ICollection)q).SyncRoot)
{ // 對整個集合進行枚舉
foreach (int item in q)
{
Console.Write("{0} ", item);
}
}
Console.WriteLine();
}
static void Main()
{ // 創建一個隊列(該隊列的內容將被生成和使用)
Queue<int> queue = new Queue<int>();
// 創建SyncEvents 的一個實例
SyncEvents syncEvents = new SyncEvents();
Console.WriteLine("配置製造者線程和使用者線程......");
// 配置 Producer 和 Consumer 對象以供輔助線程使用
Producer producer = new Producer(queue, syncEvents);
Consumer consumer = new Consumer(queue, syncEvents);
Thread producerThread = new Thread(producer.ThreadRun);
Thread consumerThread = new Thread(consumer.ThreadRun);
// 調用 Start 方法來啓動兩個輔助線程。
// 獨立於當前正在執行 Main 方法的主線程開始異步執行過程
Console.WriteLine("啓動製造者線程和使用者線程......");
producerThread.Start();
consumerThread.Start();
// Main通過調用 Sleep 方法將主線程掛起。
for (int i = 0; i < 4; i++) //重複四次
{ // 將當前正在執行的線程掛起2500毫秒
Thread.Sleep(2500);
// 2500毫秒後,Main 將重新激活,顯示隊列的內容
ShowQueueContents(queue);
}
// Main通過調用“退出線程”事件的 Set 方法通知輔助線程終止
Console.WriteLine("發信號,告知終止線程......");
syncEvents.ExitThreadEvent.Set();
//對每個輔助線程調用 Join 方法以阻止主線程,
//直到每個輔助線程都響應該事件並終止。
producerThread.Join();
consumerThread.Join();
Console.ReadLine();
}
}
5.使用Mutex同步代碼塊
mutex(mutually exclusive,互斥體)由 Mutex 類表示,與監視器(Monitor)類似,用於防止多個線程在某一時間同時執行某個代碼塊。
與監視器不同的是,mutex 可以用來使跨進程的線程同步。儘管mutex可以用於進程內的線程同步,但是它會消耗更多的計算資源,所以進程內的線程同步建議使用監視器(Monitor)。
當用於進程間同步時,mutex 稱爲“命名mutex”,因爲它將用於另一個應用程序,因此它不能通過全局變量或靜態變量共享。必須給它指定一個名稱,才能使兩個應用程序訪問同一個mutex 對象。
Mutex 有兩種類型:未命名的局部 mutex 和已命名的系統 mutex。下面是使用Mutex同步代碼塊示例。
// 本例顯示如何使用“命名mutex”在進程或線程間發送信號。
// 在2個或多個命令行窗口運行本程序。
// 每個進程將創建一個Mutex對象:命名互斥體"MyMutex".
// 命名mutex是一個系統對象,其生命週期
// 由其所代表的Mutex 對象的生命週期所確定。
// 當第一個進程創建其局部Mutex時創建命名mutex。
// 本例中,命名mutex屬於第一個進程。
// 當銷燬所有Mutex對象時,釋放此命名mutex
using System;
using System.Threading;
namespace CSharpPractice.Threading
{
public class Test
{
public static void Main()
{
// 創建命名mutex。只能存在一個名爲"MyMutex"的系統對象。
Mutex m = new Mutex(false, "MyMutex");
// 試圖獲取對命名mutex的控制權。
// 如果命名mutex被另一個線程所控制,則等待直至其被釋放
Console.WriteLine("等待Mutex. . . . . .");
m.WaitOne();
// 保持對mutex的控制,直至用戶按ENTER鍵.
Console.WriteLine("本應用擁有mutex。請按ENTER鍵釋放之並退出!");
Console.ReadLine();
m.ReleaseMutex();
}
}
}
五、線程池
線程池是可以用來在後臺執行多個任務的線程集合,這使主線程可以自由地異步執行其他任務。線程池通常用於服務器應用程序。每個傳入請求都將分配給線程池中的一個線程,因此可以異步處理請求,而不會佔用主線程,也不會延遲後續請求的處理。
一旦線程池中的某個線程完成任務,它將返回到等待線程隊列中,等待被再次使用。這種重用使應用程序可以避免爲每個任務創建新線程的開銷。
using System;
using System.Threading;
namespace CSharpPractice.Threading
{
public class Fibonacci
{
public Fibonacci(int n, ManualResetEvent doneEvent)
{
_n = n;
_doneEvent = doneEvent;
}
// 線程池回調:計算 Fibonacci 結果.
public void ThreadPoolCallback(Object threadContext)
{
int threadIndex = (int)threadContext;
Console.WriteLine("線程 {0} 開始......", threadIndex);
_fibOfN = Calculate(_n);
Console.WriteLine("線程 {0} 結果計算......", threadIndex);
_doneEvent.Set();
}
// 遞歸方法計算第N個Fibonacci數:F (n ) = F (n - 1) + F(n - 2).
public int Calculate(int n)
{
if (n <= 1) // F(0)=0; F(1)=1
{
return n;
}
return Calculate(n - 1) + Calculate(n - 2);
}
public int N { get { return _n; } }
private int _n;
public int FibOfN { get { return _fibOfN; } }
private int _fibOfN;
private ManualResetEvent _doneEvent;
}
public class ThreadPoolExample // 線程池
{
static void Main()
{
const int FibonacciCalculations = 5; // 5個Fibonacci數
// 每個事件用於每個Fibonacci對象
ManualResetEvent[] doneEvents = new ManualResetEvent[FibonacciCalculations];
Fibonacci[] fibArray = new Fibonacci[FibonacciCalculations];
Random r = new Random();
// 使用ThreadPool配置和啓動線程:
Console.WriteLine("啓動 {0} 個任務......", FibonacciCalculations);
for (int i = 0; i < FibonacciCalculations; i++)
{
doneEvents[i] = new ManualResetEvent(false);
Fibonacci f = new Fibonacci(r.Next(1, 10), doneEvents[i]);
fibArray[i] = f;
ThreadPool.QueueUserWorkItem(f.ThreadPoolCallback, i);
}
// 等待線程池中的所有線程的計算...
WaitHandle.WaitAll(doneEvents);
Console.WriteLine("完成所有計算!");
// 顯示結果:第1項到第10項之間的5個Fibonacci數...
Console.WriteLine("第1項[F(0)]到第10項[F(9)]之間的任意5個Fibonacci數爲----");
for (int i = 0; i < FibonacciCalculations; i++)
{
Fibonacci f = fibArray[i];
Console.WriteLine("Fibonacci({0}) = {1}", f.N, f.FibOfN);
}
Console.ReadLine();
}
}
}
------- Windows Phone 7手機開發、.Net培訓、期待與您交流! -------