(一)概述 所有需要等待的操作,例如,因為文件、資料庫或網路訪問都需要一定的時間,此時就可以啟動一個新的線程,同時完成其他任務。 線程是程式中獨立的指令流。 (二)Paraller類 Paraller類是對線程的一個很好的抽象,該類位於System.Threading.Tasks名稱空間中,提供了數 ...
(一)概述
所有需要等待的操作,例如,因為文件、資料庫或網路訪問都需要一定的時間,此時就可以啟動一個新的線程,同時完成其他任務。
線程是程式中獨立的指令流。
(二)Paraller類
Paraller類是對線程的一個很好的抽象,該類位於System.Threading.Tasks名稱空間中,提供了數據和任務並行性。
Paraller.For()和Paraller.ForEach()方法在每次迭代中調用相同的代碼,二Parallel.Invoke()方法允許同時調用不同的方法。Paraller.Invoke用於任務並行性,而Parallel.ForEach用於數據並行性。
1、Parallel.For()方法迴圈
ParallelLoopResult result = Parallel.For(0, 10, i => { Console.WriteLine("當前迭代順序:" + i); Thread.Sleep(10);//線程等待 });
在For()方法中,前兩個參數定義了迴圈的開頭和結束,第三個參數是一個Action<int>委托,參數是迴圈迭代的次數。
Parallel類只等待它創建的任務,而不等待其他後臺活動。
Parallel.For()方法可以提前停止:
var result = Parallel.For(10, 40, async (int i, ParallelLoopState pls) => { Console.WriteLine("迭代序號:{0}, 任務: {1}, 線程: {2}", i, Task.CurrentId, Thread.CurrentThread.ManagedThreadId); await Task.Delay(10); if (i > 15) { pls.Break(); } }); Console.WriteLine("迴圈完成狀態:" + result.IsCompleted); Console.WriteLine("Break索引:" + result.LowestBreakIteration);
需要註意的是,Break()方法僅是告知迴圈在合適的時候退出當前迭代之外的迭代。
Parallel.For()還可以對線程進行初始化和退出時制定方法:
Parallel.For<string>(10,25,()=> { Console.WriteLine("初始線程{0},任務{1}",Thread.CurrentThread.ManagedThreadId,Task.CurrentId); return string.Format("線程Id"+ Thread.CurrentThread.ManagedThreadId); }, (i,pls,str1)=> { Console.WriteLine("迭代順序:【{0}】,線程初始化返回值:【{1}】,線程Id:【{2}】,任務Id:【{3}】",i,str1, Thread.CurrentThread.ManagedThreadId, Task.CurrentId); Thread.Sleep(10); return string.Format("迭代順序:"+i); }, (str1)=> { Console.WriteLine("線程主體返回值:{0}",str1); });
除了迴圈開頭與結束的指定,第三個是對迭代調用的每個線程進行處理,第四個是迭代的方法主體,第四個是迭代完成時對線程的處理。
2、使用Paralle.ForEach()方法迴圈
string[] data = { "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k" }; Parallel.ForEach(data, (s, pls, l) => { Console.WriteLine(s + " " + l);//s是當前迴圈的項的值,pls是ParallelLoopState類型,l是當前迭代的順序 });
3、通過Parallel.Invoke()方法調用多個方法
Parallel.Invoke()方法運行傳遞一個Action委托數組,在其中可以指定需要並行運行的方法。
1 static void Main(string[] args) 2 { 3 Parallel.Invoke(Say1, Say2, Say3, Say4, Say5); 4 Console.WriteLine("---------"); 5 Say1(); 6 Say2(); 7 Say3(); 8 Say4(); 9 Say5(); 10 11 Console.ReadKey(); 12 } 13 static void Say1() 14 { 15 Thread.Sleep(100); 16 Console.WriteLine(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss ffff") + "1"); 17 } 18 static void Say2() 19 { 20 Thread.Sleep(100); 21 Console.WriteLine(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss ffff") + "2"); 22 } 23 static void Say3() 24 { 25 Thread.Sleep(100); 26 Console.WriteLine(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss ffff") + "3"); 27 } 28 static void Say4() 29 { 30 Thread.Sleep(100); 31 Console.WriteLine(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss ffff") + "4"); 32 } 33 static void Say5() 34 { 35 Thread.Sleep(100); 36 Console.WriteLine(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss ffff") + "5"); 37 }
(三)任務
為了更好的控制並行動作,可以使用System.Threading.Tasks名稱空間中的Task類。
1、啟動任務
(1)使用線程池的任務
1 private static readonly object locker = new object(); 2 static void Main(string[] args) 3 { 4 var tf = new TaskFactory(); 5 Task t1 = tf.StartNew(TaskMethod, "使用TaskFactory"); 6 7 Task t2 = Task.Factory.StartNew(TaskMethod, "使用Task.Factory"); 8 9 var t3 = new Task(TaskMethod, "使用Task構造函數並啟動"); 10 t3.Start(); 11 12 Task t4 = Task.Run(() => { TaskMethod("運行"); }); 13 14 Console.ReadKey(); 15 } 16 static void TaskMethod(object title) 17 { 18 lock (locker) 19 { 20 Console.WriteLine(title); 21 Console.WriteLine("任務Id:{0},線程Id:{1}", Task.CurrentId == null ? "no Task" : Task.CurrentId.ToString(), Thread.CurrentThread.ManagedThreadId); 22 Console.WriteLine("是否為線程池線程:{0}", Thread.CurrentThread.IsThreadPoolThread); 23 Console.WriteLine("是否為後臺線程:{0}",Thread.CurrentThread.IsBackground); 24 Console.WriteLine(); 25 } 26 }
(2)同步任務
任務不一定要使用線程池中的線程,也可以使用其他線程。
TaskMethod("主線程調用"); var t1 = new Task(TaskMethod,"同步運行"); t1.RunSynchronously();
(3)使用單獨線程的任務
如果任務的代碼應該長時間運行,就應該使用TaskCreationOptions.LongRunning告訴任務調度器創建一個新線程,而不是使用線程池中的線程。
var t1 = new Task(TaskMethod, "長時間運行任務", TaskCreationOptions.LongRunning); t1.Start();
2、Future——任務的結果
任務結束時,它可以把一些有用的狀態信息寫到共用對象中,這個共用對象必須是線程安全的。另一個選項是使用返回某個結果的任務,如Future它是Task類的一個泛型版本,使用這個類時,可以定義任務返回的結果的類型。
var t1 = new Task<Tuple<int, int>>(TaskWithResult, Tuple.Create<int, int>(10, 5)); t1.Start(); Console.WriteLine(t1.Result); t1.Wait(); Console.WriteLine("任務結果:{0} {1}",t1.Result.Item1, t1.Result.Item2);
3、連續的任務
通過任務,可以指定在任務完成後,應開始運行另一個特定任務。
Task t1 = new Task(DoOnFirst); t1.Start(); Task t2 = t1.ContinueWith(DoOnSecond); Task t3 = t1.ContinueWith(DoOnSecond); Task t4 = t2.ContinueWith(DoOnSecond); Task t5 = t3.ContinueWith(DoOnSecond, TaskContinuationOptions.OnlyOnFaulted);//第二個參數指t3在失敗的情況下運行t5
4、任務層次結構
任務也可以構成一個層次結構。一個任務啟動一個新任務時,就啟動了一個父/子層次結構。
1 static void Main(string[] args) 2 { 3 var parent = new Task(ParentTask); 4 parent.Start(); 5 Thread.Sleep(2000); 6 Console.WriteLine(parent.Status); 7 Thread.Sleep(4000); 8 Console.WriteLine(parent.Status); 9 Console.ReadKey(); 10 } 11 static void ParentTask() 12 { 13 Console.WriteLine("任務Id:"+Task.CurrentId); 14 var child = new Task(ChildTask); 15 child.Start(); 16 Thread.Sleep(1000); 17 Console.WriteLine("父級子任務已開始運行"); 18 } 19 static void ChildTask() 20 { 21 Console.WriteLine("子任務開始"); 22 Thread.Sleep(5000); 23 Console.WriteLine("子任務結束"); 24 }
(四)取消架構
.NET4.5包含一個取消架構,允許以標準方式取消長時間運行的任務。取消架構基於協作行為,它不是強制的。長時間運行的任務會檢查它是否被取消,並返回控制權。支持取消的方法接受一個CancellationToken參考。
1、Parallel.For()方法的取消
1 var cts = new CancellationTokenSource(); 2 cts.Token.Register(() => Console.WriteLine("*** token canceled")); 3 4 5 //在500毫秒以後發送取消指令 6 cts.CancelAfter(500); 7 try 8 { 9 var result = Parallel.For(0, 100, new ParallelOptions() { CancellationToken = cts.Token, }, x => 10 { 11 Console.WriteLine("{0}次迴圈開始", x) 12 int sum = 0; 13 for (int i = 0; i < 100; i++) 14 { 15 Thread.Sleep(2); 16 sum += i; 17 } 18 Console.WriteLine("{0}次迴圈結束", x); 19 }); 20 } 21 catch (OperationCanceledException ex) 22 { 23 Console.WriteLine(ex.Message); 24 }
使用.NET4.5中的一個新方法CancelAfter,在500毫秒後取消標記。在For()迴圈的實現代碼內部,Parallel類驗證CanceledToken的結果,並取消操作。一旦取消操作,For()方法就拋出一個OperationCanceledException類型的異常。
2、任務的取消
同樣的取消模式也可以用於任務。
1 var cts = new CancellationTokenSource(); 2 cts.Token.Register(() => Console.WriteLine("*** token canceled")); 3 4 //在500毫秒以後發送取消指令 5 cts.CancelAfter(500); 6 7 Task t1 = Task.Run(()=> { 8 Console.WriteLine("任務進行中..."); 9 for (int i = 0; i < 20; i++) 10 { 11 Thread.Sleep(100); 12 CancellationToken token = cts.Token; 13 if (token.IsCancellationRequested) 14 { 15 Console.WriteLine("已發送取消請求,取消請求來自當前任務"); 16 token.ThrowIfCancellationRequested(); 17 break; 18 } 19 Console.WriteLine("迴圈中..."); 20 } 21 Console.WriteLine("任務結束沒有取消"); 22 }); 23 try 24 { 25 t1.Wait(); 26 } 27 catch (AggregateException ex) 28 { 29 Console.WriteLine("異常:{0}, {1}",ex.GetType().Name,ex.Message); 30 foreach (var innerException in ex.InnerExceptions) 31 { 32 Console.WriteLine("異常:{0}, {1}", ex.InnerException.GetType().Name, ex.InnerException.Message); 33 } 34 }
(五)線程池
int nWorkerThreads; int nCompletionPortThreads; ThreadPool.GetMaxThreads(out nWorkerThreads, out nCompletionPortThreads); Console.WriteLine("線程池中輔助線程的最大數目:{0}, 線程池中非同步 I/O 線程的最大數目:{1}",nWorkerThreads,nCompletionPortThreads); for (int i = 0; i < 5; i++) { ThreadPool.QueueUserWorkItem(JobForAThread); } Thread.Sleep(3000);
需要註意的是:線程池中的所有線程都是後臺線程,不可設置線程優先順序、名稱,隨著前臺線程的結束而結束,只適用於短時間任務。
(六)Thread類
該類允許創建前臺線程,以及設置線程的優先順序。
1、給線程傳遞數據
static void Main(string[] args) { var t2 = new Thread(ThreadMainWithParameter); t2.Start("參數字元串"); Console.ReadKey(); } static void ThreadMainWithParameter(object message) { Console.WriteLine("運行主線程,接受參數:" + message.ToString()); }
如果使用了ParameterizedThreadStart委托,線程的入口點必須有一個object類型的參數,且返回類型為void。
2、後臺線程
只要有一個前臺線程在運行,應用程式的進程就在運行。如果多個前臺線程在運行,而Main()方法結束了,應用程式的進程就仍然是激活的,直到所有前臺線程完成其任務為止。
預設情況下,用Thread類創建的線程是前臺線程,線程池中的線程是後臺線程。Thread類創建線程時,可以設置IsBackground屬性來確定創建前臺還是後臺線程。
static void Main(string[] args) { var t1 = new Thread(ThreadMain) { Name = "MyNewThread", IsBackground = false }; t1.Start(); Console.WriteLine("主線程現在結束"); Console.ReadKey(); } private static void ThreadMain() { Console.WriteLine(Thread.CurrentThread.Name+"線程開始運行"); Thread.Sleep(3000); Console.WriteLine(Thread.CurrentThread.Name+"線程結束"); }
在通過Thread類創建線程的時候,設置IsBackground屬性為false,也就是創建一個前臺線程。這種情況下在主線程結束時,t1不會結束。但如果將IsBackground設置為true,則會隨著主線程的結束而結束。
3、線程的優先順序
給線程指定優先順序,就可以影響系統對線程的調度順序。在Thread類中,可以設置Priority屬性,以影響線程的基本優先順序。
4、控制線程
讀取Tread的ThreadState屬性,可以獲取線程的狀態。
Thread的Start()方法創建線程,線程狀態為UnStarted;
線程調度器選擇運行後,線程轉檯改變為Running;
Thread.Sleep()方法會使線程處於WaitSleepJoin;
Thread類的Abort()方法,觸發ThreadAbortException類型的異常,線程狀態會變為AbortedRequested,如果沒有重置終止則變為Aborted;
Thread.ResetAbort()方法可以讓線程在觸發ThreadAbortException異常後繼續運行;
Thread類的Join()會停止當前線程,等待加入的線程完成為止,此時線程狀態為WaitSleepJoin。
(七)線程問題
1、爭用條件
如果兩個或多個線程訪問相同的對象,並且對共用狀態的訪問沒有同步,就會出現爭用條件。要避免該問題,可以鎖定共用對象。這可以通過lock語句鎖定線上程中共用的state變數。
private static readonly object locker = new object(); public void ChnageI(int i) { lock (locker) { if (i == 0) { i++; Console.WriteLine(i == 1); } i = 0; } }
2、死鎖
由於兩個線程都在等待對方,就出現了死鎖,線程將無線等待下去。為了避免這個問題,可以在應用程式的體系架構中,從一開始就設計好鎖定的順序,也可以為鎖定定義超時時間。
(八)同步
共用數據必須使用同步技術,確保一次只有一個線程訪問和改變共用狀態。可以使用lock語句、Interlocked類和Monitor類進行進程內部的同步。Mutex類、Event類、SemaphoreSlim類和ReaderWriterLockSlim類提供了多個進程之間的線程同步。
1、lock語句和線程安全
lock語句是設置鎖定和解除鎖定的一種簡單方式。
在沒有使用lock語句的情況下,多個線程操作共用數據,最後得到的結果沒有一個會正確。
1 class Program 2 { 3 static void Main(string[] args) 4 { 5 for (int j = 0; j < 5; j++) 6 { 7 int numTasks = 20; 8 var state = new SharedState(); 9 var tasks = new Task[numTasks]; 10 for (int i = 0; i < numTasks; i++) 11 { 12 tasks[i] = Task.Run(() => { new Job(state).DoTheJob(); }); 13 } 14 15 for (int i = 0; i < numTasks; i++) 16 { 17 tasks[i].Wait(); 18 } 19 Console.WriteLine("最後結果:{0}", state.State); 20 } 21 Console.ReadKey(); 22 } 23 } 24 25 public class SharedState 26 { 27 public int State { get; set; } 28 } 29 30 public class Job 31 { 32 SharedState sharedState; 33 public Job(SharedState sharedState) 34 { 35 this.sharedState = sharedState; 36 } 37 public void DoTheJob() 38 { 39 for (int i = 0; i < 50000; i++) 40 { 41 sharedState.State += 1; 42 } 43 } 44 }
使用lock語句,修改DoTheJob()方法,現在才能獲得正確的結果。
private readonly object syncRoot = new object(); public void DoTheJob() { for (int i = 0; i < 50000; i++) { lock (syncRoot) { sharedState.State += 1; } } }
2、Interlocked類
Interlocked類用於使變數的簡單語句原子化。
public int State { get { lock (this) { return ++state; } } } public int State { get { return Interlocked.Increment(ref state); } }
使用Interlocked類可以更快。
3、Monitor類
lock語句由C#編譯器解析為使用Monitor類。
lock (syncRoot) { //代碼 } //C#編譯器將lock語句解析為 Monitor.Enter(syncRoot); try { //代碼 } finally { Monitor.Exit(syncRoot); }
Monitor類相對於lock語句的優點是:可以通過調用TryEnter()方法添加一個等待被鎖定的超時值。
bool lockTaken = false; Monitor.TryEnter(syncRoot, 1000, ref lockTaken); if (lockTaken) { //獲取鎖後操作 try { //代碼 } finally { Monitor.Exit(syncRoot); } } else { //沒有獲取鎖的操作 }
4、SpinLock結構
相對於Monitor垃圾回收導致過高的系統開銷,使用SpinLock結構就能有效降低系統開銷。SpinLock的使用方式與Monitor非常相似,但因為SpinLock是結構所以在把變數賦值為另一個變數會創建一個副本。
5、WaitHandle基類
WaitHandle是一個抽象基類,用於等待一個信號的設置。可以等待不同的信號,因為WaitHandle是一個基類,可以從中派生一些類。
6、Mutex類
Mutex(mutual exclusion,互斥)是.NET Framework中提供跨多個線程同步訪問的一個類。
在Mutex類的構造函數中,可以指定互斥是否最初由主調線程擁有,定義互斥的名稱,獲得互斥是否存在的信息。
bool createdNew; var mutex = new Mutex(false, "MyMutex", out createdNew);
系統可以識別有名稱的互斥,可以使用它來禁止應用程式啟動兩次。
bool createdNew; var mutex = new Mutex(false, "MyMutex", out createdNew); if (!createdNew) { Console.WriteLine("每次只能啟動一個應用程式"); Environment.Exit(0); } Console.WriteLine("運行中...");
7、Semaphore類
信號量是一種計數的互斥鎖。如果需要限制可以訪問可用資源的線程數,信號量就很有用。
.NET4.5為信號量功能提供了兩個類Semaphore和SemaphoreSlim。Semaphore類可以命名,使用系統範圍內的資源,允許在不同進程之間同步。SemaphoreSlim類是對較短等待時間進行了優化的輕型版本。
1 static void Main(string[] args) 2 { 3 int taskCount = 6; 4 int semaphoreCount = 3; 5 var semaphore = new SemaphoreSlim(semaphoreCount, semaphoreCount); 6 var tasks = new Task[taskCount]; 7 8 9 for (int i = 0; i < taskCount; i++) 10 { 11 tasks[i] = Task.Run(() => 12 { 13 TaskMain(semaphore); 14 }); 15 } 16 17 Task.WaitAll(tasks); 18 19 Console.WriteLine("所有任務已結束"); 20 Console.ReadKey(); 21 } 22 23 24 private static void TaskMain(SemaphoreSlim semaphore) 25 { 26 bool isCompleted = false; 27 while (!isCompleted) 28 { 29 if (semaphore.Wait(600)) 30 { 31 try 32 { 33 Console.WriteLine("任務{0}鎖定了信號", Task.CurrentId); 34 Thread.Sleep(2000); 35 } 36 finally 37 { 38 Console.WriteLine("任務{0}釋放了信號", Task.CurrentId); 39 semaphore.Release(); 40 isCompleted = true; 41 } 42 } 43 else 44 { 45 Console.WriteLine("任務{0}超時,等待再次執行", Task.CurrentId); 46 } 47 } 48 }
8、Events類
與互斥和信號量對象一樣,事件也是一個系統範圍內的資源同步方法。為了從托管代碼中使用系統事件,.NET Framework在System.Threading名稱空間中提供了ManualResetEvent、AutoResetEvent、ManualResetEventSlim和CountdownEvent類。
C#中event關鍵字與這裡的event類沒有任何關係。
9、Barrier類
對於同步,Barrier類非常適用於其中