­

一個例子形象的理解非同步和多執行緒的區別

一個例子形象的理解非同步和多執行緒的區別

Talk is cheap, show me the code! 所以,廢話先不說,先上程式碼:

首先寫一個WebAPI介面

/// <summary>
/// 測試介面
/// </summary>
[RoutePrefix("api/test")]
public class TestController : ApiController
{
    /// <summary>
    /// 測試GET請求
    /// </summary>
    /// <param name="val">測試參數</param>
    [HttpGet]
    [Route("TestGet")]
    public HttpResponseMessage TestGet(string val)
    {
        Thread.Sleep(200); //模擬執行耗時操作

        return new HttpResponseMessage { Content = new StringContent(val.ToString(), Encoding.UTF8, "text/plain") };
    }
}

測試程式碼

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using Utils;

namespace AsyncDemo2
{
    public partial class Form1 : Form
    {
        private int n = 200;

        public Form1()
        {
            InitializeComponent();

            Task.Factory.StartNew(() =>
            {
                while (true)
                {
                    Thread.Sleep(100);

                    ThreadPool.GetMaxThreads(out int w1, out int c1);
                    ThreadPool.GetAvailableThreads(out int w2, out int c2);

                    int w = w1 - w2;
                    int c = c1 - c2;
                    label1.BeginInvoke(new Action(() =>
                    {
                        label1.Text = string.Format("工作執行緒:{0} 非同步執行緒:{1}", w, c);
                    }));
                }
            }, TaskCreationOptions.LongRunning);
        }

        /// <summary>
        /// 日誌輸出
        /// </summary>
        private void Log(string msg)
        {
            this.BeginInvoke(new Action(() =>
            {
                textBox1.AppendText(DateTime.Now.ToString("mm:ss.fff") + ":" + msg + "\r\n");
            }));
        }

        /// <summary>
        /// 非同步請求
        /// </summary>
        private async Task ReqeustAsync(int val)
        {
            try
            {
                Log("非同步  開始請求" + val);
                string result = await HttpUtil.HttpGetAsync("//localhost:8500/api/test/TestGet?val=" + val);
                Log("非同步  返回數據" + result + "  執行緒ID:" + Thread.CurrentThread.ManagedThreadId);
            }
            catch (Exception ex)
            {
                Log("出錯:" + ex.Message);
            }
        }

        /// <summary>
        /// 在執行緒中同步請求
        /// </summary>
        private Task Request(int val)
        {
            return Task.Run(() =>
            {
                try
                {
                    Log("同步多執行緒  開始請求" + val);
                    string result = HttpUtil.HttpGet("//localhost:8500/api/test/TestGet?val=" + val);
                    Log("同步多執行緒  返回數據" + result + "  執行緒ID:" + Thread.CurrentThread.ManagedThreadId);
                }
                catch (Exception ex)
                {
                    Log("出錯:" + ex.Message);
                }
            });
        }

        //測試非同步請求
        private async void button3_Click(object sender, EventArgs e)
        {
            textBox1.Text = string.Empty;
            Stopwatch sw = new Stopwatch();
            List<Task> taskList = new List<Task>();
            sw.Start();

            for (int i = 0; i < n; i++)
            {
                Task t = ReqeustAsync(i);
                taskList.Add(t);
            }
            foreach (Task t in taskList)
            {
                await t;
            }

            Log(n + "個非同步請求完成,耗時:" + sw.Elapsed.TotalSeconds.ToString("0.000"));
            sw.Stop();
        }

        //測試多執行緒同步請求
        private void button4_Click(object sender, EventArgs e)
        {
            textBox1.Text = string.Empty;

            Task.Run(() =>
            {
                List<Task> taskList = new List<Task>();
                Stopwatch sw = new Stopwatch();
                sw.Start();

                for (int i = 0; i < n; i++)
                {
                    Task t = Request(i);
                    taskList.Add(t);
                }
                Task.WaitAll(taskList.ToArray());

                Log(n + "個多執行緒同步請求完成,耗時:" + sw.Elapsed.TotalSeconds.ToString("0.000"));
                sw.Stop();
            });
        }
    }
}

測試結果

測試結果
性能差9倍!

把WebAPI介面中模擬執行耗時操作改成1000毫秒再測試,測試結果如下:

測試結果
性能差10倍!

把Form1.cs構造函數中添加一行ThreadPool.SetMinThreads(20, 20);再測:
測試結果
設置執行緒池中執行緒的最小數量為20後,性能差距縮小了,性能只差4倍!為什麼?沒有設置執行緒池最小數量時,大約每1秒增加1到2個執行緒,執行緒增加速度太慢了,不影響非同步性能,非同步只需要很少的執行緒數量,但影響多執行緒性能。

把Form1.cs構造函數中程式碼修改成ThreadPool.SetMinThreads(200, 200);再測:
測試結果
當執行緒池中執行緒數量足夠多時,性能差不多了!

結論

通過這個形象的例子,你體會到非同步的好處了嗎?
有人可能會說,你怎麼不把WebAPI端改成非同步試試?WebAPI端是模擬的操作,在沒有外部操作(IO操作、資料庫操作等),僅有數據計算時,WebAPI端改成非同步沒區別。

有一個截圖中沒有體驗出來的,測試過程中,對於非同步測試,工作執行緒和非同步執行緒始終為0,我想非同步執行緒應該是變化的,可能只是變化太快,看不出來。而多執行緒測試,測試過程中,我們可以看到工作執行緒的數量是大於0的,維持在一定數量,直到請求完成,也就是說,測試過程中,要佔用一定數量的工作執行緒。

所以結論是什麼?
非同步在執行耗時請求時,不會佔用執行緒,在執行緒池中執行緒數量較少時,非同步的性能比多執行緒好很多,

WebAPI服務端補充說明

上面的測試,服務端我忘了說了,服務端啟動服務前,我加了一行程式碼ThreadPool.SetMinThreads(200, 200);,因為你測試客戶端之前,服務端性能要跟上,不然測了個寂寞。
如果我把這行程式碼刪掉,預熱後,再測:
測試結果
可以看到差距只有2.5倍了!因為服務端執行緒數量此時是1秒增加1、2個執行緒,服務端性能跟不上,客戶端的非同步請求自然也快不起來。

HttpUtil程式碼:

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace Utils
{
    /// <summary>
    /// Http上傳下載文件
    /// </summary>
    public class HttpUtil
    {
        /// <summary>
        /// HttpGet
        /// </summary>
        /// <param name="url">url路徑名稱</param>
        /// <param name="cookie">cookie</param>
        public static async Task<string> HttpGetAsync(string url, CookieContainer cookie = null, WebHeaderCollection headers = null)
        {
            // 設置參數
            HttpWebRequest request = WebRequest.Create(url) as HttpWebRequest;
            request.CookieContainer = cookie;
            request.Method = "GET";
            request.ContentType = "text/plain;charset=utf-8";
            request.Timeout = Timeout.Infinite;

            if (headers != null)
            {
                foreach (string key in headers.Keys)
                {
                    request.Headers.Add(key, headers[key]);
                }
            }

            //發送請求並獲取相應回應數據
            HttpWebResponse response = (HttpWebResponse)(await request.GetResponseAsync());
            //直到request.GetResponse()程式才開始向目標網頁發送Post請求
            Stream instream = response.GetResponseStream();
            StreamReader sr = new StreamReader(instream, Encoding.UTF8);
            //返回結果網頁(html)程式碼
            string content = await sr.ReadToEndAsync();
            instream.Close();
            return content;
        }

        /// <summary>
        /// HttpGet
        /// </summary>
        /// <param name="url">url路徑名稱</param>
        /// <param name="cookie">cookie</param>
        public static string HttpGet(string url, CookieContainer cookie = null, WebHeaderCollection headers = null)
        {
            // 設置參數
            HttpWebRequest request = WebRequest.Create(url) as HttpWebRequest;
            request.CookieContainer = cookie;
            request.Method = "GET";
            request.ContentType = "text/plain;charset=utf-8";
            request.Timeout = Timeout.Infinite;

            if (headers != null)
            {
                foreach (string key in headers.Keys)
                {
                    request.Headers.Add(key, headers[key]);
                }
            }

            //發送請求並獲取相應回應數據
            HttpWebResponse response = (HttpWebResponse)request.GetResponse();
            //直到request.GetResponse()程式才開始向目標網頁發送Post請求
            Stream instream = response.GetResponseStream();
            StreamReader sr = new StreamReader(instream, Encoding.UTF8);
            //返回結果網頁(html)程式碼
            string content = sr.ReadToEnd();
            instream.Close();
            return content;
        }
    }
}