快速上手C#中的Task編程

.NET4.0開始引入Task,它的出現大大簡化了異步編程的複雜度,相較於傳統的ThreadThreadPoolTask更加容易控制和使用,下面就來看看它的具體用法。

1、一個簡單的串行程序

串行程序大家肯定不陌生,說白了就是從上到下按順序執行,看下面一段代碼:

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            MethodA();
            MethodB();
            Console.WriteLine("Hello World");
            Console.ReadKey();
        }

        static void MethodA()
        {
            Thread.Sleep(4000);
            Console.WriteLine("This is MethodA");
        }

        static void MethodB()
        {
            Thread.Sleep(2000);
            Console.WriteLine("This is MethodB");
        }
    }
}

運行結果如下所示:

This is MethodA
This is MethodB
Hello World

這段代碼很簡單,首先執行MethodA,然後執行MethodB,最後輸出Hello World,想必大家對運行結果應該沒什麼疑問。但我們也需要考慮一個問題:在串行程序中,如果每個方法都耗時很長時間,一定會讓主程序處於卡死狀態,那麼能否讓這些耗時的方法同時執行,使其不影響之後的程序運行呢?。答案當然是可以的,這個時候我們就需要使用Task來解決問題,下面就來看看Task是怎麼解決這個問題的。

2、一個簡單的Task程序

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            Task a = new Task(MethodA);
            Task b = new Task(MethodB);
            a.Start();
            b.Start();
            Console.WriteLine("Hello World");
            Console.ReadKey();
        }

        static void MethodA()
        {
            Thread.Sleep(4000);
            Console.WriteLine("This is MethodA");
        }

        static void MethodB()
        {
            Thread.Sleep(2000);
            Console.WriteLine("This is MethodB");
        }
    }
}

運行結果如下所示:

Hello World
This is MethodB
This is MethodA

是不是感覺很驚喜?從運行結果來看,Task並沒有阻礙之後程序的運行,如果遇到一個耗時的方法,那就把它放進Task吧。

3、Task的幾種創建方式

創建Task的方式很多,主要就是new Task()Task.Factory.StartNew()Task.Run()這三種方式,下面的代碼演示瞭如何創建Task

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            // 第一種創建方式
            Task a = new Task(Method);
            a.Start();

            // 第二種創建方式
            Task b = new Task(() =>
            {
                Thread.Sleep(3000);
                Console.WriteLine("This is B");
            });
            b.Start();

            // 第三種創建方式
            Task c = Task.Factory.StartNew(() =>
            {
                Thread.Sleep(2000);
                Console.WriteLine("This is C");
            });

            // 第四種創建方式
            Task d = Task.Run(() =>
            {
                Thread.Sleep(1000);
                Console.WriteLine("This is D");
            });

            Console.WriteLine("Hello World");
            Console.ReadKey();
        }

        static void Method()
        {
            Thread.Sleep(4000);
            Console.WriteLine("This is A");
        }
    }
}

運行結果如下所示:

Hello World
This is D
This is C
This is B
This is A

4、創建帶返回值的Task

在前面的demo裏,我們創建的Task都沒有返回值,但有些時候我們也需要創建一些帶返回值的Task,下面的代碼演示瞭如何創建帶返回值的Task

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            // 第一種創建方式
            Task<string> a = new Task<string>(() =>
            {
                Thread.Sleep(4000);
                return "This is A";
            });
            a.Start();
            Console.WriteLine(a.Result);

            // 第二種創建方式
            Task<double> b = Task<double>.Factory.StartNew(() =>
            {
                Thread.Sleep(3000);
                return 1;
            });
            Console.WriteLine(b.Result);

            // 第三種創建方式
            Task<bool> c = Task<bool>.Run(() =>
            {
                return DateTime.Now.Year == 2020;
            });
            Console.WriteLine(c.Result);

            Console.WriteLine("Hello World");
            Console.ReadKey();
        }
    }
}

我們一般使用Result屬性獲取一個Task執行後的結果,運行結果如下所示:

This is A
1
True
Hello World

這裏你可能會發現一個問題,我們使用的不是Task嗎?爲什麼這段代碼是按照順序執行的?這好像跟串行程序沒什麼區別啊!這是因爲:task.Result取值的時候,後臺線程還沒執行完,則會等待其執行完畢!,想讓帶返回值的Task像不帶返回值的Task那樣執行,就需要使用asyncawait這兩個關鍵字,後面會進行說明。

5、等待所有任務完成——Task.WaitAll

有時候我們希望等所有的Task執行完畢後再執行之後的代碼,這就需要用到Task.WaitAll方法,看下面一段代碼:

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            // 任務a
            Task a = Task.Run(() =>
            {
                Thread.Sleep(4000);
                Console.WriteLine("This is A");
            });

            // 任務b
            Task b = Task.Run(() =>
            {
                Thread.Sleep(2000);
                Console.WriteLine("This is B");
            });

            // 等待兩個任務完成
            Task.WaitAll(a, b);
            Console.WriteLine("等待全部任務完成纔會輸出");
            Console.ReadKey();
        }
    }
}

運行結果如下所示:

This is B
This is A
等待全部任務完成纔會輸出

有時候我們也會給Task.WaitAll方法規定一個時間期限,如果超出了這個時間期限則執行其他代碼,在下面這段代碼中,規定Task.WaitAll方法的時間期限爲3秒,如果超出了3秒,則輸出Hello World

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            // 任務a
            Task a = Task.Run(() =>
            {
                Thread.Sleep(5000);
                Console.WriteLine("This is A");
            });

            // 任務b
            Task b = Task.Run(() =>
            {
                Thread.Sleep(4000);
                Console.WriteLine("This is B");
            });

            // 等待兩個任務完成,如果3秒內未完成則輸出Hello World
            bool finish = Task.WaitAll(new Task[] { a, b }, 3000);
            if (!finish)
            {
                Console.WriteLine("Hello World");
            }
            Console.ReadKey();
        }
    }
}

運行結果如下所示:

Hello World
This is B
This is A

6、等待其中一個任務完成——Task.WaitAny

Task.WaitAll方法不同,Task.WaitAny方法不需要等待所有任務完成,只要其中的一個任務完成就會繼續執行之後的代碼,下面這段代碼演示了Task.WaitAny的使用方法:

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            // 任務a
            Task a = Task.Run(() =>
            {
                Thread.Sleep(4000);
                Console.WriteLine("This is A");
            });

            // 任務b
            Task b = Task.Run(() =>
            {
                Thread.Sleep(2000);
                Console.WriteLine("This is B");
            });

            // 等待其中一個任務完成
            Task.WaitAny(a, b);
            Console.WriteLine("等待其中一個任務完成就會輸出");
            Console.ReadKey();
        }
    }
}

運行結果如下所示:

This is B
等待其中一個任務完成就會輸出
This is A

7、按順序執行Task

有時候我們希望創建的任務按照一定的順序執行,比如A任務結束後才執行B任務,B任務結束後再執行C任務,這就需要用到ContinueWith方法,看下面一段代碼:

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            // 連續任務
            Task task = Task.Run(() =>
            {
                Thread.Sleep(4000);
                Console.WriteLine("This is A");
            })
            .ContinueWith(t =>
            {
                Thread.Sleep(3000);
                Console.WriteLine("This is B");
            })
            .ContinueWith(t =>
            {
                Thread.Sleep(2000);
                Console.WriteLine("This is C");
            })
            .ContinueWith(t =>
            {
                Thread.Sleep(1000);
                Console.WriteLine("This is D");
            });

            Console.WriteLine("Hello World");
            Console.ReadKey();
        }
    }
}

運行結果如下所示:

Hello World
This is A
This is B
This is C
This is D

8、async與await

.NET4.5開始引入了asyncawait這兩個關鍵字,這兩個關鍵字的出現大大簡化了異步編程的難度,使用這兩個關鍵字的時候需要注意:async和await成對出現纔有意義async只能修飾void、Task、Task<T>這三種返回類型的方法await只能修飾Task和Task<T>

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            Print();
            Console.WriteLine("Hello World");
            Console.ReadKey();
        }

        static async void Print()
        {
            string a = await GetMessageOneAsync();
            string b = await GetMessageTwoAsync();
            Console.WriteLine(a);
            Console.WriteLine(b);
        }

        static async Task<string> GetMessageOneAsync()
        {
            return await Task<string>.Run(() =>
            {
                Thread.Sleep(5000);
                return "This is A";
            });
        }

        static async Task<string> GetMessageTwoAsync()
        {
            return await Task<string>.Run(() =>
            {
                Thread.Sleep(3000);
                return "This is B";
            });
        }
    }
}

運行結果如下所示:

Hello World
This is A
This is B

9、一個WinForm中使用async、await的實例

我在這裏做了一個WinForm的demo,界面如下圖所示:
在這裏插入圖片描述
代碼如下所示:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Diagnostics;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace WindowsFormsApplication1
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        // 按鈕一
        private void button1_Click(object sender, EventArgs e)
        {
            int a = GetParameterOne();
            int b = GetParameterTwo();
            textBox1.Text = (a + b).ToString();
        }

        // 按鈕二
        private void button2_Click(object sender, EventArgs e)
        {
            MessageBox.Show("主線程提示框", "提示", MessageBoxButtons.OK, MessageBoxIcon.Asterisk);
        }

        // 假設獲取第一個參數花費了5秒
        private int GetParameterOne()
        {
            Thread.Sleep(5000);
            return 20;
        }

        // 假設獲取第二個參數花費了3秒
        private int GetParameterTwo()
        {
            Thread.Sleep(3000);
            return 50;
        }
    }
}

運行之後你會發現:點擊按鈕一,整個界面處於假死狀態,窗體無法拖動,按鈕二無法點擊,對於用戶來說這顯然是無法接受的,因此我們希望的效果是:點擊按鈕一,窗體仍舊可以拖動,按鈕二不受干擾,點擊後能彈出提示框。下面我們就來試試用asyncawait來解決,代碼如下:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Diagnostics;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace WindowsFormsApplication1
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        // 按鈕一
        private async void button1_Click(object sender, EventArgs e)
        {
            int a = await GetParameterOneAsync();
            int b = await GetParameterTwoAsync();
            textBox1.Text = (a + b).ToString();
        }

        // 按鈕二
        private void button2_Click(object sender, EventArgs e)
        {
            MessageBox.Show("主線程不受干擾", "提示", MessageBoxButtons.OK, MessageBoxIcon.Asterisk);
        }

        // 假設獲取第一個參數花費了5秒
        private async Task<int> GetParameterOneAsync()
        {
            return await Task<int>.Run(() =>
            {
                Thread.Sleep(5000);
                return 20;
            });
        }

        // 假設獲取第二個參數花費了3秒
        private async Task<int> GetParameterTwoAsync()
        {
            return await Task<int>.Run(() =>
            {
                Thread.Sleep(3000);
                return 50;
            });
        }
    }
}

運行之後你會發現:點擊按鈕一,窗體可以拖動,按鈕二可以點擊,點擊後能彈出對話框,因此這種情況下我們可以考慮使用asyncawait來提高用戶體驗。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章