C#多線程之Task

一 簡介:

多線程的原理我就不講了,做開發(fā)處處用到。C#也提供了幾種多線程的使用方式,分別是Thread類、ThreadPool類、Parallel 類和Task類,這里只講Task,其他的知道有就可以了,就像當(dāng)初做iOS開發(fā)的時候,也把蘋果原生的幾種線程都學(xué)了,也做了博客記錄,但是在實際開發(fā)當(dāng)中這么多年卻只用了GCD這一種方式來處理,問就是好用、性能高。就像人吃飯一樣,有肉不吃干嘛非要去喝湯,對吧?

二 Task介紹

Task 是 .NET 中用于表示異步操作的類,出現(xiàn)在C#4.0時代,可以簡單看作相當(dāng)于Thead+TheadPool,其性能比直接使用Thread要好太多,它可以適用于很多種場景和功能。
比如:

  • 異步執(zhí)行代碼:Task 允許在單獨的線程上執(zhí)行代碼塊,從而避免阻塞主線程,提高程序的響應(yīng)性和并發(fā)性。
  • 等待異步操作完成:通過 await 關(guān)鍵字可以等待 Task 完成,從而實現(xiàn)非阻塞的異步操作。
  • 并行執(zhí)行多個任務(wù):Task 可以用于并行執(zhí)行多個獨立的任務(wù),從而加快任務(wù)的完成速度,提高系統(tǒng)的性能。
  • 處理異常:Task 提供了異常處理機制,可以在異步操作中捕獲和處理異常。
  • 輕量級的線程管理:Task 使用線程池來管理線程,避免了頻繁創(chuàng)建和銷毀線程的開銷,使得線程的管理更加高效。
  • 取消異步操作:Task 支持取消操作,可以通過 CancellationToken 來取消正在進行的異步操作。

總之,Task在多線程開發(fā)中起著非常重要的作用,一定要學(xué)好并用好。

三 Task開啟方式

  • Task對象的Start()方法
            Console.WriteLine("當(dāng)前線程1:{0}", Thread.CurrentThread.ManagedThreadId);
            Task task1 = new Task(() =>// 無返回值
            {
                Console.WriteLine("當(dāng)前線程2:{0}", Thread.CurrentThread.ManagedThreadId);
                Thread.Sleep(2000);
            });
            task1.Start();
            Console.WriteLine("主線程不受影響");

            Task<string> task2 = new Task<string>(() => {//  有返回值string類型
                return "task2";
            });
            task2.Start();
            string str = task2.Result;
            Console.WriteLine(str);
  • Task的靜態(tài)方法(函數(shù))Run()
            Task.Run(() =>
            {
                Console.WriteLine("這里是子線程嘍");
            });
            Task<string> task = Task.Run<string>(() =>
            {
                return"這里是子線程嘍";
            });
            string str = task.Result;
            Console.WriteLine($"{str}");
  • Task工廠
            TaskFactory factory = Task.Factory;
            factory.StartNew(() =>
            {
                Console.WriteLine("你好 task");
            });

            Task<string> task = factory.StartNew<string>(() =>
            {
                return "你好 task";
            });
            string str = task.Result;
            Console.WriteLine($"{str}");

四 Task調(diào)用方法

            Debug.WriteLine("主線程--{0}", Thread.CurrentThread.ManagedThreadId);
            Task.Run(t1);
            Task.Run(() => t1());

            Task.Run(() => t2("ceshi"));
        private void t1()
        {
            Debug.WriteLine("t1--{0}", Thread.CurrentThread.ManagedThreadId);
        }
        private void t2(string str)
        {
            Debug.WriteLine("t2--{0}", Thread.CurrentThread.ManagedThreadId);
        }

五 Task線程等待(阻塞線程)

  • Wait()
            Task task1 = Task.Run(() =>
            {
                Thread.Sleep(1000);
                Console.WriteLine("我排第二");
            });
            Console.WriteLine("我先打印");
            task1.Wait(2000); //等待2000毫秒后再往下走 如果寫task1.Wait()就是必須要等task1子線程走完才能往下走(那我有必要開子線程嘛)
            Task task2 = Task.Run(() =>
            {
                Thread.Sleep(1000);
                Console.WriteLine("我墊底了嘛");
            });
            task2.Wait(500);
            Console.WriteLine("我當(dāng)老三");
  • WaitAll()
            Console.WriteLine("來任務(wù)咯");
            Task task1 = Task.Run(() => {
                Thread.Sleep(1000);
                Console.WriteLine("我耗時1秒");
            });
            Task task2 = Task.Run(() => {
                Thread.Sleep(2000);
                Console.WriteLine("我耗時2秒");
            });
            Task.WaitAll(task1, task2);// 等待task1和task2任務(wù)全走完才能往后走
            Console.WriteLine("沒事,我等你們走完我再走");
  • WaitAny()
            Console.WriteLine("來任務(wù)咯");
            Task task1 = Task.Run(() => {
                Thread.Sleep(1000);
                Console.WriteLine("我耗時1秒");
            });
            Task task2 = Task.Run(() => {
                Thread.Sleep(2000);
                Console.WriteLine("我耗時2秒");
            });
            Task.WaitAny(task1, task2);// task1和task2只要有一個任務(wù)完成就往后走
            Console.WriteLine("只要有一個任務(wù)完成我就走嘍");

六 Task任務(wù)的延續(xù)

  • TaskAwaiter<TResult> GetAwaiter():Task類的實例方法,返回TaskAwaiter對象。
  • TResult GetResult():TaskAwaiter類的實例方法,返回線程任務(wù)的返回結(jié)果。
            Task<int> task1 = Task.Run<int>(() => { return 1; });
            Task<int> task2 = Task.Run<int>(() => { return 2; });
            TaskAwaiter<int> task1Awaiter = task1.GetAwaiter();
            TaskAwaiter<int> task2Awaiter = task2.GetAwaiter();
            Task<int> task = Task.Run<int>(() =>
            {
                return task1Awaiter.GetResult() + task2Awaiter.GetResult();
            });
            Console.WriteLine($"讓我來看看你們給的數(shù)字和是:{task.Result}");
            Console.WriteLine("會阻塞主線程哦");
  • WhenAll(task1,task2,...):Task的靜態(tài)方法,作用是異步等待指定任務(wù)完成后,返回結(jié)果。當(dāng)線程任務(wù)有返回值時,返回Task<TResult[]>對象,否則返回Task對象。
  • WhenAny()用法與WhenAll()是一樣的,不同的是只要指定的任意一個線程任務(wù)完成則立即返回結(jié)果。
            // WhenAll WhenAny + ContinueWith 不阻塞主線程
            Task<int> task1 = Task.Run(() => { Thread.Sleep(5000); return 1; });
            Task<int> task2 = Task.Run(() => { Thread.Sleep(5000); return 2; });
            Task<string> task = Task.WhenAll(task1, task2).ContinueWith((t) => {
                // t:ContinueWith的返回值,一個新的延續(xù)task
                Debug.WriteLine(task1.Result);
                Debug.WriteLine(task2.Result);
                return "3";
            });
            Console.WriteLine($"我立馬打印,對我沒影響");
            Console.WriteLine($"我要等task走完才能打印哦:{task.Result}");
            Console.WriteLine($"上面堵了,我也要等了");
        }
  • ContinueWith():Task類的實例方法,異步創(chuàng)建當(dāng)另一任務(wù)完成時可以執(zhí)行的延續(xù)任務(wù)。也就是當(dāng)調(diào)用對象的線程任務(wù)完成后,執(zhí)行ContinueWith()中的任務(wù)。
  • ContinueWhenAny:某一個任務(wù)執(zhí)行結(jié)束后,去觸發(fā)一個動作,不卡主線程,TaskFactory實例方法,等價于WhenAny+ContinueWith
  • ContinueWhenAll:所有任務(wù)執(zhí)行完成后,去觸發(fā)一個動作,不卡主線程,TaskFactory實例方法,等價于WhenAll+ContinueWith

七 Task枚舉

  • AttachedToParent:父子任務(wù)
    假設(shè)遇到如下需求,線程parentTask中開啟了線程task1和task2,希望在開啟parentTask線程任務(wù)的主線程中阻塞等待parentTask、task1和task2的任務(wù)完成。
    此時可以將task1和task2線程依附到parentTask線程上作為parentTask的子線程,這樣主線程在等待parentTask線程完成時,就必須同步等待task1和task2線程的任務(wù)完成
Task parentTask = new Task(() => {
    Task task1 = new Task(() => { Console.WriteLine("task1任務(wù)。。。。。。"); }, TaskCreationOptions.AttachedToParent);
    Task task2 = new Task(() => { Console.WriteLine("task2任務(wù)。。。。。。"); }, TaskCreationOptions.AttachedToParent);
    task1.Start();
    task2.Start();
});
parentTask.Start();
parentTask.Wait();
Console.WriteLine("我是主線程");
  • LongRunning:耗時任務(wù)
    當(dāng)要執(zhí)行的線程任務(wù)比較耗時時,建議在創(chuàng)建線程對象時傳入?yún)?shù)TaskCreationOptions.LongRunning,以此來聲明為長時間運行的線程任務(wù)。

默認(rèn)情況下,新建Task線程是從線程池ThreadPool中分配出來的,當(dāng)使用TaskCreationOptions.LongRunning聲明后則是直接新建一個線程。這樣就可以避免耗時任務(wù)一直占用線程池資源的情況。當(dāng)然了,也可以直接使用Thread,效果上是一樣的。

            Task task = new Task(() => { ...}, TaskCreationOptions.LongRunning);
            task.Start();

八 Task中使用取消令牌

  • 介紹說明
    Task中的取消功能使用的是CanclelationTokenSource,即取消令牌源對象,可用于解決多線程任務(wù)中協(xié)作取消和超時取消。
    CancellationToken Token:CanclelationTokenSource類的屬性成員,返回CancellationToken對象,可以在開啟或創(chuàng)建線程時作為參數(shù)傳入。
    bool IsCancellationRequested:CanclelationTokenSource類的屬性成員,表示當(dāng)前任務(wù)是否已經(jīng)請求取消。Token類中也有此屬性成員,兩者互相關(guān)聯(lián)。
    void Cancel():CanclelationTokenSource類的實例方法,取消線程任務(wù),同時將自身以及關(guān)聯(lián)的Token對象中的IsCancellationRequested屬性置為true。
    void CancelAfter(int millisecondsDelay):CanclelationTokenSource類的實例方法,用于延遲取消線程任務(wù)。
    CancellationTokenRegistration Register(Action callback):Token類的實例方法,用于注冊取消任務(wù)后的回調(diào)任務(wù)。
    Dispose (): 當(dāng)不再需要取消令牌時,應(yīng)調(diào)用 CancellationTokenSource 的 Dispose 方法來釋放資源。這是很重要的,特別是在長時間運行的應(yīng)用程序中,以確保不會發(fā)生資源泄露
            // Task任務(wù)的取消和判斷
            CancellationTokenSource cst = new CancellationTokenSource();
            Task task = Task.Run(() => {
                while (!cst.IsCancellationRequested)
                {
                    Console.WriteLine("持續(xù)時間:" + DateTime.Now);
                }
            }, cst.Token);//這里第二個參數(shù)傳入取消令牌

            Thread.Sleep(2000);
            cst.Cancel(); //兩秒后結(jié)束
            // 任務(wù)的延時取消可以用于訪問超時、執(zhí)行超時等情況下的任務(wù)強制終止
            CancellationTokenSource cst = new CancellationTokenSource();
            Task task = Task.Run(() => {
                while (!cst.IsCancellationRequested)
                {
                    Console.WriteLine("持續(xù)時間:" + DateTime.Now);
                }
            }, cst.Token);//這里第二個參數(shù)傳入取消令牌
            cst.CancelAfter(2000); //兩秒后結(jié)束 也是異步進行
            // Task任務(wù)取消回調(diào):如果取消任務(wù)后希望做一些處理工作。
            // 此時可以使用CancellationToken類的Register()函數(shù)來注冊一個委托(回調(diào)函數(shù)),用于取消線程后調(diào)用。
            CancellationTokenSource cst = new CancellationTokenSource();
            Task task = Task.Run(() => {
                while (!cst.IsCancellationRequested)
                {
                    Console.WriteLine("持續(xù)時間:" + DateTime.Now);
                    Thread.Sleep(500);
                }
            }, cst.Token);//這里第二個參數(shù)傳入取消令牌
            cst.Token.Register(() => {
                Console.WriteLine("開始處理工作......");
                Thread.Sleep(2000);
                Console.WriteLine("處理工作完成......");
            });
            Thread.Sleep(2000);
            cst.Cancel(); //兩秒后結(jié)束
  • 常規(guī)用法:
        private Task task;
        private CancellationTokenSource cancellationSource;

        private void TestTokenSource()
        {
            // 常規(guī)使用
            cancellationSource = new CancellationTokenSource();
            CancellationToken token = cancellationSource.Token;
            task = Task.Run(() =>
            {
                while (!token.IsCancellationRequested)
                {
                    Console.WriteLine("持續(xù)工作");
                }
            }, token);
            task.Wait(5000);
            cancellationSource?.Cancel();
            cancellationSource?.Dispose();
        }

或者

        private CancellationTokenSource cts123;

        private void button4_Click(object sender, EventArgs e)
        {
            cts123 = new CancellationTokenSource();
            CancellationToken token = cts123.Token;
            Task.Run(async () =>
            {
                Debug.WriteLine("開啟循環(huán)");
                while (!token.IsCancellationRequested)
                {
                    Debug.WriteLine("任務(wù)進行中...");
                    await Task.Delay(1000);
                    //await Task.Delay(1000,token); 如果取下這里會立即報 異常,可外部增加try來捕捉,就不用再等1秒了
                }
                Debug.WriteLine("結(jié)束循環(huán)");
            }, token);
        }

        private void button5_Click(object sender, EventArgs e)
        {
            cts123?.Cancel();
            cts123?.Dispose();
            cts123 = null;

        }
  • CancellationTokenSource和CancellationToken區(qū)別:
    CancellationTokenSource(CTS): 這是生成取消令牌的主要對象??梢酝ㄟ^調(diào)用它的 Cancel 方法來請求取消一個或多個操作。
    CancellationToken(CT): 這是一個輕量級的結(jié)構(gòu),由 CancellationTokenSource 生成,并可以傳遞到多個操作中。操作可以檢查此令牌的狀態(tài)以確定是否應(yīng)該取消其執(zhí)行。
            cancellationSource = new CancellationTokenSource();
            cancellationSource.Cancel();
            cancellationSource.Dispose();


            if (cancellationSource.Token.IsCancellationRequested)// 報錯 System.ObjectDisposedException:“CancellationTokenSource 已釋放?!?            {
                Console.WriteLine("cancle");
            }
            else
            {
                Console.WriteLine("not cancle");
            }


            if (cancellationSource.IsCancellationRequested)
            {
                Console.WriteLine("cancle");// 打印
            }
            else
            {
                Console.WriteLine("not cancle");
            }


            if (cancellationSource != null)
            {
                Console.WriteLine("not null");// 打印
            }
            else
            {
                Console.WriteLine(" null");
            }

九 Task跨線程訪問控件

在使用Winform或WPF編寫程序時,經(jīng)常會遇到跨線程訪問控件的情況,除了使用Invoke和委托等方法外,還可以有以下兩種解決方法。

  • 方式一:
    直接將TaskScheduler對象做為參數(shù)傳給Start()函數(shù)(使用TaskScheduler.FromCurrentSynchronizationContext()可以獲得TaskScheduler對象),以此來將線程任務(wù)傳送到指定的調(diào)度程序中運行,結(jié)合WPF編程寶典多線程章節(jié)中的內(nèi)容,應(yīng)該是將線程任務(wù)丟給控件元素所在線程的調(diào)度程序中運行。這樣做雖然可以跨線程訪問控件,但是帶來的弊端就是,如果線程任務(wù)耗時,就會讓整個窗體卡住。
            Task task = new Task(() =>
            {
                Thread.Sleep(5000);//模擬耗時處理
                txt_Info.Text = "test"; //此為文本控件
            });
            task.Start(TaskScheduler.FromCurrentSynchronizationContext());
  • 方式二:
    針對線程耗時的情況,如果直接使用方式一,會導(dǎo)致整個UI界面都卡住,等到控件處理完成才恢復(fù),這樣顯然是不可以的。因此要改變一下用法,利用線程延續(xù),將耗時的任務(wù)與訪問UI控件的任務(wù)分為兩個線程,訪問UI的線程放到延續(xù)的線程中。
            txt_Info.Text = "數(shù)據(jù)正在處理中......";
            txt_Info.Text = "數(shù)據(jù)正在處理中......";
            Task.Run(() =>
            {
                Thread.Sleep(5000);
            }).ContinueWith(t => {
                txt_Info.Text = "test";
            }, TaskScheduler.FromCurrentSynchronizationContext());

十 Task的異常處理

1.線程外部使用Wait:Task線程的異常處理不能直接將線程對象相關(guān)代碼try-catch來捕獲,那樣是捕獲不到異常的,因為開始異常還沒發(fā)生,主線程已經(jīng)執(zhí)行完畢,需要通過調(diào)用線程對象的wait()函數(shù),通過wait()函數(shù)來進行線程的異常捕獲。此外,線程的異常會聚合到AggregateException異常對象中(AggregateException是專門用來收集線程異常的異常類),需要通過遍歷該異常對象,獲取正確的異常信息。如果捕獲到線程異常之后,還想繼續(xù)往上拋出,就需要調(diào)用AggregateException對象的Handle函數(shù),并返回false。(Handle函數(shù)遍歷了一下AggregateException對象中的異常)

            Task task1 = Task.Run(() =>
            {
                throw new Exception("線程1的異常拋出");
            });
            Task task2 = Task.Run(() =>
            {
                throw new Exception("線程2的異常拋出");
            });
            Task task3 = Task.Run(() =>
            {
                throw new Exception("線程3的異常拋出");
            });

            try
            {
                task1.Wait();
                task2.Wait();
                task3.Wait();
            }
            catch (AggregateException ex)
            {
                foreach (var item in ex.InnerExceptions)
                {
                    Console.WriteLine(item.Message);
                }
                //如果希望再將異常往外拋出,可以調(diào)用AggregateException的Handle函數(shù)
                //ex.Handle(p => false);
            }
            Console.Read();

2.線程內(nèi)部可直接try-catch

            try
            {
                Task task = Task.Run(() =>
                {
                    try
                    {
                        int i = 0;
                        int j = 10;
                        int k = j / i; //嘗試除以0,會異常
                    }
                    catch (Exception ex)
                    {
                        Debug.WriteLine($"線程內(nèi)異常{ex.Message}");
                    }
                });
            }
            catch (AggregateException aex)
            {
                foreach (var exception in aex.InnerExceptions)
                {
                    Debug.WriteLine($"線程不等待:異常{exception.Message}");
                }
            }

十一 Task.Delay(5000).Wait()和await Task.Delay(5000)區(qū)別

  • Task.Delay(5000); // 創(chuàng)建一個在指定的毫秒數(shù)后完成的任務(wù),Task.Delay() 是一個異步執(zhí)行的方法。
  • Wait() 方法是一個同步方法,它會阻塞調(diào)用它的線程,直到 Task.Delay 返回的任務(wù)完成。
  • await關(guān)鍵字用于等待一個Task或Task<T> 的完成,而不會阻塞當(dāng)前線程。你可以在async方法內(nèi)部使用await

十二 token傳遞和不傳遞的區(qū)別

在官方網(wǎng)站中是這樣解釋這個重載方法的:
Run(Action, CancellationToken):將指定的工作排入隊列,以在線程池上運行,并返回表示該工作的 Task 對象。 取消令牌允許取消工作(如果尚未啟動)。
那么我們試一下例子

image.png

我們傳遞token后
image.png

所以如果不傳遞token,我們就無法控制這個任務(wù)的取消。

在看一個例子


通過打印,可以看到,一旦task開啟后,即使傳遞了token,外面也無法真的取消掉,因此我們傳遞token的同時還必須在內(nèi)部對token進行IsCancellationRequested的判斷。
在C#中,當(dāng)您使用Task.Run并傳遞一個CancellationToken時,這個令牌不僅可以在任務(wù)內(nèi)部用于檢查取消請求,而且Task.Run方法本身也會對這個令牌進行監(jiān)視。這意味著,如果令牌在任務(wù)開始執(zhí)行之前或執(zhí)行過程中被取消,Task.Run返回的Task對象將能夠反映出這一點。然而,如果您不將CancellationToken作為參數(shù)傳遞給Task.Run,那么即使您在任務(wù)內(nèi)部定義了一個CancellationToken變量并檢查它,Task.Run返回的Task對象也不會知道這個令牌的存在或狀態(tài)。換句話說,Task對象本身不會因為這個令牌而被取消或改變其狀態(tài)。
其實,我們把token傳遞之后,任務(wù)本身知道了token的狀態(tài),會在令牌取消后,自己做一些提前退出或釋放資源等等,所以最好還是把token作為第二個參數(shù)進行傳遞。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容