1. 程式人生 > 實用技巧 >C# Task和非同步方法

C# Task和非同步方法

本文主要參考:

https://www.cnblogs.com/qtiger/p/13497807.html


ThreadPool中有若干數量的執行緒。當有任務需要處理時,會從執行緒池中獲取一個空閒的執行緒來執行任務,任務執行完畢後執行緒不會銷燬,而是被執行緒池回收以供後續任務使用。當執行緒池中所有的執行緒都被佔用,又有新任務要處理時,執行緒池會新建一個執行緒來處理該任務。如果執行緒數量達到設定的最大值,任務會排隊,等待其他任務釋放執行緒後再執行。ThreadPool相對於Thread來說可以減少執行緒的建立,有效減小系統開銷。但是ThreadPool不能控制執行緒的執行順序,也不能獲取執行緒池內執行緒取消/異常/完成的通知,即不能有效監控和控制執行緒池中的執行緒。因此NET4.0在ThreadPool的基礎上推出了Task。Task擁有執行緒池的優點,同時也解決了使用執行緒池不易控制的弊端。

1.無返回值的Task的建立和執行

using System;
using System.Threading.Tasks;
using System.Threading;

namespace TaskDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            // 例項化一個Task,通過Start方法啟動
            Task task = new Task(
                () =>
                {
                    Thread.Sleep(
1000); Console.WriteLine($"NEW例項化一個task,執行緒ID為{Thread.CurrentThread.ManagedThreadId}"); } ); task.Start(); // Task.Factory.StartNew(Action action)建立和啟動一個Task Task task2 = Task.Factory.StartNew( () => { Thread.Sleep(
500); Console.WriteLine($"Task.Factory.StartNew方式建立一個task,執行緒ID為{Thread.CurrentThread.ManagedThreadId}"); }); // Task.Run(Action action)將任務放線上程池佇列,返回並啟動一個Task Task task3 = Task.Run( () => { Thread.Sleep(200); Console.WriteLine($"Task.Run方式建立一個task,執行緒ID為{Thread.CurrentThread.ManagedThreadId}"); }); Console.WriteLine("執行主執行緒"); Console.Read(); } } }

執行結果:

2.用Task.Result獲取返回值的Task的建立和執行

namespace TaskDemo
{
    class Program
    {
        static void Main(string[] args)
        {

            // 有返回值的啟動task
            Task<string> task = new Task<string>(
                () =>
                {
                    Thread.Sleep(1000);
                    return $"NEW例項化一個task,執行緒ID為{Thread.CurrentThread.ManagedThreadId}";
                }
                );

            task.Start();

            // Task.Factory.StartNew(Action action)建立和啟動一個Task

            Task<string> task2 = Task.Factory.StartNew(
                () =>
                {
                    Thread.Sleep(3000);
                    return $"Task.Factory.StartNew方式建立一個task,執行緒ID為{Thread.CurrentThread.ManagedThreadId}";
                });

            // Task.Run(Action action)將任務放線上程池佇列,返回並啟動一個Task

            Task<string> task3 = Task.Run(
                () =>
                {
                    Thread.Sleep(2000);
                    return $"Task.Run方式建立一個task,執行緒ID為{Thread.CurrentThread.ManagedThreadId}";
                });

            Console.WriteLine("執行主執行緒");
            Console.WriteLine(task.Result);
            Console.WriteLine(task2.Result);
            Console.WriteLine(task3.Result);
            Console.Read();
        }
    }
}

執行結果:

可見Task.Result獲取返回值時會阻塞執行緒。本例中,必須等到task2執行完成,獲取到返回值後,才能繼續執行task3。但是上面兩個例子中的Task的執行都是非同步的,不會阻塞主執行緒。

3.同步執行Task,會阻塞主執行緒

namespace TaskDemo
{
    class Program
    {
        static void Main(string[] args)
        {

            Task task = new Task(
                () =>
                {
                    Thread.Sleep(1000);
                    Console.WriteLine("執行Task結束");
                }
                );           

            // 同步執行,會阻礙主執行緒
            task.RunSynchronously();
            Console.WriteLine("執行主執行緒");
            Console.Read();
        }
    }
}

執行結果:

4.Task的阻塞方法(Wait/WaitAll/WaitAny)

4.1Thread阻塞執行緒的方法

使用thread.Join()方法可阻塞主執行緒

namespace TaskDemo
{
    class Program
    {
        static void Main(string[] args)
        {

            Thread thread1 = new Thread(
                () =>
                {
                    Thread.Sleep(1000);
                    Console.WriteLine("執行緒1執行完畢");
                });
            thread1.Start();

            Thread thread2 = new Thread(
                () =>
                {
                    Thread.Sleep(2000);
                    Console.WriteLine("執行緒2執行完畢");
                });
            thread2.Start();

            //阻塞主執行緒
            thread1.Join();
            thread2.Join();
            Console.WriteLine("主執行緒執行完畢");
            Console.Read();
        }
    }
}

執行結果:

使用Thread.Join()方法的弊端包括:

  • 如果要實現很多執行緒的阻塞,每個執行緒都要呼叫一次Join()方法;
  • 如果讓所有的執行緒執行完畢(或任一執行緒執行完畢)時,立即解除阻塞,使用Join()方法不容易實現。

4.2使用Task Wait/WaitAll/WaitAny方法,實現阻塞執行緒

  • task.Wait()表示等待task執行完畢,類似於thread.Join()
  • task.WaitAll(Task[] tasks)表示只有所有的task都執行完畢再解除阻塞
  • task.WaitAny(Task[] tasks)表示只要有一個task執行完畢就解除阻塞
Task task1 = new Task(
                () =>
                {
                    Thread.Sleep(1000);
                    Console.WriteLine("執行緒1執行完畢");
                });
            task1.Start();

            Task task2 = new Task(
                () =>
                {
                    Thread.Sleep(2000);
                    Console.WriteLine("執行緒2執行完畢");
                });
            task2.Start();

            // 阻塞主執行緒。task1和task2都執行完畢再執行主執行緒
            //task1.Wait();
            //task2.Wait();
            Task.WaitAll(new Task[] { task1, task2 });
            Console.WriteLine("主執行緒執行完畢");
            Console.Read();

執行結果:

使用task1.Wait(); task2.Wait()可以達到同樣的目的。如果把WaitAll改成WaitAny,則執行結果如下所示:

5.Task的延續操作(WhenAny/WhenAll/ContinueWith)

Wait/WaitAll/WaitAny方法返回值都是void,這些方法只是單純的實現阻塞執行緒。使用WhenAny/WhenAll/ContinueWith方法可以讓task執行完畢後,繼續執行後續操作,這些方法執行完成返回一個task例項。

  • task.WhenAll(Task[] tasks)表示所有的task都執行完畢後再去執行後續的操作
  • task.WhenAny(Task[] tasks)表示任一task執行完畢後就開始執行後續操作
Task task1 = new Task(
                () =>
                {
                    Thread.Sleep(1000);
                    Console.WriteLine("執行緒1執行完畢");
                });
            task1.Start();

            Task task2 = new Task(
                () =>
                {
                    Thread.Sleep(2000);
                    Console.WriteLine("執行緒2執行完畢");
                });
            task2.Start();

            Task.WhenAll(new Task[] { task1, task2 }).ContinueWith(
                (t) =>
                {
                    Thread.Sleep(1000);
                    Console.WriteLine("執行後續操作完畢");
                });

            Console.WriteLine("主執行緒執行完畢");
            Console.Read();

執行結果:

WhenAll/WhenAny方法並不會阻塞主執行緒。也可以使用Task.Factory.ContinueWhenAll來實現

Task.Factory.ContinueWhenAll(new Task[] { task1, task2 }, (t) =>
            {
                Thread.Sleep(1000);
                Console.WriteLine("執行後續操作完畢");
            });

6.Task的任務取消(CancellationTokenSource)

6.1Thread取消任務執行

通過設定一個變數來控制任務是否停止。

        bool isStop = false;
            int index = 0;

            Thread thread1 = new Thread(
                () =>
                {
                    Console.WriteLine($"thread1的執行緒ID是{Thread.CurrentThread.ManagedThreadId}");
                    while (!isStop)
                    {
                        Thread.Sleep(1000);
                        Console.WriteLine($"第{++index}次執行,執行緒執行中...");
                    }
                    
                });
            
            thread1.Start();
            Console.WriteLine($"主執行緒開始執行,主執行緒的ID是{Thread.CurrentThread.ManagedThreadId}");
            // 5s後取消任務執行
            Thread.Sleep(5000);
            isStop = true;
            Console.WriteLine("主執行緒執行完畢");
            Console.Read();

執行結果:

6.2Task取消任務執行

使用專門類CancellationTokenSource來取消任務執行。

CancellationTokenSource source = new CancellationTokenSource();
            int index = 0;
            Task task1 = new Task(
                () =>
                {
                    while (!source.IsCancellationRequested)
                    {
                        Thread.Sleep(1000);
                        Console.WriteLine($"第{++index}次執行,執行緒執行中...");
                    }

                });
            task1.Start();
            Console.WriteLine("主執行緒開始執行");
            Thread.Sleep(5000);
            source.Cancel();
            Console.WriteLine("主執行緒執行完畢");
            Console.Read();

執行結果:

還可以使用source.CancelAfter(5000)實現5s後自動取消任務,即Thread.Sleep(5000); source.Cancel();這兩條程式碼由source.CancelAfter(5000)取代。執行結果:

注意這兩次執行結果中,“主執行緒執行完畢”的區別。也可以通過source.Token.Register(Action action)註冊取消任務觸發的回撥函式。

CancellationTokenSource source = new CancellationTokenSource();
            source.Token.Register(
                () =>
                {
                    Console.WriteLine("任務被取消後執行的操作");
                });
            int index = 0;
            Task task1 = new Task(
                () =>
                {
                    Console.WriteLine($"task1的執行緒ID是{Thread.CurrentThread.ManagedThreadId}");
                    while (!source.IsCancellationRequested)
                    {
                        Thread.Sleep(1000);
                        Console.WriteLine($"第{++index}次執行,執行緒執行中...");
                    }

                });
            task1.Start();
            Console.WriteLine($"主執行緒開始執行,主執行緒的ID是{Thread.CurrentThread.ManagedThreadId}");
            source.CancelAfter(5000);
            Console.WriteLine("主執行緒執行完畢");
            
            Console.Read();

執行結果:

7.非同步方法(async/await)

async static Task<string>GetContentAsync(string fileName)
        {
            Console.WriteLine($"當前執行緒ID是{Thread.CurrentThread.ManagedThreadId}");
            Console.WriteLine($"開始讀取檔案:{DateTime.Now}");
            Thread.Sleep(1000);
            using(StreamReader sr = new StreamReader(fileName))
            {
                string program = await sr.ReadToEndAsync();
                Console.WriteLine($"讀取檔案結束:{DateTime.Now}");
                return program;
            }
        }

        // 同步讀取檔案內容
        static string GetContent(string fileName)
        {
            using (StreamReader sr = new StreamReader(fileName))
            {
                string program = sr.ReadToEnd();
                return program;
            }
        }
        static void Main(string[] args)
        {
            string path = @"D:\Demos\TaskDemo\postdata.txt";
            Console.WriteLine($"主執行緒ID是{Thread.CurrentThread.ManagedThreadId}");
            Console.WriteLine($"主程式執行開始:{DateTime.Now}");
            string content = GetContentAsync(path).Result;
            Console.WriteLine($"主程式輸入結果:{content}");
            Console.WriteLine($"主程式執行結束:{DateTime.Now}");
            Console.Read();
        }

執行結果:

主程式等待GetContentAsync方法執行完畢後,獲取到返回值後才繼續執行。這說明,如果呼叫方法要從呼叫中獲取一個T型別的值,非同步方法的返回型別必須是Task<T>,而且呼叫會獲取到返回值後才會繼續執行下去。如果僅僅是呼叫一下非同步方法,不和非同步方法做其他互動,則將非同步方法簽名返回值為void,這種呼叫形式也被稱為“呼叫並忘記”。

async static void GetContentAsync(string fileName)
        {
            Console.WriteLine($"當前執行緒ID是{Thread.CurrentThread.ManagedThreadId}");
            Console.WriteLine($"開始讀取檔案:{DateTime.Now}");
            Thread.Sleep(1000);
            using(StreamReader sr = new StreamReader(fileName))
            {
                string program = await sr.ReadToEndAsync();
                Console.WriteLine($"讀取檔案結束:{DateTime.Now}");
            }
        }
static void Main(string[] args)
        {
            string path = @"D:\Demos\TaskDemo\postdata.txt";
            Console.WriteLine($"主執行緒ID是{Thread.CurrentThread.ManagedThreadId}");
            Console.WriteLine($"主程式執行開始:{DateTime.Now}");
            GetContentAsync(path);
            Console.WriteLine($"主程式執行結束:{DateTime.Now}");
            Console.Read();
}

執行結果: