初探.net framework 下的異步多線程
目錄
1、多線程的出現條件
2、Thread和ThreadPool的相關Api及用法
3、Task和Parallel的相關Api及用法
4、Async&&Await
多線程的出現條件
- 用戶在執行一個操作的時候,可以同時的執行一些其他操作。(例如在寫入一個文件的時候,可以同時推送一條信息;還有一種情況,就是例如在編寫Winform代碼時候,提交一個比較費時的操作,這時候會造成UI界面假死,此時就可以把這個費事的操作交給一個子線程來完成,亦或者方法的異步調用。)
- 我們的CPU是高速的,分時間片執行的,操作系統將其封裝為一個個的線程,多個線程運行於一個進程之中,在我們的.net framework框架,將操作系統級別的線程 做了再次封裝,就是我們所了解到的Thread類。
- 這里我們再談下多線程和異步的區別。在我們編寫C#代碼的時候,異步方法是這么調用的。
在啟用BeginInvoke方法時候,action異步調用。此時可以看到,回掉函數由一個Id為3的線程來執行的。在.net framework中,異步就是由主線程開啟一個子線程來完成回調任務。
Console.WriteLine("***************委托的異步調用***************");
Console.WriteLine($"this is main_{Thread.CurrentThread.ManagedThreadId}");
Action<string> act = t => { Console.WriteLine(t); };
IAsyncResult result = null;
AsyncCallback asyncCallback = t =>
{
Console.WriteLine($"{string.ReferenceEquals(t, result)}");
Console.WriteLine($"this is asynccallback{Thread.CurrentThread.ManagedThreadId}");
};
result = act.BeginInvoke("ssss", asyncCallback, null);
act.EndInvoke(result);
Console.Read();
Thread和ThreadPool的相關Api及用法
- Thread 是.net framework 1.X版本的類(沒記錯的話),Thread類接受一個ThreadStart的委托,這個委托沒有參數,沒有返回值。我們這里定義一個耗時的測試方法(下文中都用這個方法代表一些費事的邏輯操作。)寫一個測試方法,創建五個線程分別執行這個方法。
private static void DoSomethingLong(string name)
{
Console.WriteLine($"*************DoSomethingLong Start {Thread.CurrentThread.ManagedThreadId}*************");
long Result = 0;
for(int i = 0; i < 1000000000; i++)
{
Result += i;
}
Console.WriteLine("*************DoSomethingLong Start {Thread.CurrentThread.ManagedThreadId}*************");
}
private void ThreadTest()
{
for(int i = 0; i < 5; i++)
{
Thread thread = new Thread(() => { DoSomethingLong(""); });
thread.Start();
}
}
private void ThreadCallBackTest() //這執行下
{
Thread thread = new Thread(
() => ThreadCallBack(() => { Console.WriteLine("這是Thread接收的threadStart"); },
() => Console.WriteLine("這是回調函數")));
thread.Start();
}
private void ThreadCallBack(ThreadStart threadStart,Action act) //這里包裝一層,Thread的回調方法放入Action參數中
{
ThreadStart start = new ThreadStart(() => //ThreadStart 本身是一個無參數無返回值的委托, 將ThreadStart和Action 都執行下
{
threadStart.Invoke(); //這里相當於把原來的ThreadStart(()=>{} )包了一層,里面又一個ThreadStart(()=>{} ),需要體會下
act.Invoke();
});
Thread thread = new Thread(start); //啟用一個線程執行。
thread.Start();
}

- 使用Thread寫一個帶返回值的委托。
private void ThreadReturnParTest()
{
Thread thread = new Thread(() =>
{
var e = ThreadReturnPar<string>(() => { return "ssss"; });
var s = e.Invoke();
Console.WriteLine(s);
});
thread.Start();
}
private Func<T> ThreadReturnPar<T>(Func<T> func)
{
T t = default(T);
ThreadStart start = new ThreadStart(() =>
{
t = func.Invoke();
});
Thread thread = new Thread(start);
thread.Start();
return () =>
{
while (thread.ThreadState != System.Threading.ThreadState.Stopped)
{
thread.Join();
}
return t;
};
}

- ThreadPool是.net framework 2.X版本的類(沒記錯的話),線程池對線程做一個池化的管理(對應設計模式為享元模式),使用ThreadPool時候不再由.netframework 框架從操作系統層面創建一個新的線程,而是由ThreadPool統一管理,我們向ThreadPool申請一個線程,使用完了以后把這個線程資源歸還給ThreadPool。
- ThreadPool的QueueUserWorkItem方法接收一個WaitCallback委托,這個委托的參數就是QueueUserWorkItem的第二個參數。這里還要介紹一個對象,ManualResetEvent對象,可以把這個對象理解為一把鎖,這個鎖有個初始狀態,ManualResetEvent對象的Reset方法阻塞線程,Set方法使阻塞線程繼續運行。
private void ThreadPoolTest()
{
Console.WriteLine($"ThreadPoolTest start {DateTime.Now.Millisecond}");
ManualResetEvent mre = new ManualResetEvent(true);
mre.Reset();
ThreadPool.QueueUserWorkItem(
new WaitCallback(
t => {
Console.WriteLine($"this is t {t}");
Thread.Sleep(1000);
Console.WriteLine("this is threadpool queue waitcallback");
mre.Set();
}), "lmc");
mre.WaitOne();
Console.WriteLine($"ThreadPoolTest End {DateTime.Now.Millisecond}");
}
Task和Parallel的相關Api及用法
- Task類基於.net framework3.5, Task基於線程池,Task創建方式由兩種,Task 可以基於工廠創建;也可以用new Task創建 ;相關Api WaitAll 等待所有Task全部完成了再執行; WaitAny 等待一個Task執行完了之后再執行后面的; (以上兩個API會卡主線程); Task.Factory.ContinueWhenAll 以回調形式等待所有任務完成后執行一個委托。Task.Factory.ContinueWhenAny以回調形式等待任意一個任務完成后執行一個委托。四個Task的等待執行的Api需要記好。
private void TaskTest()
{
List<Task> tasklist = new List<Task>();
Console.WriteLine($"TaskTest start_{Thread.CurrentThread.ManagedThreadId} {DateTime.Now.Millisecond}");
for (int i = 0; i < 5; i++)
{
string name = $"lmc_{i}";
Task task = Task.Factory.StartNew(() =>
{
Thread.Sleep(1000);
Console.WriteLine($"{name}_{Thread.CurrentThread.ManagedThreadId} " );
});
tasklist.Add(task);
}
Console.WriteLine("before waitall");
Task.WaitAll(tasklist.ToArray());
Console.WriteLine("after waitall");
Console.WriteLine("before waitany");
Task.WaitAny(tasklist.ToArray());
Console.WriteLine("after waitany");
Task.Factory.ContinueWhenAll(tasklist.ToArray(),
tlist => { Console.WriteLine($"ContinueWhenAll_{tlist.Count()}_{Thread.CurrentThread.ManagedThreadId}");
Console.WriteLine($"ContinueWhenAll_{Thread.CurrentThread.ManagedThreadId}"); });
Task.Factory.ContinueWhenAny(tasklist.ToArray(),
t => { Console.WriteLine($"ContinueWhenAny_{t.Id}_{Thread.CurrentThread.ManagedThreadId}");
Console.WriteLine($"ContinueWhenAny_{Thread.CurrentThread.ManagedThreadId}"); });
Console.WriteLine($"TaskTest End_{Thread.CurrentThread.ManagedThreadId} {DateTime.Now.Millisecond}");
}

- Parallel並行任務,主線程CPU參與計算,Invoke方法接受一個委托的數組。Parallel的另外兩個常用的方法是For和Foreach,兩個方法類似,這里拿For來舉個例子。(仔細觀察下就像是Task.WaitAll)
private void ParallelTest()
{
Console.WriteLine($"ParallelTest start_{Thread.CurrentThread.ManagedThreadId} {DateTime.Now.Millisecond}");
Parallel.Invoke(
() => { Console.WriteLine($"ParallelTest_1_{Thread.CurrentThread.ManagedThreadId}"); },
() => { Console.WriteLine($"ParallelTest_2_{Thread.CurrentThread.ManagedThreadId}"); },
() => { Console.WriteLine($"ParallelTest_3_{Thread.CurrentThread.ManagedThreadId}"); },
() => { Console.WriteLine($"ParallelTest_4_{Thread.CurrentThread.ManagedThreadId}"); }
);
ParallelOptions parallelOptions = new ParallelOptions();
parallelOptions.MaxDegreeOfParallelism = 3;
Parallel.For(1, 10, parallelOptions,(t,state) =>
{
Thread.Sleep(1000);
Console.WriteLine($"ParallelTest_{t}_{Thread.CurrentThread.ManagedThreadId}_{DateTime.Now.Millisecond}");
state.Stop();
return;
});
Console.WriteLine($"ParallelTest end_{Thread.CurrentThread.ManagedThreadId} {DateTime.Now.Millisecond}");
}
Async&&Await
- 在.netframework 4.5 出來以后,我們經常能看到,async和await兩個關鍵字,代表方法的異步執行。使用await關鍵字必須要在async關鍵字修飾的方法下。主線程遇到await關鍵字就立即返回,把剩下的任務交由一個子線程來回調完成。這個過程就像是Task.Factory.ContinueWhenAny,參數就是await 后面的代碼組成的一個委托。
static void Main(string[] args)
{
Program prm = new Program();
Console.WriteLine("1");
prm.AsyncTest();
Console.WriteLine("2");
}
private async Task AsyncTest()
{
Console.WriteLine($"this is async task Main Start_{Thread.CurrentThread.ManagedThreadId}_{DateTime.Now.Millisecond}");
await Task.Factory.StartNew(() =>
{
Thread.Sleep(3000);
Console.WriteLine($"this is async task Son Start__{Thread.CurrentThread.ManagedThreadId}_{DateTime.Now.Millisecond}");
Console.WriteLine($"this is async task Son End_{Thread.CurrentThread.ManagedThreadId}_{DateTime.Now.Millisecond}");
});
Console.WriteLine($"this is async task Main End_{Thread.CurrentThread.ManagedThreadId}_{DateTime.Now.Millisecond}");
}

- 最后讓我們來看下處理多線程的異常。多線程運行中,子線程執行的任務拋出的異常不會主動影響主線程是其停止,最好的方法就是使用CancellationTokenSource對象。一個線程執行時候,首先判斷下CancellationTokenSource對象的標識、假如異常,取消執行。一個線程的終止或異常,由它自身來完成。
private void TaskInteractive()
{
try
{
CancellationTokenSource cts = new CancellationTokenSource();
TaskFactory taskFactory = new TaskFactory();
List<Task> tasklist = new List<Task>();
for (int i = 0; i < 40; i++)
{
string name = $"this is {i}";
Action<object> act = t =>
{
try
{
Thread.Sleep(500);
if (!cts.IsCancellationRequested)
{
if (t.ToString().Equals("this is 11"))
{
cts.Cancel();
throw new Exception($"{t}執行失敗");
}
if (t.ToString().Equals("this is 12"))
{
cts.Cancel();
throw new Exception($"{t}執行失敗");
}
Console.WriteLine($"{t}執行成功");
}
else
{
Console.WriteLine($"{t}********執行放棄********");
cts.Token.ThrowIfCancellationRequested();
}
}
catch (Exception ex)
{
Console.WriteLine($"this is logging {ex.Message}");
}
};
tasklist.Add(taskFactory.StartNew(act, name,cts.Token));
}
Task.WaitAll(tasklist.ToArray());
}
catch (AggregateException aex)
{
foreach (var ex in aex.InnerExceptions)
{
Console.WriteLine(ex.Message);
}
}
}