1. 程式人生 > 其它 >.net非同步效能測試(包括ASP.NET MVC WebAPI非同步方法)

.net非同步效能測試(包括ASP.NET MVC WebAPI非同步方法)

很久沒有寫部落格了,今年做的產品公司這兩天剛剛開了釋出會,稍微清閒下來,想想我們做的產品還有沒有效能優化空間,於是想到了.Net的非同步可以優化效能,但到底能夠提升多大的比例呢?恰好有一個朋友正在做各種語言的非同步效能測試(有關非同步和同步的問題,請參考客《AIO與BIO介面效能對比》),於是我今天寫了一個C#的測試程式。

首先,建一個 ASP.NET MVC WebAPI專案,在預設的控制器 values裡面,增加兩個方法:

 // GET api/values?sleepTime=10
        [HttpGet]
        public async Task<string> ExecuteAIO(int sleepTime)
        {
            await Task.Delay(sleepTime);
            return  "Hello world,"+ sleepTime;
        }

        [HttpGet]
        // GET api/values?sleepTime2=10
        public string ExecuteBIO(int sleepTime2)
        {
            System.Threading.Thread.Sleep(sleepTime2);
            return "Hello world," + sleepTime2;
        }

然後,建立一個控制檯程式,來測試這個web API:

 class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("按任意鍵開始測試 WebAPI:http://localhost:62219/api/values?sleepTime={int}");
            Console.Write("請輸入執行緒數:");
            int threadNum = 100;
            int.TryParse(Console.ReadLine(), out threadNum);
            while (Test(threadNum)) ;

            Console.ReadLine();
            Console.ReadLine();
        }

        private static bool Test(int TaskNumber)
        {
            Console.Write("請輸入此API方法的睡眠時間(毫秒),輸入非數字內容退出:");
            string input = Console.ReadLine();
            int SleepTime = 50;
            if (!int.TryParse(input, out SleepTime))
                return false;
            HttpClient client = new HttpClient();
            client.BaseAddress = new Uri("http://localhost:62219/");
            var result = client.GetStringAsync("api/values?sleepTime=" + input).Result;
            Console.WriteLine("Result:{0}", result);
            //int TaskNumber = 1000;


            Console.WriteLine("{0}次 BIO(同步)測試(睡眠{1} 毫秒):", TaskNumber, SleepTime);
            System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch();


            sw.Start();
            Task[] taskArr = new Task[TaskNumber];
            for (int i = 0; i < TaskNumber; i++)
            {
                Task task = client.GetStringAsync("api/values?sleepTime2=" + SleepTime);
                taskArr[i] = task;

            }
            Task.WaitAll(taskArr);
            sw.Stop();
            double useTime1 = sw.Elapsed.TotalSeconds;
            Console.WriteLine("耗時(秒):{0},QPS:{1,10:f2}", useTime1, TaskNumber/useTime1);
            sw.Reset();

            Console.WriteLine("{0}次 AIO(非同步)測試(睡眠{1} 毫秒):", TaskNumber, SleepTime);
            sw.Start();
            for (int i = 0; i < TaskNumber; i++)
            {
                Task task = client.GetStringAsync("api/values?sleepTime=" + SleepTime);
                taskArr[i] = task;
            }
            Task.WaitAll(taskArr);
            sw.Stop();
            double useTime2 = sw.Elapsed.TotalSeconds;
            Console.WriteLine("耗時(秒):{0},QPS:{1,10:f2}", useTime2, TaskNumber / useTime2);
            return true;
        }
    }

其實主要是下面幾行程式碼:

HttpClient client = new HttpClient();
client.BaseAddress = new Uri("http://localhost:62219/");
var result = client.GetStringAsync("api/values?sleepTime=" + input).Result;

注意,你可能需要使用Nuget新增下面這個包:

Microsoft.AspNet.WebApi.Client

最後,執行這個測試,結果如下:

按任意鍵開始測試 WebAPI:http://localhost:62219/api/values?sleepTime={int}
請輸入執行緒數:1000
請輸入此API方法的睡眠時間(毫秒),輸入非數字內容退出:10
Result:"Hello world,10"
1000次 BIO(同步)測試(睡眠10 毫秒):
耗時(秒):1.2860545,QPS:    777.57
1000次 AIO(非同步)測試(睡眠10 毫秒):
耗時(秒):0.4895946,QPS:   2042.51
請輸入此API方法的睡眠時間(毫秒),輸入非數字內容退出:100
Result:"Hello world,100"
1000次 BIO(同步)測試(睡眠100 毫秒):
耗時(秒):8.2769307,QPS:    120.82
1000次 AIO(非同步)測試(睡眠100 毫秒):
耗時(秒):0.5435111,QPS:   1839.89

本來想嘗試測試10000個執行緒,但報錯了。

上面的測試結果,QPS並不高,但由於使用的是IISExpress,不同的Web伺服器軟體效能不相同,所以還得對比下程序內QPS結果,於是新建一個控制檯程式,程式碼如下:

 class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("按任意鍵開始測試 ");
            Console.Write("請輸入執行緒數:");
            int threadNum = 100;
            int.TryParse(Console.ReadLine(), out threadNum);
            while (Test(threadNum)) ;

            Console.ReadLine();
            Console.ReadLine();
        }

        private static bool Test(int TaskNumber)
        {
            Console.Write("請輸入此API方法的睡眠時間(毫秒),輸入非數字內容退出:");
            string input = Console.ReadLine();
            int SleepTime = 50;
            if (!int.TryParse(input, out SleepTime))
                return false;

            var result = ExecuteAIO(SleepTime).Result;
            Console.WriteLine("Result:{0}", result);
            //int TaskNumber = 1000;


            Console.WriteLine("{0}次 BIO(同步)測試(睡眠{1} 毫秒):", TaskNumber, SleepTime);
            System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch();


            sw.Start();
            Task[] taskArr = new Task[TaskNumber];
            for (int i = 0; i < TaskNumber; i++)
            {
                Task task = Task.Run<string>(()=> ExecuteBIO(SleepTime));
                taskArr[i] = task;

            }
            Task.WaitAll(taskArr);
            sw.Stop();
            double useTime1 = sw.Elapsed.TotalSeconds;
            Console.WriteLine("耗時(秒):{0},QPS:{1,10:f2}", useTime1, TaskNumber / useTime1);
            sw.Reset();

            Console.WriteLine("{0}次 AIO(非同步)測試(睡眠{1} 毫秒):", TaskNumber, SleepTime);
            sw.Start();
            for (int i = 0; i < TaskNumber; i++)
            {
                Task task = ExecuteAIO(SleepTime);
                taskArr[i] = task;
            }
            Task.WaitAll(taskArr);
            sw.Stop();
            double useTime2 = sw.Elapsed.TotalSeconds;
            Console.WriteLine("耗時(秒):{0},QPS:{1,10:f2}", useTime2, TaskNumber / useTime2);
            return true;
        }

        public static async Task<string> ExecuteAIO(int sleepTime)
        {
            await Task.Delay(sleepTime);
            return "Hello world," + sleepTime;
        }

        public static string ExecuteBIO(int sleepTime2)
        {
            System.Threading.Thread.Sleep(sleepTime2);
            //不能在非非同步方法裡面使用 Task.Delay,否則可能死鎖
            //Task.Delay(sleepTime2).Wait();
            return "Hello world," + sleepTime2;
        }
    }

注意,關鍵程式碼只有下面兩個方法:

 public static async Task<string> ExecuteAIO(int sleepTime)
        {
            await Task.Delay(sleepTime);
            return "Hello world," + sleepTime;
        }

        public static string ExecuteBIO(int sleepTime2)
        {
            System.Threading.Thread.Sleep(sleepTime2);
            //不能在非非同步方法裡面使用 Task.Delay,否則可能死鎖
            //Task.Delay(sleepTime2).Wait();
            return "Hello world," + sleepTime2;
        }

這兩個方法跟WebAPI的測試方法程式碼是一樣的,但是呼叫程式碼稍微不同:

同步呼叫:

 Task[] taskArr = new Task[TaskNumber];
            for (int i = 0; i < TaskNumber; i++)
            {
                Task task = Task.Run<string>(()=> ExecuteBIO(SleepTime));
                taskArr[i] = task;

            }
            Task.WaitAll(taskArr);

非同步呼叫:

 for (int i = 0; i < TaskNumber; i++)
            {
                Task task = ExecuteAIO(SleepTime);
                taskArr[i] = task;
            }
            Task.WaitAll(taskArr);

可見,這裡測試的時候,同步和非同步呼叫,客戶端程式碼都是使用的多執行緒,主要的區別就是非同步方法使用了 async/await 語句。

下面是非Web的程序內非同步多執行緒和同步多執行緒的結果:

請輸入執行緒數:1000
請輸入此API方法的睡眠時間(毫秒),輸入非數字內容退出:10
Result:Hello world,10
1000次 BIO(同步)測試(睡眠10 毫秒):
耗時(秒):1.3031966,QPS:    767.34
1000次 AIO(非同步)測試(睡眠10 毫秒):
耗時(秒):0.026441,QPS:  37820.05
請輸入此API方法的睡眠時間(毫秒),輸入非數字內容退出:100
Result:Hello world,100
1000次 BIO(同步)測試(睡眠100 毫秒):
耗時(秒):9.8502858,QPS:    101.52
1000次 AIO(非同步)測試(睡眠100 毫秒):
耗時(秒):0.1149469,QPS:   8699.67

請輸入執行緒數:10000
請輸入此API方法的睡眠時間(毫秒),輸入非數字內容退出:10
Result:Hello world,10
10000次 BIO(同步)測試(睡眠10 毫秒):
耗時(秒):7.7966125,QPS:   1282.61
10000次 AIO(非同步)測試(睡眠10 毫秒):
耗時(秒):0.083922,QPS: 119158.27
請輸入此API方法的睡眠時間(毫秒),輸入非數字內容退出:100
Result:Hello world,100
10000次 BIO(同步)測試(睡眠100 毫秒):
耗時(秒):34.3646036,QPS:    291.00
10000次 AIO(非同步)測試(睡眠100 毫秒):
耗時(秒):0.1721833,QPS:  58077.64

結果表示,.NET程式開啟10000個任務(不是10000個原生執行緒,需要考慮執行緒池執行緒),非同步方法的QPS超過了10萬,而同步方法只有1000多點,效能差距還是很大的。

注:以上測試結果的測試環境是 

Intel i7-4790K CPU,4核8執行緒,記憶體 16GB,Win10 企業版

總結:

不論是普通程式還是Web程式,使用非同步多執行緒,可以極大的提高系統的吞吐量。

後記:

感謝網友“雙魚座” 的提示,我用訊號量和都用執行緒Sleep的方式,對同步和非同步方法進行了測試,結果如他所說,TPL非同步方式,開銷很大,下面是測試資料:

使用 semaphoreSlim 的情況:

請輸入執行緒數:1000
請輸入此API方法的睡眠時間(毫秒),輸入非數字內容退出:10
Result:Hello world,10
1000次 BIO(同步)測試(睡眠10 毫秒):
耗時(秒):1.2486964,QPS:    800.84
1000次 AIO(非同步)測試(睡眠10 毫秒):
耗時(秒):10.5259443,QPS:     95.00
請輸入此API方法的睡眠時間(毫秒),輸入非數字內容退出:100
Result:Hello world,100
1000次 BIO(同步)測試(睡眠100 毫秒):
耗時(秒):12.2754003,QPS:     81.46
1000次 AIO(非同步)測試(睡眠100 毫秒):
耗時(秒):100.5308431,QPS:      9.95
請輸入此API方法的睡眠時間(毫秒),輸入非數字內容退出:1000
Result:Hello world,1000
1000次 BIO(同步)測試(睡眠1000 毫秒):
耗時(秒):54.0055828,QPS:     18.52
1000次 AIO(非同步)測試(睡眠1000 毫秒):
耗時(秒):1000.4749124,QPS:      1.00

使用執行緒 Sleep的程式碼改造:

  public static async Task<string> ExecuteAIO(int sleepTime)
        {
            //await Task.Delay(sleepTime);
            //return "Hello world," + sleepTime;
            //await Task.Delay(sleepTime);
            //semaphoreSlim.Wait(sleepTime);
            System.Threading.Thread.Sleep(sleepTime);
            return await Task.FromResult("Hello world," + sleepTime);
        }

        public static string ExecuteBIO(int sleepTime2)
        {
            System.Threading.Thread.Sleep(sleepTime2);
            //semaphoreSlim.Wait(sleepTime2);
            //不能在非非同步方法裡面使用 Task.Delay,否則可能死鎖
            //Task.Delay(sleepTime2).Wait();
            return "Hello world," + sleepTime2;
        }

執行結果如下:

請輸入執行緒數:1000
請輸入此API方法的睡眠時間(毫秒),輸入非數字內容退出:10
Result:Hello world,10
1000次 BIO(同步)測試(睡眠10 毫秒):
耗時(秒):1.3099217,QPS:    763.40
1000次 AIO(非同步)測試(睡眠10 毫秒):
耗時(秒):10.9869045,QPS:     91.02
請輸入此API方法的睡眠時間(毫秒),輸入非數字內容退出:100
Result:Hello world,100
1000次 BIO(同步)測試(睡眠100 毫秒):
耗時(秒):8.5861461,QPS:    116.47
1000次 AIO(非同步)測試(睡眠100 毫秒):
耗時(秒):100.9829406,QPS:      9.90
請輸入此API方法的睡眠時間(毫秒),輸入非數字內容退出:1000
Result:Hello world,1000
1000次 BIO(同步)測試(睡眠1000 毫秒):
耗時(秒):27.0158904,QPS:     37.02
1000次 AIO(非同步)測試(睡眠1000 毫秒):

在每次睡眠1秒的非同步方法測試中,很久都沒有出來結果,不用考慮,QPS肯定低於一秒了。

經驗教訓:

在非同步方法中,不要使用 Thread.Sleep;在同步方法中,不要使用Task.Delay ,否則可能出現執行緒死鎖,結果難出來。