同步調用,異步調用,異步回調,Task任務


一:創建加法類

//定義委托
    public delegate int AddHandler(int a, int b);

    class AddMethod
    {
        public static int Add(int a, int b)
        {
            Console.WriteLine("開始計算:" + a + "+" + b);
            Thread.Sleep(3000); //模擬該方法運行三秒
            Console.WriteLine("計算完成!");
            return a + b;
        }
    }

二.同步調用

委托的Invoke方法用來進行同步調用。同步調用也可以叫阻塞調用,它將阻塞當前線程,然后執行調用,調用完畢后再繼續向下進行。

class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("===== 同步調用 SyncInvokeTest =====");
            AddHandler handler = new AddHandler(AddMethod.Add);
            int result = handler.Invoke(1, 2);

            Console.WriteLine("繼續做別的事情。。。");

            Console.WriteLine(result);
            Console.ReadKey();
        }
    

運行結果:

同步調用會阻塞線程,如果是要調用一項繁重的工作(如大量IO操作),可能會讓程序停頓很長時間,造成糟糕的用戶體驗,這時候異步調用就很有必要了。

三.異步調用

1.BeginInvoke方法和EndInvoke方法

BeginInvoke方法觸發你的異步方法,它和你想要執行的異步方法有相同的參數。另外還有兩個可選參數,第一個是AsyncCallback委托是異步完成的回調方法。第二個是用戶自定義對象,該對象將傳遞到回調方法中。BeginInvoke立即返回並且不等待完成異步的調用(繼續執行該下面的代碼,不需要等待)。BeginInvoke返回IAsyncResult接口,可用於檢測異步調用的過程。

通過EndInvoke方法檢測異步調用的結果。如果異步調用尚未完成,EndInvoke將阻塞調用線程,直到它完成。EndInvoke參數包括out和ref參數。

下面代碼演示使用BeginInvoke和EndInvoke進行異步調用的四種常見方式。在調用BeginInvoke可以做以下工作:

  1. 做一些其他操作,然后調用EndInvoke方法阻塞線程直到該方法完成。
  2. 使用IAsyncResult.AsyncWaitHandle屬性,使用它的WaitOne方法阻塞線程直到收到WaitHandle信號,然后調用EndInvoke。
  3. 檢查BeginInvoke返回值IAsyncResult的狀態來決定方法是否完成,然后調用EndInvoke方法。
  4. 通過在BeginInvoke方法中傳遞該委托,在回調方法中調用該委托的EenInvoke方法。

注意

無論你怎么使用,都必須調用EndInvoke方法結束你的異步調用。

2.異步調用

異步調用不阻塞線程,而是把調用塞到線程池中,程序主線程或UI線程可以繼續執行。
委托的異步調用通過BeginInvoke和EndInvoke來實現。

class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("===== 異步調用 AsyncInvokeTest =====");

            AddHandler handler = new AddHandler(AddMethod.Add);//委托

            //IAsyncResult: 異步操作接口(interface)
            //BeginInvoke: 委托(delegate)的一個異步方法的開始,調用BeginInvoke方法,做一些其他操作(主線程繼續),然后調用EndInvoke方法阻塞線程(阻塞主線程)直到該方法完成。
            IAsyncResult result = handler.BeginInvoke(1, 2, null, null);

            Console.WriteLine("繼續做別的事情。。。");

       //調用EndInvoke,等待異步執行完成 Console.WriteLine("等待異步方法TestMethodAsync執行完成");
       //等待異步執行完畢信號
       //ressult.AsyncWaitHandle.WaitOne();
       //Console.WriterLine("收到WaitHandle信號");

//異步操作返回,無論怎么使用,都必須調用EndInvoke方法結束你的異步調用 Console.WriteLine(handler.EndInvoke(result)); Console.ReadKey(); } }

運行結果:

 

 可以看到,主線程並沒有等待,而是直接向下運行了。
但是問題依然存在,當主線程運行到EndInvoke時,如果這時調用沒有結束(這種情況很可能出現),這時為了等待調用結果,線程依舊會被阻塞。

注意:

  如果創建的是NET.Core項目,則會報一下錯誤:

因為在.NET Core中不支持Action.BeginInvoke(null, null)的委托異步調用方法

重新創建一個.NET Framework項目(測試用的是4.5.2),把代碼重新復制過去后,就沒有異常了。

3.Task

  任務(Task表示一個通過或不通過線程實現的並發操作任務是可組合的使用延續(continuation可將它們串聯在一起,它們可以使用線程池減少啟動延遲,可使用回調方法避免多個線程同時等待I/O密集操作。

  Task 類的表示單個操作不返回一個值,通常以異步方式執行。 Task 對象是一個的中心思想 基於任務的異步模式

3.1Task和ThreadPool的區別

  1.任務是架構在線程之上的,也就是說任務最終還是要拋給線程去執行;

  2.任務和線程不是一對一的關系,比如開10個任務並不是說會開10個線程,這點任務有點類似線程池,但是任務相比線城池有很小的開銷和精確的控制;

ThreadPool相對於Thread來說可以減少線程的創建,有效減小系統開銷;但是ThreadPool不能控制線程的執行順序,我們也不能獲取線程池內線程取消/異常/完成的通知,即我們不能有效監控和控制線程池中的線程。

3.2Task的創建和運行 

class Program
    {
        static void Main(string[] args)
        {
            //1.new方式實例化一個Task,需要通過Start方法啟動
            Task task = new Task(() =>
            {
                Thread.Sleep(100);
                Console.WriteLine($"hello, task1的線程ID為{Thread.CurrentThread.ManagedThreadId}");
            });
            task.Start();

            //2.Task.Factory.StartNew(Action action)創建和啟動一個Task
            Task task2 = Task.Factory.StartNew(() =>
            {
                Thread.Sleep(100);
                Console.WriteLine($"hello, task2的線程ID為{ Thread.CurrentThread.ManagedThreadId}");
            });

            //3.Task.Run(Action action)將任務放在線程池隊列,返回並啟動一個Task
            Task task3 = Task.Run(() =>
            {
                Thread.Sleep(100);
                Console.WriteLine($"hello, task3的線程ID為{ Thread.CurrentThread.ManagedThreadId}");
            });
            Console.WriteLine("執行主線程!");
            Console.ReadKey();
        }
    }

運行結果:

 

我們看到先打印"執行主線程",然后再打印各個任務,說明了Task不會阻塞主線程。上邊的例子Task都沒有返回值,我們也可以創建有返回值的~task,用法和沒有返回值的基本一致,我們簡單修改一下上邊的例子,代碼如下:

class Program
    {
        static void Main(string[] args)
        {
            //1.new方式實例化一個Task,需要通過Start方法啟動
            Task<string> task = new Task<string>(() =>
            {
                return $"hello, task1的ID為{Thread.CurrentThread.ManagedThreadId}";
            });
            task.Start();

            //2.Task.Factory.StartNew(Func func)創建和啟動一個Task
           Task<string> task2 = Task.Factory.StartNew<string>(() =>
           {
               return $"hello, task2的ID為{ Thread.CurrentThread.ManagedThreadId}";
           });

            //3.Task.Run(Func func)將任務放在線程池隊列,返回並啟動一個Task
           Task<string> task3 = Task.Run<string>(() =>
           {
               return $"hello, task3的ID為{ Thread.CurrentThread.ManagedThreadId}";
           });

            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任務,代碼如下:

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

運行結果:

 

 

異步執行:

Task task = new Task(() =>
            {
                Thread.Sleep(100);
                Console.WriteLine("執行Task結束!");
            });
            //同步執行,task會阻塞主線程
            task.Start();
            Console.WriteLine("執行主線程結束!");
            Console.ReadKey();

運行結果:

 

 3.3Task任務控制

 

 

 3.3.1 阻塞方法

1.Thread阻塞線程的方法:

static void Main(string[] args)
        {
            Thread th1 = new Thread(() => {
                Thread.Sleep(500);
                Console.WriteLine("線程1執行完畢!");
            });
            th1.Start();

            Thread th2 = new Thread(() => {
                Thread.Sleep(500);
                Console.WriteLine("線程2執行完畢");
            });
            th2.Start();

            th1.Join();
            th2.Join();

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

如果注釋掉兩個Join,執行結果是:先打印【主線程執行完畢】,而添加兩個Join方法后執行結果如下,實現了線程阻塞:

 

 

Thread使用join方法可以阻塞調用線程,但是有弊端:

  (1)如果實現很多線程的阻塞時,每個線程都要調用一次join方法;

  (2)如果想讓所有線程執行完畢(或任一線程執行完畢時),立即解除阻塞,使用join方法不容易實現。

 

2.Task阻塞線程的方法

Task提供了 Wait/WaitAny/WaitAll 方法,可以更方便地控制線程阻塞。

task.Wait() 表示等待task執行完畢,功能類似於thead.Join()Task.WaitAll(Task[] tasks) 表示只有所有的task都執行完成了再解除阻塞;Task.WaitAny(Task[] tasks)表示只要有一個task執行完畢就解除阻塞;

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.阻塞方法:

(1)Task.Wait()

Task1.Wait():就是等待任務執行(task1)完成,task1的狀態變為Completed。

(2)Task.WaitAll()

等待所有的任務都執行完成。即當task,task2,task3…N全部任務都執行完成之后才會往下執行代碼(打印出:“主線程執行完畢!”)。

(3)Task.WaitAny()

同Task.WaitAll,就是等待任何一個任務完成就繼續向下執行

3.3.2 Task的延續操作

Wait/WaitAny/WaitAll方法返回值為void,這些方法單純的實現阻塞線程。我們現在想讓所有task執行完畢(或者任一task執行完畢)后,開始執行后續操作,怎么實現呢?這時就可以用到WhenAny/WhenAll方法了,這些方法執行完成返回一個task實例。 task.WhenAll(Task[] tasks) 表示所有的task都執行完畢后再去執行后續的操作, task.WhenAny(Task[] tasks) 表示任一task執行完畢后就開始執行后續操作;

 

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) 來實現 ,修改上邊例子代碼如下,執行結果不變:

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();
        }
    }

第一個Task完成后自動啟動下一個Task,實現Task的延續;

3.3.3Task取消

Thread取消任務執行:Thread取消任務執行的一般流程:設置一個變量來控制任務是否停止,如設置一個變量isStop,然后線程輪詢查看isStop,如果isStop為true就停止;

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();
        }

運行結果:

 

 

 

Task任務取消:

Task中有一個專門的類 CancellationTokenSource 來取消任務執行;

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會被執行。

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()判斷,任務已經執行中了:

 

 

跨線程實例:

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

        private void button1_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);
                }
            });
        }
    }

運行結果:

 

 3.4異步方法(asyc/await)

獲取文件內容示例:

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;
        }
    }

運行結果:

 

 注意一個小問題:異步方法中方法簽名返回值為Task,代碼中的返回值為T。上邊栗子中GetContentAsync的簽名返回值為Task,而代碼中返回值為string。牢記這一細節對我們分析異步代碼很有幫助。

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

① Task:如果調用方法想通過調用異步方法獲取一個T類型的返回值,那么簽名必須為Task;

② Task:如果調用方法不想通過異步方法獲取一個值,僅僅想追蹤異步方法的執行狀態,那么我們可以設置異步方法簽名的返回值為Task;

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

3.5工作示例

Task任務:

//沒有返回值得Task任務 
private async Task GetDDPT(RequestPram reqPram, DataList repPram)
        {
            await Task.Run(() => {
                try
                {
                  //業務代碼。。。
             
                }
                catch (Exception ex)
                { }
            });
        }    
//沒有返回值得Task任務 
private async Task GetDDPT(RequestPram reqPram, DataList repPram)
        {
            await Task.Run(() => {
                try
                {
                  //業務代碼。。。
             
                }
                catch (Exception ex)
                { }
            });
        }    
//沒有返回值得Task任務 
private async Task GetDDPT(RequestPram reqPram, DataList repPram)
        {
            await Task.Run(() => {
                try
                {
                  //業務代碼。。。
             
                }
                catch (Exception ex)
                { }
            });
        }    

調用:

[HttpPost]
public async Task<ReponsePram> QueryInsulateData(RequestPram pram)
        {
            ReponsePram reponsePrams = new ReponsePram();
            reponsePrams.code = "200";
            reponsePrams.msg = "數據查詢成功";

            DataList dataList = new DataList();

            //times.Start();
            var taskDdpt = GetDDPT(pram, dataList);
            var taskPangGu = GetPanGu(pram, dataList);
            var taskMoEr = GetMoEr(pram, dataList);
            var tasks = new[] { taskDdpt, taskPangGu, taskMoEr };//任務組
            var process = tasks.Select(async ts =>              
            {
                await ts;
            });
            await Task.WhenAll(process);                         //等待處理完全
                                                                 // times.Stop();

          //業務代碼
    
            return reponsePrams;
        }

 

 

三.異步回調

 1.異步調用

  static void Main(string[] args)
        {
            Console.WriteLine("===== 異步調用 AsyncInvokeTest =====");

            AddHandler handler = new AddHandler(AddMethod.Add);//委托

            //IAsyncResult: 異步操作接口(interface)
            //BeginInvoke: 委托(delegate)的一個異步方法的開始,調用BeginInvoke方法,做一些其他操作(主線程繼續),然后調用EndInvoke方法阻塞線程(阻塞主線程)直到該方法完成。
            IAsyncResult result = handler.BeginInvoke(1, 2, null, null);

            Console.WriteLine("繼續做別的事情。。。");

            //異步操作返回,無論怎么使用,都必須調用EndInvoke方法結束你的異步調用
            Console.WriteLine(handler.EndInvoke(result));
            Console.WriteLine("異步調用方法結束后,繼續做其他事!");
            Console.ReadKey();
        }

運行結果:

 

 即調用EndInvoke方法時,這是會阻塞主線程,直到異步方法執行完畢,主線程才會繼續執行下去!

2.異步回調

用回調函數,當調用結束時會自動調用回調函數,解決了為等待調用結果,而讓線程依舊被阻塞的局面;

 static void Main(string[] args)
        {
            Console.WriteLine("===== 異步回調 AsyncInvokeTest =====");
            AddHandler handler = new AddHandler(AddMethod.Add);
            Console.WriteLine("主線程做第一件事!");

            //異步操作接口(注意BeginInvoke方法的不同!)
            IAsyncResult result = handler.BeginInvoke(1, 2, new AsyncCallback(CallBackMethod), "AsycState:OK");

            Console.WriteLine("繼續做別的事情,主線程不會被阻塞。。。");
            Console.ReadKey();
        }

        static void CallBackMethod(IAsyncResult result)
        {      //result 是“加法類.Add()方法”的返回值

            //AsyncResult 是IAsyncResult接口的一個實現類,空間:System.Runtime.Remoting.Messaging
            //AsyncDelegate 屬性可以強制轉換為用戶定義的委托的實際類。
            AddHandler handler = (AddHandler)((AsyncResult)result).AsyncDelegate;
            Console.WriteLine(handler.EndInvoke(result));
            Console.WriteLine(result.AsyncState);
        }

 運行結果:

 

  我定義的委托的類型為AddHandler,則為了訪問 AddHandler.EndInvoke,必須將異步委托強制轉換為 AddHandler。可以在異步回調函數(類型為 AsyncCallback)中調用 MAddHandler.EndInvoke,以獲取最初提交的 AddHandler.BeginInvoke 的結果。

 

問題:

(1)int result = handler.Invoke(1,2);
為什么Invoke的參數和返回值和AddHandler委托是一樣的呢?
答:
Invoke方法的參數很簡單,一個委托,一個參數表(可選),而Invoke方法的主要功能就是幫助你在UI線程上調用委托所指定的方法。Invoke方法首先檢查發出調用的線程(即當前線程)是不是UI線程,如果是,直接執行委托指向的方法,如果不是,它將切換到UI線程,然后執行委托指向的方法。不管當前線程是不是UI線程,Invoke都阻塞直到委托指向的方法執行完畢,然后切換回發出調用的線程(如果需要的話),返回。
所以Invoke方法的參數和返回值和調用他的委托應該是一致的。

(2)IAsyncResult result = handler.BeginInvoke(1,2,null,null);

BeginInvoke : 開始一個異步的請求,調用線程池中一個線程來執行,
返回IAsyncResult 對象(異步的核心). IAsyncResult 簡單的說,他存儲異步操作的狀態信息的一個接口,也可以用他來結束當前異步。
注意: BeginInvoke和EndInvoke必須成對調用.即使不需要返回值,但EndInvoke還是必須調用,否則可能會造成內存泄漏。

 

(3)IAsyncResult.AsyncState 屬性:
獲取用戶定義的對象,它限定或包含關於異步操作的信息。 例如:

static void AddComplete(IAsyncResult result) 
{   
      AddHandler handler = (AddHandler)result.AsyncState;    
      Console.WriteLine(handler.EndInvoke(result)); 
      。。。。。
}

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM