[C#]使用Parallel來做並行編程

[C#]使用Parallel來做並行編程

前言

之前有寫過執行緒的文章,也說明了除非三種情境,不然不建議再使用執行緒了,因為效率真的不高,而Parallel算是又簡單,又可以馬上提高運算速度,加快回應速度的好東西,大致上Parallel也只有三種,接下來就說明一下如何使用。

模擬情境

假設我們現在是在寫web api,我們常常有類似的情境就是需要去讀取不同的邏輯層,然後最後組出一個ViewModel回傳給前端使用,在原始的C#可能是一步一步來的,假設我們的DB很快,都是幾百毫秒就回吐資料了,三個任務總共只耗時一秒左右

void Main()
{
	Stopwatch totalTime = new Stopwatch();
	totalTime.Start();
	int t1,t2,t3;

	t1= Task1();
	t2= Task2();
	t3= Task3();
	totalTime.Stop();
	totalTime.ElapsedMilliseconds.Dump();
	//假設t1,t2,t3最後結合成一個viewModel

	int Task1()
	{
		Task.Delay(200).Wait();
		return 1;
	}
	int Task2()
	{
		Task.Delay(300).Wait();
		return 2;
	}
	int Task3()
	{
		Task.Delay(500).Wait();
		return 3;
	}
}

最後想當然的處理時間大約是一秒左右,雖然DB寫得不錯,回應速度也夠快了,但是其實我們可以在AP層做更好的處理,讓回應更加快速,之前執行緒這篇文章我也已經說明過了,其實都是要把任何原本同步的任務變成多工執行的概念,那Parallel如何做呢,其實非常簡單而且比執行緒更好理解,程式碼也更好閱讀,而且對硬體的效率上也更好

void Main()
{
	Stopwatch totalTime = new Stopwatch();
	totalTime.Start();
	int t1,t2,t3;

	Parallel.Invoke(
	() =>t1= Task1(),
	() =>t2= Task2(),
	() =>t3= Task3());//傳入型別是Action[],所以可以傳入多個Action,但也僅限於Action
	totalTime.Stop();
	totalTime.ElapsedMilliseconds.Dump();
	//假設t1,t2,t3最後結合成一個viewModel

	int Task1()
	{
		Task.Delay(200).Wait();
		return 1;
	}
	int Task2()
	{
		Task.Delay(300).Wait();
		return 2;
	}
	int Task3()
	{
		Task.Delay(500).Wait();
		return 3;
	}
}

三個一起執行,最後執行的時間是最長時間的500毫秒左右,從這邊就可以看到我們用很好理解的程式碼方式,卻把原本一個等一個的任務,變成多的任務一起跑,而加快了處理速度,當然就可以更快的回應了。

Parallel還有另外兩種用法,一種是for另一種則是foreach,簡單來說明一下for的方式,為求方便還是用剛剛的方式,只是加上了for的方式,先看一下原始for的執行方式,總共要跑了十次

void Main()
{
	Stopwatch totalTime = new Stopwatch();
	totalTime.Start();
	int t1, t2, t3;
	
	for (int i = 0; i < 10; i++)
	{
		t1 = Task1();
		t2 = Task2();
		t3 = Task3();
	}

	totalTime.Stop();
	totalTime.ElapsedMilliseconds.Dump();

	int Task1()
	{
		Task.Delay(200).Wait();
		return 1;
	}
	int Task2()
	{
		Task.Delay(300).Wait();
		return 2;
	}
	int Task3()
	{
		Task.Delay(500).Wait();
		return 3;
	}
}

最後執行時間為10309,如果我們改成Parallel.For的話

void Main()
{
	Stopwatch totalTime = new Stopwatch();
	totalTime.Start();
	int t1, t2, t3;
	Parallel.For(0, 10, i=>
	{
		t1 = Task1();
		t2 = Task2();
		t3 = Task3();
	});

	totalTime.Stop();
	totalTime.ElapsedMilliseconds.Dump();

	int Task1()
	{
		Task.Delay(200).Wait();
		return 1;
	}
	int Task2()
	{
		Task.Delay(300).Wait();
		return 2;
	}
	int Task3()
	{
		Task.Delay(500).Wait();
		return 3;
	}
}

最後跑出來的結果,就得看你的機器效能了,如果效能夠強的話,十個當一個跑,就會非常的快,每次筆者執行速度不一,最快有可能是1000左右,最慢可能是5000左右,如果我們在裡面再跑一個並行的話呢?

void Main()
{
	Stopwatch totalTime = new Stopwatch();
	totalTime.Start();
	int t1, t2, t3;
	Parallel.For(0, 10, i=>
	{
		Parallel.Invoke(
		() => t1 = Task1(),
		() => t2 = Task2(),
		() => t3 = Task3());
	});

	totalTime.Stop();
	totalTime.ElapsedMilliseconds.Dump();

	int Task1()
	{
		Task.Delay(200).Wait();
		return 1;
	}
	int Task2()
	{
		Task.Delay(300).Wait();
		return 2;
	}
	int Task3()
	{
		Task.Delay(500).Wait();
		return 3;
	}
}

最快的話只要500毫秒,最慢的話可能也是5000左右,取決於機器和執行緒的調用狀況,而foreach也是差不多的方式,就不多做說明,接著我們來說明一下如果我們有共享變數,在並行的方式使用之下的狀況。

void Main()
{
	Stopwatch totalTime = new Stopwatch();
	totalTime.Start();
	var syncObject = new object();
	int t1, t2, t3;
	int counte = 0;
	
	Parallel.For(0, 10, i=>
	{
		counte += Task1();
		counte += Task2();
		Task3();
	});

	totalTime.Stop();
	counte.Dump();
	totalTime.ElapsedMilliseconds.Dump();

	int Task1()
	{
		Task.Delay(200).Wait();
		return 1;
	}
	int Task2()
	{
		Task.Delay(300).Wait();
		return 1;
	}
	int Task3()
	{
		Task.Delay(500).Wait();
		return 3;
	}
}

這次印出來count卻不如我們預期的,我預期應該是20,但多跑幾次,每次count的累加都不一樣,在此我們可能要注意一下thread safe的問題哦,適時的使用lock不然會造成資料錯亂,在此我定義了一個全局變數,在每個方法裡面加1再回傳,所以遇到這種情況則要適時的使用lock在該同步的地方。

void Main()
{
	Stopwatch totalTime = new Stopwatch();
	totalTime.Start();
	var syncObject = new object();
	int t1, t2, t3;
	int counte = 0;
	
	Parallel.For(0, 10, i=>
	{
		lock (this)
		{
			counte += Task1();
			counte += Task2(); //在這邊lock起來,使共享的變數不會互相干擾
		}
		Task3();
	});

	totalTime.Stop();
	counte.Dump();
	totalTime.ElapsedMilliseconds.Dump();

	int Task1()
	{
		Task.Delay(200).Wait();
		return 1;
	}
	int Task2()
	{
		Task.Delay(300).Wait();
		return 1;
	}
	int Task3()
	{
		Task.Delay(500).Wait();
		return 3;
	}
}

假如我們想在某個條件之下,停止並行變回同步的話,那我們可以傳進第二個參數ParallelLoopState來操作,例子如下

 

特別提醒

如果你認為能讓反應速度變快,就不加思索的全部使用並行,那你就得注意了,你只要使用到並行,但是有共享變數的話,很可能造成最後運算結果有錯,但你卻不自知,再者因為並行會使用到若干個執行緒,雖然並行是用執行緒集區為基底運作,會比自行用執行緒還要好,但是cpu使用率變高和記憶體消耗還是避不了,而且如果我們是web網站,機器上的cpu和記憶體就會顯得有點寶貴,所以有時候應該是在真的慢的地方在使用,還有因為使用了並行的方式,想當然如果你有順序性問題的話,這個方式就無法使用,因為當我們把任務一起跑下去,誰先完成是無法估計的。

筆者經驗

我曾經有接過客戶的一個運算價格的dll,每次準備好各種幣別等等的資訊,然後丟進去他的運算核心裡面,總是要跑二十幾分鐘,但是因為另一邊要來取價格的大約15分鐘要取一次,以這種狀況根本不可能來得及提供最新的報價,原因是客戶是用了巢狀的dictionary還有五層以上的巢狀迴圈再算價格,所以總共的迴圈需要跑好幾百個,假設我們讓第一個迴圈用十個一起跑的方式在運行,就可以加快處理的速度,最後我使用Parallel.For的方式去運行,時間縮短到了五分鐘左右。

結論

這邊特別記錄一下,其實因為筆者主要工作都是在寫web,所以並不總是會考慮使用Parallel,但有時候真的過慢,而硬體的方面還很空閒的狀況下,就可以用這些方法來試試看加快回應速度的處理。