none
async/await 非同步請求網路的方法寫法所照成的順序問題 RRS feed

  • 問題

  • 測試環境 Client: .NET 4.6.1 WPF, Server: .NET 4.6.1 ASP.NET MVC Web API 2

    大家好,我正在參考 非同步方法中執行了哪些工作 做非同步的練習,

    這邊遇到一個先取得 Task<Result>後,在使用 awiat task,與直接 await Task<Result> 的順序差異的問題,

    先提到 Server(localhost) Code 是固定的,用來輸出被呼叫後的時間顯示:

        public class TestController : ApiController
        {
            public string Get()
            {
                var time = DateTime.Now.ToString("mm:ss:fff");
                System.Diagnostics.Debug.WriteLine("Get" + time);
                return "Get action";
            }
    
            public string Post(TestDto testDto)
            {
                var time = DateTime.Now.ToString("mm:ss:fff");
                System.Diagnostics.Debug.WriteLine("Post" + time);
                return "Post action: " + testDto?.Name;
            }
    
            public string Put()
            {
                var time = DateTime.Now.ToString("mm:ss:fff");
                System.Diagnostics.Debug.WriteLine("Put" + time);
                return "Put action";
            }
        }

    Client Code1: 都各別將工作指派到某個變數,等待做完 DoShowTip()後,才用 await 變數

            /// <summary>
            /// Gets the TestCommand.
            /// </summary>
            public ICommand TestCommand
            {
                get
                {
                    return _testCommand
                        ?? (_testCommand = new RelayCommand(
                        async () =>
                        {
                            var map = new Dictionary<string, string>
                            {
                                ["Name"] = "Jerry",
                                ["Information"] = "Test"
                            };
                            var postData = new FormUrlEncodedContent(map);
                            var putData = new FormUrlEncodedContent(map);
    
                            var url = "http://localhost:51286/api/test";
                            var client1 = new HttpClient();
                            var client2 = new HttpClient();
                            var client3 = new HttpClient();
    
                            var postTextTask = client1.PostAsync(url, postData);
                            var getTextTask = client2.GetAsync(url);
                            var putTask = client3.PutAsync(url, putData);
    
                            DoShowTip();
    
                            var post = await postTextTask;
                            var get = await getTextTask;
                            var put = await putTask;
    
                            var time = DateTime.Now.ToString("mm:ss:fff");
                            Console.WriteLine(time + post.StatusCode.ToString() + get.StatusCode.ToString() + put.StatusCode.ToString());
                        }));
                }
            }

    執行後,在 Server 端"大部分"都會輸出這樣的順序,分別為 Get 幾乎最常為第一個,連續執行有時可能順序會改變:

    Get09:18:316
    Put09:18:316
    Post09:18:318

    但我以為應該會先輸出 Post ,一直執行的話,Post 仍然會第一個的情況幾乎非常小,

    接著我改為第二種寫法,Client Code2:

            public ICommand TestCommand
            {
                get
                {
                    return _testCommand
                        ?? (_testCommand = new RelayCommand(
                        async () =>
                        {
                            var map = new Dictionary<string, string>
                            {
                                ["Name"] = "Jerry",
                                ["Information"] = "Test"
                            };
                            var postData = new FormUrlEncodedContent(map);
                            var putData = new FormUrlEncodedContent(map);
    
                            var url = "http://localhost:51286/api/test";
                            var client1 = new HttpClient();
                            var client2 = new HttpClient();
                            var client3 = new HttpClient();
    
                            var post = await client1.PostAsync(url, postData);
                            var get = await client2.GetAsync(url);
                            var put = await client3.PutAsync(url, putData);
                            
                            DoShowTip();
                           
                            var time = DateTime.Now.ToString("mm:ss:fff");
                            Console.WriteLine(time + post.StatusCode.ToString() + get.StatusCode.ToString() + put.StatusCode.ToString());
                        }));
                }
            }

    Server輸出順序:

    Post20:03:844
    Get20:03:851
    Put20:03:861
    跟我程式寫的順序一樣了!

    我不清楚是 HttpClient, Async/Await, 還是 Web API 的問題,

    請問這差異的原因是什麼?是否有參考的運作原理網址可參考,感謝各位。


    2016年3月29日 上午 10:24

解答

  • 其實這就是 Wait Handle 在哪的問題而已.

    (1)

    var post = await client1.PostAsync(url, postData);

    像這一行, 表示必須等到 client1.PostAsync 的資料抓到以後並且指派給 post 變數才會往下.

    (2) 當你這樣寫的時候

     var postTextTask = client1.PostAsync(url, postData);
    ...上面這行沒有 Wait Handle , 所以繼續往下...
     ......

     var post = await postTextTask; <--- Wait Handle在這 , 可是極有可能在還沒到這行前 PostAsync 早就做完了, 只是把執行個體擺在記憶體裡等你拿而已.


    (3) 所以我用一個簡單的 Windows Forms 加上 Wait Handle 模擬這個狀況, 我刻意讓先執行的執行緒裡等待的時間比較長用以模擬那個執行緒需要較長的時間才會做完, 你比較一下 StopType1 和 StopType2 的結果, 就知道 Wait Handle 在哪個位置的影響是甚麼了.

    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 SimulatorAsyncAwait
    {
        public partial class Form1 : Form
        {
            public Form1()
            {
                InitializeComponent();
            }
    
            private void button1_Click(object sender, EventArgs e)
            {
                Thread t = new Thread(ThreadMaster);
                t.IsBackground = true;
                t.Start(true);
            }
    
            private void button2_Click(object sender, EventArgs e)
            {
                Thread t = new Thread(ThreadMaster);
                t.IsBackground = true;
                t.Start(false);
            }
    
    
            List<AutoResetEvent> stops;
            List<Thread> threads;
            private void ThreadMaster(object stopType)
            {
                stops = new List<AutoResetEvent>();
                stops.Add(new AutoResetEvent(false));
                stops.Add(new AutoResetEvent(false));
                stops.Add(new AutoResetEvent(false));
                threads = new List<Thread>();
                threads.Add(new Thread(DoThread1) { IsBackground = true });
                threads.Add(new Thread(DoThread2) { IsBackground = true });
                threads.Add(new Thread(DoThread3) { IsBackground = true });
                if ((bool)stopType == true)
                {
                    StopType1();
                }
                else
                {
                    StopType2();
                }
    
                Debug.WriteLine("All End");
            }
    
            private void StopType1()
            {
                threads[0].Start();
                stops[0].WaitOne();
                threads[1].Start();
                stops[1].WaitOne();
                threads[2].Start();
                stops[2].WaitOne();
            }
    
            private void StopType2()
            {
                threads[0].Start();           
                threads[1].Start();           
                threads[2].Start();
                stops[0].WaitOne();
                stops[1].WaitOne();
                stops[2].WaitOne();
            }
    
    
    
            private void DoThread1()
            {
                Debug.WriteLine("1 Begin");
                Thread.Sleep(1000);
    
                Debug.WriteLine("1 End");
                stops[0].Set(); 
            }
    
            private void DoThread2()
            {
                Debug.WriteLine("2 Begin");
                Thread.Sleep(500);
    
                Debug.WriteLine("2 End");
                stops[1].Set();
            }
    
            private void DoThread3()
            {
                Debug.WriteLine("3 Begin");
                Thread.Sleep(100);
    
                Debug.WriteLine("3 End");
                stops[2].Set();
            }
    
           
        }
    
    }
    
    



    在現實生活中,你和誰在一起的確很重要,甚至能改變你的成長軌跡,決定你的人生成敗。 和什麼樣的人在一起,就會有什麼樣的人生。 和勤奮的人在一起,你不會懶惰; 和積極的人在一起,你不會消沈; 與智者同行,你會不同凡響; 與高人為伍,你能登上巔峰。

    • 已提議為解答 小朱MVP, Moderator 2016年3月30日 上午 12:37
    • 已標示為解答 503 2016年3月30日 上午 01:28
    2016年3月29日 下午 09:46
    版主

所有回覆

    • 已編輯 亂馬客 2016年3月29日 下午 02:06
    2016年3月29日 下午 02:04
  • 感謝回覆,

    所以我先不用 await 與先用 await 的差異是這段說明:

    標記的非同步方法可以使用 Await 或 await 來指定暫停點。 await 運算子會告知編譯器,非同步方法只有在等候的非同步處理序完成後,才能繼續通過該點。 同時,控制權會返回非同步方法的呼叫端。

    所以我可以解讀成以下這樣嗎?

    Client Code1 的部分因為沒有先用 await,各自方法自己先進行了非同步處理,所以在 Server端看到的時間明顯較接近1毫秒,這樣寫會因為 HttpClient 所使用的動詞不同,處理到送出 Request 時間可能不同,而抵達 Server 端的順序就有些微差異。

    請問這樣是否代表如果要同時處理不互相影響的資料時,採用第一個方案較好?

    Client Code2 的部分因為先用了 await,所以會等待處理完後,才換下一行 await 處理,所以 Server端看到的時間平均間距差了10毫秒,所以這樣寫會按照順序執行。

    請問這種寫法,是否代表相關資料必須按照順序更新、取得時,有順序性的要按照這種寫法?


    另外感謝async & await 的前世今生(Updated)連結。


    • 已編輯 503 2016年3月29日 下午 02:57 修正有 <> 標記符號
    2016年3月29日 下午 02:54
  • 其實這就是 Wait Handle 在哪的問題而已.

    (1)

    var post = await client1.PostAsync(url, postData);

    像這一行, 表示必須等到 client1.PostAsync 的資料抓到以後並且指派給 post 變數才會往下.

    (2) 當你這樣寫的時候

     var postTextTask = client1.PostAsync(url, postData);
    ...上面這行沒有 Wait Handle , 所以繼續往下...
     ......

     var post = await postTextTask; <--- Wait Handle在這 , 可是極有可能在還沒到這行前 PostAsync 早就做完了, 只是把執行個體擺在記憶體裡等你拿而已.


    (3) 所以我用一個簡單的 Windows Forms 加上 Wait Handle 模擬這個狀況, 我刻意讓先執行的執行緒裡等待的時間比較長用以模擬那個執行緒需要較長的時間才會做完, 你比較一下 StopType1 和 StopType2 的結果, 就知道 Wait Handle 在哪個位置的影響是甚麼了.

    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 SimulatorAsyncAwait
    {
        public partial class Form1 : Form
        {
            public Form1()
            {
                InitializeComponent();
            }
    
            private void button1_Click(object sender, EventArgs e)
            {
                Thread t = new Thread(ThreadMaster);
                t.IsBackground = true;
                t.Start(true);
            }
    
            private void button2_Click(object sender, EventArgs e)
            {
                Thread t = new Thread(ThreadMaster);
                t.IsBackground = true;
                t.Start(false);
            }
    
    
            List<AutoResetEvent> stops;
            List<Thread> threads;
            private void ThreadMaster(object stopType)
            {
                stops = new List<AutoResetEvent>();
                stops.Add(new AutoResetEvent(false));
                stops.Add(new AutoResetEvent(false));
                stops.Add(new AutoResetEvent(false));
                threads = new List<Thread>();
                threads.Add(new Thread(DoThread1) { IsBackground = true });
                threads.Add(new Thread(DoThread2) { IsBackground = true });
                threads.Add(new Thread(DoThread3) { IsBackground = true });
                if ((bool)stopType == true)
                {
                    StopType1();
                }
                else
                {
                    StopType2();
                }
    
                Debug.WriteLine("All End");
            }
    
            private void StopType1()
            {
                threads[0].Start();
                stops[0].WaitOne();
                threads[1].Start();
                stops[1].WaitOne();
                threads[2].Start();
                stops[2].WaitOne();
            }
    
            private void StopType2()
            {
                threads[0].Start();           
                threads[1].Start();           
                threads[2].Start();
                stops[0].WaitOne();
                stops[1].WaitOne();
                stops[2].WaitOne();
            }
    
    
    
            private void DoThread1()
            {
                Debug.WriteLine("1 Begin");
                Thread.Sleep(1000);
    
                Debug.WriteLine("1 End");
                stops[0].Set(); 
            }
    
            private void DoThread2()
            {
                Debug.WriteLine("2 Begin");
                Thread.Sleep(500);
    
                Debug.WriteLine("2 End");
                stops[1].Set();
            }
    
            private void DoThread3()
            {
                Debug.WriteLine("3 Begin");
                Thread.Sleep(100);
    
                Debug.WriteLine("3 End");
                stops[2].Set();
            }
    
           
        }
    
    }
    
    



    在現實生活中,你和誰在一起的確很重要,甚至能改變你的成長軌跡,決定你的人生成敗。 和什麼樣的人在一起,就會有什麼樣的人生。 和勤奮的人在一起,你不會懶惰; 和積極的人在一起,你不會消沈; 與智者同行,你會不同凡響; 與高人為伍,你能登上巔峰。

    • 已提議為解答 小朱MVP, Moderator 2016年3月30日 上午 12:37
    • 已標示為解答 503 2016年3月30日 上午 01:28
    2016年3月29日 下午 09:46
    版主
  • 感謝,這正是我所想瞭解的原理。
    2016年3月30日 上午 01:28