1. 程式人生 > >12 非同步多執行緒(二)Thread,ThreadPool,Task

12 非同步多執行緒(二)Thread,ThreadPool,Task

一.Thread

1.Thread 是framework1.0時候就存在的,可以用TreadStart來啟動多執行緒。

Stopwatch watch = new Stopwatch();//計時器
watch.Start();
Console.WriteLine($"*******btnThreads_Click Start {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")} *********");
ThreadStart threadStart = new ThreadStart(()=>this.DoSomethingLong("btnThreads_Click"));

Thread thread = new Thread(threadStart);
thread.Start();
watch.Stop();
Console.WriteLine($"*****btnThreads_Click End {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}*****");

private void DoSomethingLong(string name)
{
    Console.WriteLine($"****************DoSomethingLong Start {name} {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***************");
    long lResult = 0;
    for (int i = 0; i < 1000000000; i++)
    {    
        lResult += i;
    }
     Console.WriteLine($"****************DoSomethingLong   End {name} {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")} {lResult}***************");
}

執行結果是:

這裡插一句委託用lambda表示式的寫法:

//這裡為什麼要寫成 Action<string>,因為this.DoSomethingLong方法是有string引數的,Action要和它配套使用,Action的引數是in,表示傳入型別,所以寫成Action<string>
Action<string> action = this.DoSomethingLong;

還有這一句:

// ThreadStart是無參無返回值的,但是this.DoSomethingLong是需要傳一個string引數,這裡就有衝突了,要包一層
//包成()=>this.DoSomethingLong("btnThreads_Click"),就成了無參無返回值的委託
ThreadStart threadStart = new ThreadStart(()=>this.DoSomethingLong("btnThreads_Click"));

Thread還有一些其他的方法,比如:

thread.Suspend();//已經拋棄
thread.Resume();//已經拋棄
thread.Join();//做等待,現在用Thread時,也就這個方法靠譜了
thread.Abort();//已經拋棄

但是除去Join()方法,其他方法不靠譜,因為執行緒是作業系統分配的,也歸作業系統管理,這些方法不一定真正被執行了。

thread.Join()的效果:

介紹一個概念,前臺執行緒和後臺執行緒。

thread.Start();//預設是前臺執行緒,UI執行緒退出後,還會繼續執行完;後臺執行緒就直接退出了,只有Thread有前臺執行緒,其他Task、 Parallel都沒有。加上thread.IsBackground = true,變成後臺執行緒。

2.既然Thread是framwork1.0時代的,那時候還沒有呼叫回撥函式callback方法,所以需要自己寫。

private void ThreadWithCallback(ThreadStart threadStart,Action callback)
{
    ThreadStart startNew = new ThreadStart(
        () =>
        {
            threadStart.Invoke();
            callback.Invoke();
        });
        Thread thread = new Thread(startNew);
        thread.Start();
}

這樣寫,等於是threadStart.Invoke()執行完以後,再執行callback.Invoke(),它們在同一執行緒內是順序執行的,達到了回撥目的。

而測試程式碼可以這樣寫:

Stopwatch watch = new Stopwatch();
watch.Start();
Console.WriteLine($"****************btnThreads_Click Start {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***************");

this.ThreadWithCallback(()=>
	{
		Thread.Sleep(2000);
		Console.WriteLine($"這裡是ThreadStart {Thread.CurrentThread.ManagedThreadId.ToString("00")}");
	}
	,()=>
	{
		Thread.Sleep(2000);
		Console.WriteLine($"這裡是callback {Thread.CurrentThread.ManagedThreadId.ToString("00")}");
	}
);

watch.Stop();
Console.WriteLine($"****************btnThreads_Click End   {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}  耗時{watch.ElapsedMilliseconds}ms***************");

因為ThreadWithCallback方法的兩個引數:ThreadStart 和 Action都是委託,所以可以這樣寫。

執行結果:

同樣Thread是framwork1.0時代的,那時候還沒有得到返回值的EndInvoke方法,所以需要自己寫。

private Func<T> ThreadWithReturn<T>(Func<T> funcT)
{
	T t = default(T);
        //只有將funcT包到子執行緒裡面,才不會卡介面
	ThreadStart startNew = new ThreadStart(
		()=>
		{
			t = funcT.Invoke();
		});
		Thread thread = new Thread(startNew);
		thread.Start();
		
		return new Func<T>(()=>
		{
                        //這裡既然要拿到返回值,就要等待執行緒計算完,所以要Join
			thread.Join();
			return t;
		});
}

測試程式碼:

Stopwatch watch = new Stopwatch();
watch.Start();
Console.WriteLine($"****************btnThreads_Click Start {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***************");

Func<int> func = this.ThreadWithReturn(() =>//begininvoke
{
	Thread.Sleep(2000);
	Console.WriteLine($"這裡是ThreadStart {Thread.CurrentThread.ManagedThreadId.ToString("00")}");
	return 12345;
});

Console.WriteLine("已經執行到這裡了。。。");
int iResult = func.Invoke();//endinvoke

watch.Stop();
Console.WriteLine($"****************btnThreads_Click End iResult={iResult},  {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}  耗時{watch.ElapsedMilliseconds}ms***************");
        

執行結果:

二.ThreadPool

ThreadPool是在framework2.0出現的,它避免了Thread給使用者的權利太強大,做了限制,只有一個QueueUserWorkItem方法來啟動執行緒。例如:

ThreadPool.QueueUserWorkItem(o =>
{
	Thread.Sleep(5000);
	this.DoSomethingLong("btnThreads_Click1");
});

ThreadPool的好處是:

1 去掉各種 api 避免濫用,降低複雜度

2 池化:1)減少建立/銷燬的成本 2 )限制最大執行緒數量

什麼是池化?池化就是執行緒池一開始就向作業系統申請了一些程序,準備好了放線上程池中。當程式需要程序時,向執行緒池要,用完之後,自己也不用銷燬,直接送還給執行緒池,執行緒池的操作是framework底層做的,不需要人工干預,所以提供給使用者呼叫的方法很少。

ManualResetEvent 它是一個訊號值,可以像WaitOne搭配Thread一樣,搭配ThreadPool使用。

ManualResetEvent mre = new ManualResetEvent(false);//false 關閉
new Action(() =>
     {
        Thread.Sleep(5000);
        Console.WriteLine("委託的非同步呼叫");
        mre.Set();//開啟
     }).BeginInvoke(null, null);

//只有上面子執行緒執行完,mrc.Set()重新開啟,才可以執行下面的方法
mre.WaitOne();
Console.WriteLine("12345");
mre.Reset();//關閉
new Action(() =>
{
    Thread.Sleep(5000);
    Console.WriteLine("委託的非同步呼叫2");
    mre.Set();//開啟
}).BeginInvoke(null, null);
mre.WaitOne();
Console.WriteLine("23456");         

但是,如果沒有這個需求,就不要等待,阻塞執行緒,很容易死鎖。最好用回撥。

三.Task

目前最推薦的就是Task,它是framework3.0時候出來的,API的功能很強大。執行緒都來自執行緒池,所以都是後臺執行緒。

1.WaitAll和WaitAny:這兩者是等待。

WaitAll:主執行緒等待所有子執行緒執行完才返回,所以會卡介面

WaitAny:某個子執行緒執行完就返回。比如要獲取資料,可以從介面,可以從資料庫,可以從檔案,那麼,只要從一個途徑拿到了資料,就返回,其他途徑的結果可以不要了,類似於這種只需要一個結果的場景,就可以用WaitAny。

TaskFactory taskFactory = Task.Factory;// new TaskFactory();
都說List<Task> taskList = new List<Task>();
taskList.Add(taskFactory.StartNew(() => this.DoSomethingLong("btnTask_Click_002")));
taskList.Add(taskFactory.StartNew(() => this.DoSomethingLong("btnTask_Click_001")));
taskList.Add(taskFactory.StartNew(() => this.DoSomethingLong("btnTask_Click_003")));
taskList.Add(taskFactory.StartNew(() => this.DoSomethingLong("btnTask_Click_004")));
taskList.Add(taskFactory.StartNew(() => this.DoSomethingLong("btnTask_Click_005")));

Task.WaitAny(taskList.ToArray());//卡介面
Console.WriteLine("某個任務都完成,才會執行");

Task.WaitAll(taskList.ToArray());//卡介面
Console.WriteLine("全部任務都完成,才會執行");

WaitAll和WaitAny,都是卡介面的,有沒有不卡介面的呢?

2.ContinueWhenAll 和 ContinueWhenAny :這兩者是回撥。

一個是等待子執行緒全部完成,一個是等待某一個子執行緒完成,然後再開一個新的子執行緒執行下面的操作,它們都是回撥。

TaskFactory taskFactory = Task.Factory;// new TaskFactory();
List<Task> taskList = new List<Task>();
taskList.Add(taskFactory.StartNew(() => this.DoSomethingLong("btnTask_Click_002")));
taskList.Add(taskFactory.StartNew(() => this.DoSomethingLong("btnTask_Click_001")));
taskList.Add(taskFactory.StartNew(() => this.DoSomethingLong("btnTask_Click_003")));
taskList.Add(taskFactory.StartNew(() => this.DoSomethingLong("btnTask_Click_004")));
taskList.Add(taskFactory.StartNew(() => this.DoSomethingLong("btnTask_Click_005")));
//回撥
taskFactory.ContinueWhenAny(taskList.ToArray(), t => Console.WriteLine($"ContinueWhenAny {Thread.CurrentThread.ManagedThreadId.ToString("00")}"));
taskFactory.ContinueWhenAll(taskList.ToArray(), tList => Console.WriteLine($"這裡是ContinueWhenAll {Thread.CurrentThread.ManagedThreadId.ToString("00")}"));

總結一下,WaitAll,WaitAny,ContinueWhenAll , ContinueWhenAny,有了這2個等待和2個回撥的方法,可以解決絕大部分多執行緒執行的控制順序的問題。

還有帶返回值的(Func):

Task<int> intTask = taskFactory.StartNew(() => 123);
int iResult = intTask.Result;

還有單獨一個多執行緒執行後的回撥:

// "煎餅果子"就是t.AsyncState
Task task = taskFactory.StartNew(t => this.DoSomethingLong("btnTask_Click_005"), "煎餅果子")
                                  .ContinueWith(t => Console.WriteLine($"這裡是{t.AsyncState}的回撥"));

千萬不要在Task裡面去啟動Task