1. 程式人生 > 其它 >小程式低成本生成App技術,引流與留存兩不誤

小程式低成本生成App技術,引流與留存兩不誤

原文連結:https://blog.csdn.net/btfireknight/article/details/97766193

一、什麼是非同步

當一個方法被呼叫時,呼叫者需要等待該方法執行完畢並返回才能繼續執行,我們稱這個方法是同步方法;

當一個方法被呼叫時立即返回,並獲取一個執行緒執行該方法內部的業務,呼叫者不用等待該方法執行完畢,我們稱這個方法為非同步方法。

非同步的好處在於非阻塞(呼叫執行緒不會暫停執行去等待子執行緒完成),因此我們把一些不需要立即使用結果、較耗時的任務設為非同步執行,可以提高程式的執行效率。

.Net Framework 4.0在ThreadPool的基礎上推出了Task類,微軟極力推薦使用Task來執行非同步任務,現在C#類庫中的非同步方法基本都用到了Task;

NET 5.0推出了async/await,讓非同步程式設計更為方便。

本篇主要介紹Task、async/await相關的內容

二、Task介紹

  Task是在ThreadPool的基礎上推出的。

ThreadPool中有若干數量的執行緒,如果有任務需要處理時,會從執行緒池中獲取一個空閒的執行緒來執行任務,任務執行完畢後執行緒不會銷燬,而是被執行緒池回收以供後續任務使用。

當執行緒池中所有的執行緒都在忙碌時,又有新任務要處理時,執行緒池才會新建一個執行緒來處理該任務,如果執行緒數量達到設定的最大值,任務會排隊,等待其他任務釋放執行緒後再執行。

using System;
using System.Threading;

namespace ThreadPoolDemo { class Program { static void Main(string[] args) { for (int i = 1; i <= 10; i++) { ThreadPool.QueueUserWorkItem(new WaitCallback((obj) => { Console.WriteLine($"第{obj}個執行任務
"); }),i); } Console.ReadKey(); } } }

執行結果:

 ThreadPool相對於Thread來說可以減少執行緒的建立,有效減小系統開銷;

但是ThreadPool不能控制執行緒的執行順序,我們也不能獲取執行緒池內執行緒取消/異常/完成的通知,即我們不能有效監控和控制執行緒池中的執行緒。

1、Task建立和執行

  我們知道了ThreadPool的弊端:我們不能控制執行緒池中執行緒的執行順序,也不能獲取執行緒池內執行緒取消/異常/完成的通知。net 4.0在ThreadPool的基礎上推出了Task,Task擁有執行緒池的優點,同時也解決了使用執行緒池不易控制的弊端。首先看一下怎麼去建立並執行一個Task,Task的建立和執行方式有如下三種:

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

namespace TaskDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            #region 方式一:NEW例項化一個Task,通過Start方法啟動
            Task task = new Task(() =>
            {
                Thread.Sleep(100);
                Console.WriteLine($"NEW例項化一個task,執行緒ID為{Thread.CurrentThread.ManagedThreadId}");
            });
            task.Start();
            #endregion

            #region 方式二:Task.Factory.StartNew(Action action)建立和啟動一個Task           
            Task task2 = Task.Factory.StartNew(() =>
            {
                Thread.Sleep(100);
                Console.WriteLine($"Task.Factory.StartNew方式建立一個task,執行緒ID為{ Thread.CurrentThread.ManagedThreadId}");
            });
            #endregion

            #region 方式三:Task.Run(Action action)將任務放線上程池佇列,返回並啟動一個Task
            Task task3 = Task.Run(() =>
            {
                Thread.Sleep(100);
                Console.WriteLine($"Task.Run方式建立一個task,執行緒ID為{ Thread.CurrentThread.ManagedThreadId}");
            });
            #endregion
            Console.WriteLine("執行主執行緒!");
            Console.ReadKey();
        }
    }
}
執行結果如下:

 我們看到先列印"執行主執行緒",然後再列印各個任務,說明了Task不會阻塞主執行緒。

上邊的例子Task都沒有返回值,我們也可以建立有返回值的task,用法和沒有返回值的基本一致 

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

namespace TaskParamDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            #region 方式一:NEW例項化一個Task,通過Start方法啟動
            Task<string> task = new Task<string>(() =>
            {
                return $"NEW例項化一個task,執行緒ID為{Thread.CurrentThread.ManagedThreadId}";
            });
            task.Start();
            #endregion

            #region 方式二:Task.Factory.StartNew(Action action)建立和啟動一個Task
            Task<string> task2 = Task.Factory.StartNew<string>(() =>
            {
                return $"Task.Factory.StartNew方式建立一個task,執行緒ID為{ Thread.CurrentThread.ManagedThreadId}";
            });
            #endregion
            #region 方式三:Task.Run(Action action)將任務放線上程池佇列,返回並啟動一個Task
            Task<string> task3 = Task.Run<string>(() =>
            {
                return $"Task.Run方式建立一個task,執行緒ID為{ Thread.CurrentThread.ManagedThreadId}";
            });
            #endregion
            Console.WriteLine("執行主執行緒!");
            Console.WriteLine(task.Result);
            Console.WriteLine(task2.Result);
            Console.WriteLine(task3.Result);
            Console.ReadKey();
        }
    }
}

注意task.Resut獲取結果時會阻塞執行緒,即如果task沒有執行完成,會等待task執行完成獲取到Result,然後再執行後邊的程式碼,程式執行結果如下:

 上邊的所有例子中Task的執行都是非同步的,不會阻塞主執行緒。有些場景下我們想讓Task同步執行怎麼辦呢?Task提供了 task.RunSynchronously()用於同步執行Task任務,程式碼如下:

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

namespace TaskDemo1
{
    class Program
    {
        static void Main(string[] args)
        {
            Task task = new Task(() =>
            {
                Thread.Sleep(100);
                Console.WriteLine("執行Task結束!");
            });           
            //同步執行,task會阻塞主執行緒
            task.RunSynchronously();
            Console.WriteLine("執行主執行緒結束!");
            Console.ReadKey();
        }
    }
}

執行結果如下:

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

(1)Thread阻塞執行緒的方法

  使用Thread時,我們知道用thread.Join()方法即可阻塞主執行緒。看一個例子:

using System;
using System.Threading;

namespace TaskDemo1
{
    class Program
    {
        static void Main(string[] args)
        {
            Thread th1 = new Thread(() => {
                Thread.Sleep(500);
                Console.WriteLine("執行緒1執行完畢!");
            });
            th1.Start();
            Thread th2 = new Thread(() => {
                Thread.Sleep(1000);
                Console.WriteLine("執行緒2執行完畢!");
            });
            th2.Start();
            //阻塞主執行緒
            th1.Join();
            th2.Join();
            Console.WriteLine("主執行緒執行完畢!");
            Console.ReadKey();
        }
    }
}

執行結果:

 Thread的Join方法可以阻塞呼叫執行緒,但是有一些弊端:

①如果我們要實現很多執行緒的阻塞時,每個執行緒都要呼叫一次Join方法;

②如果我們想讓所有的執行緒執行完畢(或者任一執行緒執行完畢)時,立即解除阻塞,使用Join方法不容易實現。

(2)Task提供了 Wait/WaitAny/WaitAll 方法,可以更方便地控制執行緒阻塞。

task.Wait() 表示等待task執行完畢,功能類似於thead.Join(); 

Task.WaitAll(Task[] tasks) 表示只有所有的task都執行完成了再解除阻塞;

Task.WaitAny(Task[] tasks)表示只要有一個task執行完畢就解除阻塞,看一個例子:

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

namespace TaskDemo1
{
    class Program
    {
        static void Main(string[] args)
        {
            Task task1 = new Task(() => {
                Thread.Sleep(500);
                Console.WriteLine("執行緒1執行完畢!");
            });
            task1.Start();
            Task task2 = new Task(() => {
                Thread.Sleep(1000);
                Console.WriteLine("執行緒2執行完畢!");
            });
            task2.Start();
            //阻塞主執行緒。task1,task2都執行完畢再執行主執行緒
            //執行【task1.Wait();task2.Wait();】可以實現相同功能
            Task.WaitAll(new Task[] { task1, task2 });
            Console.WriteLine("主執行緒執行完畢!");
            Console.ReadKey();
        }
    }
}

執行結果:

如果將例子中的WaitAll換成WaitAny,那麼任一task執行完畢就會解除執行緒阻塞,執行結果是:先列印【執行緒1執行完畢】,然後列印【主執行緒執行完畢】,最後列印【執行緒2執行完畢】

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

  上邊的Wait/WaitAny/WaitAll方法返回值為void,這些方法單純的實現阻塞執行緒。我們現在想讓所有task執行完畢(或者任一task執行完畢)後,開始執行後續操作,怎麼實現呢?這時就可以用到WhenAny/WhenAll方法了,這些方法執行完成返回一個task例項。 

  task.WhenAll(Task[] tasks) 表示所有的task都執行完畢後再去執行後續的操作

  task.WhenAny(Task[] tasks) 表示任一task執行完畢後就開始執行後續操作。看一個例子:

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

namespace TaskDemo1
{
    class Program
    {
        static void Main(string[] args)
        {
            Task task1 = new Task(() => {
                Thread.Sleep(500);
                Console.WriteLine("執行緒1執行完畢!");
            });
            task1.Start();
            Task task2 = new Task(() => {
                Thread.Sleep(1000);
                Console.WriteLine("執行緒2執行完畢!");
            });
            task2.Start();
            //task1,task2執行完了後執行後續操作
            Task.WhenAll(task1, task2).ContinueWith((t) => {
                Thread.Sleep(100);
                Console.WriteLine("執行後續操作完畢!");
            });

            Console.WriteLine("主執行緒執行完畢!");
            Console.ReadKey();
        }
    }
}

執行結果如下,我們看到WhenAll/WhenAny方法不會阻塞主執行緒,當使用WhenAll方法時所有的task都執行完畢才會執行後續操作;如果把栗子中的WhenAll替換成WhenAny,則只要有一個執行緒執行完畢就會開始執行後續操作,這裡不再演示。

 上邊的例子也可以通過 Task.Factory.ContinueWhenAll(Task[] tasks, Action continuationAction)和 Task.Factory.ContinueWhenAny(Task[] tasks, Action continuationAction) 來實現 ,修改上邊例子程式碼如下,執行結果不變。

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

namespace TaskDemo1
{
    class Program
    {
        static void Main(string[] args)
        {
            Task task1 = new Task(() => {
                Thread.Sleep(500);
                Console.WriteLine("執行緒1執行完畢!");
            });
            task1.Start();
            Task task2 = new Task(() => {
                Thread.Sleep(1000);
                Console.WriteLine("執行緒2執行完畢!");
            });
            task2.Start();
            //通過TaskFactroy實現
            Task.Factory.ContinueWhenAll(new Task[] { task1, task2 }, (t) =>
            {
                Thread.Sleep(100);
                Console.WriteLine("執行後續操作");
            });

            Console.WriteLine("主執行緒執行完畢!");
            Console.ReadKey();
        }
    }
}

執行結果如下:

4 Task的任務取消(CancellationTokenSource)

(1)Thread取消任務執行

  在Task前我們執行任務採用的是Thread,Thread怎麼取消任務呢?一般流程是:設定一個變數來控制任務是否停止,如設定一個變數isStop,然後執行緒輪詢檢視isStop,如果isStop為true就停止,程式碼如下:

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

namespace TaskDemo1
{
    class Program
    {
        static void Main(string[] args)
        {
            bool isStop = false;
            int index = 0;
            //開啟一個執行緒執行任務
            Thread th1 = new Thread(() =>
            {
                while (!isStop)
                {
                    Thread.Sleep(1000);
                    Console.WriteLine($"第{++index}次執行,執行緒執行中...");
                }
            });
            th1.Start();
            //五秒後取消任務執行
            Thread.Sleep(5000);
            isStop = true;
            Console.ReadKey();
        }
    }
}

執行結果:

(2) Task取消任務執行

  Task中有一個專門的類 CancellationTokenSource 來取消任務執行,還是使用上邊的例子,我們修改程式碼如下,程式執行的效果不變。

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

namespace TaskDemo1
{
    class Program
    {
        static void Main(string[] args)
        {
            CancellationTokenSource source = new CancellationTokenSource();
            int index = 0;
            //開啟一個task執行任務
            Task task1 = new Task(() =>
            {
                while (!source.IsCancellationRequested)
                {
                    Thread.Sleep(1000);
                    Console.WriteLine($"第{++index}次執行,執行緒執行中...");
                }
            });
            task1.Start();
            //五秒後取消任務執行
            Thread.Sleep(5000);
            //source.Cancel()方法請求取消任務,IsCancellationRequested會變成true
            source.Cancel();
            Console.ReadKey();
        }
    }
}

執行結果:

 CancellationTokenSource的功能不僅僅是取消任務執行,我們可以使用 source.CancelAfter(5000)實現5秒後自動取消任務,也可以通過 source.Token.Register(Action action)註冊取消任務觸發的回撥函式,即任務被取消時註冊的action會被執行。 看一個例子:

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

namespace TaskDemo1
{
    class Program
    {
        static void Main(string[] args)
        {
            CancellationTokenSource source = new CancellationTokenSource();
            //註冊任務取消的事件
            source.Token.Register(() =>
            {
                Console.WriteLine("任務被取消後執行xx操作!");
            });

            int index = 0;
            //開啟一個task執行任務
            Task task1 = new Task(() =>
            {
                while (!source.IsCancellationRequested)
                {
                    Thread.Sleep(1000);
                    Console.WriteLine($"第{++index}次執行,執行緒執行中...");
                }
            });
            task1.Start();
            //延時取消,效果等同於Thread.Sleep(5000);source.Cancel();
            source.CancelAfter(5000);
            Console.ReadKey();
        }
    }
}
執行結果如下,第5次執行在取消回撥後列印,這是因為,執行取消的時候第5次任務已經通過了while()判斷,任務已經執行中了:

 最後看跨執行緒的例子,點選按鈕啟動一個任務,給tetxtbox賦值,我們把Thread改成Task,程式碼如下:

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

        private void mySetValueBtn_Click(object sender, EventArgs e)
        {
            Task.Run(() =>
            {
                Action<int> setValue = (i) => { myTxtbox.Text = i.ToString(); };
                for (int i = 0; i < 1000000; i++)
                {
                    myTxtbox.Invoke(setValue,i);
                }
            });
        }
    }

執行介面如下,賦值的task不會阻塞UI執行緒:

三、非同步方法(async/await)

  在C#5.0中出現的async和await ,讓非同步程式設計變得更簡單。我們看一個獲取檔案內容的例子:

using System;
using System.IO;
using System.Text;
using System.Threading.Tasks;

namespace AsyncDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            string content = GetContentAsync(Environment.CurrentDirectory + @"/test.txt").Result;
            //呼叫同步方法
            //string content = GetContent(Environment.CurrentDirectory + @"/test.txt");
            Console.WriteLine(content);
            Console.ReadKey();
        }
        //非同步讀取檔案內容
        async static Task<string> GetContentAsync(string filename)
        {
            FileStream fs = new FileStream(filename, FileMode.Open);
            var bytes = new byte[fs.Length];
            //ReadAync方法非同步讀取內容,不阻塞執行緒
            Console.WriteLine("開始讀取檔案");
            int len = await fs.ReadAsync(bytes, 0, bytes.Length);
            string result = Encoding.UTF8.GetString(bytes);
            return result;
        }
        //同步讀取檔案內容
        static string GetContent(string filename)
        {
            FileStream fs = new FileStream(filename, FileMode.Open);
            var bytes = new byte[fs.Length];
            //Read方法同步讀取內容,阻塞執行緒
            int len = fs.Read(bytes, 0, bytes.Length);
            string result = Encoding.UTF8.GetString(bytes);
            return result;
        }
    }
}

執行結果為:

上邊的例子也寫出了同步讀取的方式,將main函式中的註釋去掉即可同步讀取檔案內容。我們可以看到非同步讀取程式碼和同步讀取程式碼基本一致。async/await讓非同步編碼變得更簡單,我們可以像寫同步程式碼一樣去寫非同步程式碼。注意一個小問題:非同步方法中方法簽名返回值為Task,程式碼中的返回值為T。上邊例子中GetContentAsync的簽名返回值為Task,而程式碼中返回值為string。牢記這一細節對我們分析非同步程式碼很有幫助。 

非同步方法簽名的返回值有以下三種:

① Task<T>:如果呼叫方法想通過呼叫非同步方法獲取一個T型別的返回值,那麼簽名必須為Task<T>;

using System;
using System.IO;
using System.Text;
using System.Threading.Tasks;

namespace AsyncDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine($"主程式執行開始:{DateTime.Now}");
            string content = GetContentAsync(Environment.CurrentDirectory + @"/test.txt").Result;
            Console.WriteLine($"主程式輸出的結果:{content}");
            Console.WriteLine($"主程式執行結束:{DateTime.Now}");
            Console.ReadKey();
        }
        //非同步讀取檔案內容
        async static Task<string> GetContentAsync(string filename)
        {
            FileStream fs = new FileStream(filename, FileMode.Open);
            var bytes = new byte[fs.Length];
            //ReadAync方法非同步讀取內容,不阻塞執行緒
            Console.WriteLine($"開始讀取檔案{DateTime.Now}");
            int len = await fs.ReadAsync(bytes, 0, bytes.Length);
            Console.WriteLine($"完成檔案讀取:{DateTime.Now}");
            string result = Encoding.UTF8.GetString(bytes);
            return result;
        }
    }
}

執行結果:

 從上述可以看出,主程式呼叫非同步方法GetContentAsync後,主程式並沒有繼續往下執行,而是等待GetContentAsync執行完,返回結果後才繼續執行。如果呼叫方法要從呼叫中獲取一個T型別的值,非同步方法的返回型別就必須是Task<T>,而且呼叫者會等待結果返回才會繼續往下執行。 

② Task:如果呼叫方法不想通過非同步方法獲取一個值,僅僅想追蹤非同步方法的執行狀態,那麼我們可以設定非同步方法簽名的返回值為Task;

③ void:如果呼叫方法僅僅只是呼叫一下非同步方法,不和非同步方法做其他互動,我們可以設定非同步方法簽名的返回值為void,這種形式也叫做“呼叫並忘記”。

using System;
using System.IO;
using System.Text;
using System.Threading.Tasks;

namespace AsyncDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine($"主程式執行開始:{DateTime.Now}");
            GetContentAsync(Environment.CurrentDirectory + @"/test.txt");           
            Console.WriteLine($"主程式執行結束:{DateTime.Now}");
            Console.ReadKey();
        }
        //非同步讀取檔案內容
        async static void GetContentAsync(string filename)
        {
            FileStream fs = new FileStream(filename, FileMode.Open);
            var bytes = new byte[fs.Length];
            //ReadAync方法非同步讀取內容,不阻塞執行緒
            Console.WriteLine($"開始讀取檔案{DateTime.Now}");
            int len = await fs.ReadAsync(bytes, 0, bytes.Length);
            Console.WriteLine($"完成檔案讀取:{DateTime.Now}");
        }
    }
}

從上述看出,主程式呼叫非同步方法GetContentAsync後,主程式繼續往下執行。如果呼叫方法僅僅只是呼叫一下非同步方法,不和非同步方法做其他互動,我們可以設定非同步方法簽名的返回值為void,而且呼叫者不會等待,而是繼續執行。 

 

四、小結

  通過上邊的介紹,我們知道async/await是基於Task的,而Task是對ThreadPool的封裝改進,主要是為了更有效的控制執行緒池中的執行緒(ThreadPool中的執行緒,我們很難通過程式碼控制其執行順序,任務延續和取消等等);ThreadPool基於Thread的,主要目的是減少Thread建立數量和管理Thread的成本。async/await Task是C#中更先進的,也是微軟大力推廣的特性,我們在開發中可以嘗試使用Task來替代Thread/ThreadPool,處理本地IO和網路IO任務是儘量使用async/await來提高任務執行效率。