委派的非同步方法

可以透過BeginInvoke執行委派的非同步方法

Action<T>.BeginInvoke(<T> obj,AsyncCallback callback,Object @object)

第一個內容的 obj,只的是要傳入acction委派的參數
第二個AsyncCallback,是當Action內容執行完後下一段要執行的程式碼
第三個參數 可以讓第二個參數AsyncCallback透過ia.AsyncState讀取到

   Action<string> acction = this.doSomething;           
   AsyncCallback callback = ia => Console.WriteLine($"到這計算完成");
   acction.BeginInvoke("btnAsync", callback, null); 

執行緒等待

            //判斷IsCompleted狀態是否結束 如果還沒就讓主執行緒睡覺
            while (!asyncResult.IsCompleted)
                Thread.Sleep(200);
            asyncResult.AsyncWaitHandle.WaitOne();//等待任務完成
            asyncResult.AsyncWaitHandle.WaitOne(-1);//等待任務完成
            asyncResult.AsyncWaitHandle.WaitOne(100);//等待任務完成,但最多等待100ms
            acction.EndInvoke(asyncResult);//等待任務完成,可以取得委派的返回數值

EndInvoke 示範 (主動使用EndInvoke,可以線呈更好的重用)

            Func<int> fuck = () =>
                Thread.Sleep(2000);
                return DateTime.Now.Day;
            Console.WriteLine($"func.Invoke() ={fuck.Invoke()}");
            IAsyncResult asyncResult1 = fuck.BeginInvoke(
                    Console.WriteLine(r.AsyncState);
                }, "冰封的心");
            Console.WriteLine($"func.EndInvoke(asyncResult1) = {fuck.EndInvoke(asyncResult1)}");

3.0 Task 是基於ThreadPool Task增加了多個API

Task.Run

Task.Run(() => this.doSomething("task1"));

Task工廠模式版本

    TaskFactory taskFactory = Task.Factory;//4.0
    taskFactory.StartNew(() => this.doSomething("task3"));

Task.Start

    new Task(() => this.doSomething("task5")).Start();

Task阻塞

主程序的情況

List<Task> taskList = new List<Task>();
taskList.Add(Task.Run(() => this.doSomething("01")));
taskList.Add(Task.Run(() => this.doSomething("02")));
taskList.Add(Task.Run(() => this.doSomething("03")));
taskList.Add(Task.Run(() => this.doSomething("04")));

會卡介面的方式

  • WaitAny 方法會判斷 taskList中如果有其中一個執行緒結束,就會往下執行下面的程式
  • WaitAll 方法會判斷 taskList中全部的執行緒結束,才會往下執行下面的程式
  • //阻塞 等者某個任務完成後才會往下進行
    Task.WaitAny(taskList.ToArray());//卡介面
    //阻塞 等者全部任務完成後才會往下進行
    Task.WaitAll(taskList.ToArray());//卡介面
    

    不會卡介面的方式

    WhenXXX.ContinueWith 的方式會在創造一條子執行緒,等待條件結束會在執行,
    ContinueWith裡面的內容

    Task.WhenAny(taskList.ToArray()).ContinueWith(t =>
        Console.WriteLine($"哈哈哈哈:{Thread.CurrentThread.ManagedThreadId}");
    Task.WhenAll(taskList.ToArray()).ContinueWith(t =>
        Console.WriteLine($"部屬環境,測試完成  執行緒:{Thread.CurrentThread.ManagedThreadId}");
    

    執行緒等待的方式 Sleep & Delay

    Thread.Sleep 會卡介面

    當前執行緒等待XX秒

    Stopwatch stopwatch = new Stopwatch();
    stopwatch.Start();
    Thread.Sleep(2000);
    stopwatch.Stop();
    Console.WriteLine(stopwatch.ElapsedMilliseconds);
    

    Task.Delay 延遲 不會卡介面

    Stopwatch stopwatch = new Stopwatch();
    stopwatch.Start();
    Task.Delay(2000).ContinueWith(t =>
       stopwatch.Stop();
       Console.WriteLine(stopwatch.ElapsedMilliseconds);
    

    類似的功能

    Stopwatch stopwatch = new Stopwatch();
    stopwatch.Start();
    Task.Run(() =>
      Thread.Sleep(2000);
      stopwatch.Stop();
      Console.WriteLine(stopwatch.ElapsedMilliseconds);
    

    Parallel

    並行線程 在Task的基礎上做了封裝 4.5
    Parallel 卡介面 主線程參與計算,節約了一個線程

    Parallel.Invoke(() => this.doSomething("test1"),
                                () => this.doSomething("test1"),
                                () => this.doSomething("test1"));
    
    Parallel.For(0, 5, i => this.doSomething("第"+i));
    
     Parallel.ForEach(new string[] { "1", "2", "3", "4", "5" },i =>this.doSomething(i));       
    

    設定最多執行緒數量

    ParallelOptions parallelOptions = new ParallelOptions();
    parallelOptions.MaxDegreeOfParallelism = 3;
    Parallel.For(0, 5, i => this.doSomething("第" + i));
    

    Break =>類似conntinue,Stop=> 類似break;

    //Break  Stop  都不推荐用
    ParallelOptions parallelOptions = new ParallelOptions();
    parallelOptions.MaxDegreeOfParallelism = 3;
    Parallel.For(0, 100, parallelOptions, (i, state) =>
        if (i == 2)
            Console.WriteLine("Break,當前線呈結束");
            state.Break();//當前線呈結束
            return;//必须带上,才會釋放資源
        if (i == 30)
            Console.WriteLine("線呈Stop,結束");
            state.Stop();//结束Parallel
            return;//必须带上,才會釋放資源
        this.Coding("當前參數", "Client" + i);
    

    目前測試Break && Stop 看不出效果
    Break 實際上結束了當前這個線呈,如果是主現成 等於Parallel都結束了

    	ParallelOptions parallelOptions = new ParallelOptions();
    	parallelOptions.MaxDegreeOfParallelism = 1;
    	Parallel.For(1, 100, (i, ParallelLoopState) =>
    		Console.WriteLine($"開始 i => {i} 主要執行續 => {Thread.CurrentThread.ManagedThreadId.ToString("00")}");
    		if (i == 5)
    			Console.WriteLine($"掰掰 i => {i} 主要執行續 => {Thread.CurrentThread.ManagedThreadId.ToString("00")}");
    			// 跳出當前執行單元
    			ParallelLoopState.Stop();
    			return;//不加return,可能會發生該程序資源未釋放。
    		Console.WriteLine($"結束 i => {i} 主要執行續 => {Thread.CurrentThread.ManagedThreadId.ToString("00")}");
    

    例外(異常)處理

    TaskFactory taskFactory = new TaskFactory();
    List<Task> taskList = new List<Task>();
        for (int i = 0; i < 20; i++)
            string name = string.Format($"btnThreadCore_Click_{i}");
            Action<object> act = t =>
                Thread.Sleep(2000);
                if (t.ToString().Equals("btnThreadCore_Click_11"))
                    throw new Exception(string.Format($"{t} 执行失败"));
                if (t.ToString().Equals("btnThreadCore_Click_12"))
                    throw new Exception(string.Format($"{t} 执行失败"));
                Console.WriteLine("{0} 执行成功", t);
            taskList.Add(taskFactory.StartNew(act, name));
    catch (AggregateException aex)
        foreach (var item in aex.InnerExceptions)
            Console.WriteLine(item.Message);
    catch (Exception ex)
        Console.WriteLine(ex.Message);
    

    雖然在多執行緒,外包try catch卻捕捉不到例外

    執行緒裡面的異常會被吞掉,因為已經郭離try catch的範圍了 使用waitAll 可以抓到多線呈裡面裡面的全部異常

    TaskFactory taskFactory = new TaskFactory();
    	List<Task> taskList = new List<Task>();
    		for (int i = 0; i < 20; i++)
    			string name = string.Format($"btnThreadCore_Click_{i}");
    			Action<object> act = t =>
    				Thread.Sleep(2000);
    				if (t.ToString().Equals("btnThreadCore_Click_11"))
    					throw new Exception(string.Format($"{t} 執行失敗"));
    				if (t.ToString().Equals("btnThreadCore_Click_12"))
    					throw new Exception(string.Format($"{t} 執行失敗"));
    				Console.WriteLine("{0} 執行成功", t);
    			taskList.Add(taskFactory.StartNew(act, name));
    		Task.WaitAll(taskList.ToArray());
    	catch (AggregateException aex)
    		foreach (var item in aex.InnerExceptions)
    			Console.WriteLine(item.Message);
    	catch (Exception ex)
    		Console.WriteLine(ex.Message);
    

    必須使用waitAll,才捕捉的到Exception,but會導致卡畫面使用者體驗上會有等待的感覺

    建議 執行緒裡面的action不允許出現Exception,自己處理好

                    for (int i = 0; i < 20; i++)
                        string name = string.Format($"btnThreadCore_Click_{i}");
                        Action<object> act = t =>
                                Thread.Sleep(2000);
                                if (t.ToString().Equals("btnThreadCore_Click_11"))
                                    throw new Exception(string.Format($"{t} 执行失败"));
                                if (t.ToString().Equals("btnThreadCore_Click_12"))
                                    throw new Exception(string.Format($"{t} 执行失败"));
                                Console.WriteLine("{0} 执行成功", t);
                            catch (Exception ex)
                                Console.WriteLine($"Exception:{ex.Message}");
                        taskList.Add(taskFactory.StartNew(act, name));
                    Task.WaitAll(taskList.ToArray());
    

    執行續取消

    可以透過CancellationTokenSource類別實作

    //多个线程并发,某个失败后,希望通知别的线程,都停下来
    //task是外部无法中止,Thread.Abort不靠谱,因为线程是OS的资源,无法掌控啥时候取消
    //线程自己停止自己--公共的访问变量--修改它---线程不断的检测它(延迟少不了)
    //CancellationTokenSource去标志任务是否取消  Cancel取消   IsCancellationRequested  是否已经取消了
    //Token 启动Task的时候传入,那么如果Cancel了,这个任务会放弃启动,抛出一个异常
    CancellationTokenSource cts = new CancellationTokenSource();//bool值 //bool flag = true;
    for (int i = 0; i < 40; i++)
        string name = string.Format("btnThreadCore_Click{0}", i);
        Action<object> act = t =>
                //if (cts.IsCancellationRequested)
                //    Console.WriteLine("{0} 取消一个任务的执行", t);
                Thread.Sleep(2000);
                if (t.ToString().Equals("btnThreadCore_Click11"))
                    throw new Exception(string.Format("{0} 执行失败", t));
                if (t.ToString().Equals("btnThreadCore_Click12"))
                    throw new Exception(string.Format("{0} 执行失败", t));
                if (cts.IsCancellationRequested)//检查信号量
                    Console.WriteLine("{0} 放弃执行", t);
                                    return;
                    Console.WriteLine("{0} 执行成功", t);
            catch (Exception ex)
                cts.Cancel();
                Console.WriteLine(ex.Message);
        taskList.Add(taskFactory.StartNew(act, name, cts.Token));
    Task.WaitAll(taskList.ToArray());
    在這裡 (1) 的cts因為一開始輸入參數為false 所以會跳出exception 被(2) 捕捉到,因此才會印出 工作已取消。

    線呈臨時變數

    因為i是全域變數,所以最後印出來的結果會是i = 5;
    input:

    for (int i = 0; i < 5; i++)
        Task.Run(() =>
            Thread.Sleep(100);
            Console.WriteLine(i);
    

    output:

    修正方式:

    i最后是5 全程就只有一个i 等着打印的时候,i == 5
    k 全程有5个k 分别是0 1 2 3 4
    如果k在外面宣告 全程就只有一个k,等着打印的时候,k == 4

    for (int i = 0; i < 5; i++)
        int k = i;
        Task.Run(() =>
            Thread.Sleep(100);
            Console.WriteLine(k);
    

    執行緒安全 lock

    容易造成 資料錯誤的原因:

    共用的變數:都能共同訪問的區域變數/全域變數/數據庫的一個值/硬碟文件

    執行緒内部不共享的是安全

    	int TotalCountIn = 0;
    	for (int i = 0; i < 10000; i++)
    		TotalCountIn++;
    	Console.WriteLine($"TotalCountIn = {TotalCountIn}");
    

    輸出結果會是:

    TotalCountIn = 10000
    

    如果改成用多執行緒的方式

    	int TotalCountIn = 0;
    	for (int i = 0; i < 10000; i++)
    		Task.Run(() => 
    			TotalCountIn++;
    	Console.WriteLine($"TotalCountIn = {TotalCountIn}");
    

    輸出結果會是:

    TotalCountIn = 8972
    

    為什麼會出現這樣的情況呢?

    private 防止外面也去lock static 全场唯一 readonly不要改动 object表示引用

    微軟推薦的方式 =>將要鎖住的內容包在lock裡面

    private static readonly object btnThreadCore_Click_Lock = new object();
    lock (btnThreadCore_Click_Lock)
    
    private static readonly object btnThreadCore_Click_Lock = new object();
    void Main()
    	List<Task> taskList = new List<Task>();
    	int TotalCountIn = 0;
    	List<int> IntList = new List<int>();
    	for (int i = 0; i < 10000; i++)
    		int newI = i;
    		taskList.Add(Task.Run(() =>
    			lock(btnThreadCore_Click_Lock) 
    			   TotalCountIn+=1;
    			   IntList.Add(newI);
    	Task.WaitAll(taskList.ToArray());
    	Console.WriteLine($"TotalCountIn = {TotalCountIn}");
    	Console.WriteLine("IntList 總數量為 = " + IntList.Count());
    

    輸出結果:

    TotalCountIn = 10000
    IntList 總數量為 = 10000
    

    在這裡需要加上Task.WaitAll(taskList.ToArray()); 確保所有執行緒都執行完成,這樣顯示的結果才會是正確的

    解法2 lock(this)

    this form1的實體 每次實體化是不同的鎖,同一個實體是相同的鎖
    但是這個實體別人也能訪問到,別人也能鎖住

    lock(this)
    
    void Main()
    	List<Task> taskList = new List<Task>();
    	int TotalCountIn = 0;
    	List<int> IntList = new List<int>();
    	for (int i = 0; i < 10000; i++)
    		int newI = i;
    		taskList.Add(Task.Run(() =>
    			lock(this) 
    			   TotalCountIn+=1;
    			   IntList.Add(newI);
    	Task.WaitAll(taskList.ToArray());
    	Console.WriteLine($"TotalCountIn = {TotalCountIn}");
    	Console.WriteLine("IntList 總數量為 = " + IntList.Count());
    

    解法3 Monitor

    將程式包在Monitor裡面就跟lock一樣

    Monitor.Enter(btnThreadCore_Click_Lock);
    要執行的程式
    Monitor.Exit(btnThreadCore_Click_Lock);
    
    private static readonly object btnThreadCore_Click_Lock = new object();
    void Main()
    	List<Task> taskList = new List<Task>();
    	int TotalCountIn = 0;
    	List<int> IntList = new List<int>();
    	for (int i = 0; i < 10000; i++)
    		int newI = i;
    		taskList.Add(Task.Run(() =>
    			Monitor.Enter(btnThreadCore_Click_Lock);
    			TotalCountIn+=1;
    			IntList.Add(newI);
    			Monitor.Exit(btnThreadCore_Click_Lock);
    	Task.WaitAll(taskList.ToArray());
    	Console.WriteLine($"TotalCountIn = {TotalCountIn}");
    	Console.WriteLine("IntList 總數量為 = " + IntList.Count());
    

    使用 安全對列 ConcurrentQueue 一個執行緒去完成操作

    值類型不行lock

    int m = 3 + 2;
    lock (m) { }//值類型不能lock
    

    只能鎖引用類型,占用這個引用鏈結 不要用string 因為享元

    string teacher = "Eleven";
    string teacherVip = "Eleven";
    lock (teacher)
    lock (teacherVip)
    

    lock 解决,因为只有一个线程可以进去,没有并发,所以解决了问题 但是牺牲了性能,所以要尽量缩小lock的范围

    不要衝突--數據拆分,避免衝突

    C# 開發實戰:非同步程式開發技巧

    本篇已同步發表至個人部落格
    https://moushih.com/2022ithome20/