好久沒有寫博客了,今年作的產品公司這兩天剛剛開了發佈會,稍微悠閒下來,想一想咱們作的產品還有沒有性能優化空間,因而想到了.Net的異步能夠優化性能,但到底可以提高多大的比例呢?剛好有一個朋友正在作各類語言的異步性能測試(有關異步和同步的問題,請參考客《AIO與BIO接口性能對比》),因而我今天寫了一個C#的測試程序。web
首先,建一個 ASP.NET MVC WebAPI項目,在默認的控制器 values裏面,增長兩個方法:api
// 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異步
最後,運行這個測試,結果以下:async
按任意鍵開始測試 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個線程,但報錯了。ide
上面的測試結果,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 ,不然可能出現線程死鎖,結果難出來。