前言:之前的文章介紹了了並行編程的一些基礎的知識,從本篇開始,將會講述並行編程中實際遇到一些問題,接下來的幾篇將會講述數據共享問題。
本篇的議題如下:
數據競爭
解決方案提出
順序的執行解決方案
數據不變解決方案
在開始之前,首先,我們來看一個很有趣的例子:
class BankAccount
{
public int Balance
{
get;
set;
}
}
class App
{
static void Main(string[] args)
{
// create the bank account instance
BankAccount account = new BankAccount();
// create an array of tasks
Task[] tasks = new Task[10];
for (int i = 0; i < 10; i++)
{
// create a new task
tasks[i] = new Task(() =>
{
// enter a loop for 1000 balance updates
for (int j = 0; j < 1000; j++)
{
// update the balance
account.Balance = account.Balance + 1;
}
});
// start the new task
tasks[i].Start();
}
// wait for all of the tasks to complete
Task.WaitAll(tasks);
// write out the counter value
Console.WriteLine("Expected value {0}, Counter value: {1}",
10000, account.Balance);
// wait for input before exiting
Console.WriteLine("Press enter to finish");
Console.ReadLine();
}
}
在上面的例子中,創建了10個task,每個task都是把BankAccount.Balance自增1000次。之後代碼就等到10個task執行完畢,然後打印出Balance的值。大家猜想一下,上次的代碼執行完成之後,打印出來的Balance的結果是多少?
J結果確實和大家猜想的一樣:結果不等於10000。每次執行一次上面的代碼,都會得到不同的結果,而且這些結果值都在10000左右,如果運氣好,可能看到有那麼一兩次結果為10000.為什麼會這樣?
下面就是本篇和接下來的幾篇文章要講述的內容。
數據競爭
如果大家對多線程編程比較熟悉,就知道上面情況的產生是因為 “共享數據競爭”導致的(對多線程不熟悉不清楚的朋友也不用擔心)。當有兩個或者更多的task在運行並且操作同一個共享公共數據的時候,就存在潛在的競爭。如果不合理的處理競爭問題,就會出現上面意想不到的情況。
下面就來分析一下:上面代碼的情況是怎麼產生的。
當在把account對象的Balance進行自增的時候,一般執行下面的三個步驟:
讀取現在account對象的Balance屬性的值。
計算,創建一個臨時的新變量,並且把Balance屬性的值賦值給新的變量,而且把新變量的值增加1
把新變量的值再次賦給account的Balance屬性
在理論上面,上面的三個步驟是代碼的執行步驟,但是實際中,由於編譯器,.NET 運行時對自增操作的優化操作,和操作系統等的因素,在執行上面代碼的時候,並不一定是按照我們設想的那樣運行的,但是為了分析的方便,我們還是假設代碼是按照上面的三個步驟運行的。
之前的代碼每次執行一次,執行代碼的計算機就每次處於不同的狀態:CPU的忙碌狀況不同,內存的剩余多少不同,等等,所以每次代碼的運行,計算機不可能處於完全一樣的環境中。
在下面的圖中,顯示了兩個task之間是如何發生競爭的。當兩個task啟動了之後(雖然說是並行運算,但是不管這樣,兩個的task的執行時間不可能完全一樣,也就是說,不可能恰好就是同時開始執行的,起碼在開始執行的時間上是有一點點的差異的)。
1.首先Task1讀取到當前的balance的值為0。
2.然後,task2運行了,並且也讀取到當前的balance值為0。
3.兩個task都把balance的值加1
4.Task1把balance的值加1後,把新的值保存到了balance中
5.Task2 也把新的保存到了balance中
所以,結果就是:雖然兩個task 都為balance加1,但是balance的值還是1。
通過這個例子,相信大家應該清楚,為什麼上面的10個task執行1000,而執行後的結果不是10000了。
2.解決方案提出
數據競爭就好比一個生日party。其中,每一個task都是參加party的人,當生日蛋糕出來之後,每個人都興奮了。如果此時,所有的人都一起沖過去拿屬於他們自己的那塊蛋糕,此時party就一團糟了,沒有如何順序。
在之前的圖示例講解中,balance那個屬性就好比蛋糕,因為task1,task2都要得到它,然後進行運算。當我們來讓多個task共享一個數據時就可能出現問題。下面列出了四種解決方案:
1.順序執行:也就是讓第一個task執行完成之後,再執行第二個。
2.數據不變:我們讓task不能修改數據。
3.隔離:我們不共享數據,讓每個task都有一份自己的數據拷貝。
4.同步:通過調整task的執行,有序的執行task。
注意:同步和以前多線程中的同步,或者數據庫操作時的同步概念不一樣
3. 順序的執行的解決方案
順序的執行解決了通過每次只有一個task訪問共享數據的方式解決了數據競爭的問題,其實在本質上,這種解決方案又回到了之前的單線程編程模型。如果拿之前的party分蛋糕的例子,那麼現在就是一次只能允許一個人去拿蛋糕。
數據不變解決方案
數據不變的解決方案就是通過讓數據不能被修改的方式來解決共享數據競爭。如果拿之前的蛋糕為例子,那麼此時的情況就是:現在蛋糕只能看,不能吃。
在C#中,可以同關鍵字 readonly 和 const來聲明一個字段不能被修改:
public const int AccountNumber=123456;
被聲明為const的字段只能通過類型來訪問:如,上面的AccountNumber是在Blank類中聲明的,那麼訪問的方式就是Blank. AccountNumber
readonly的字段可以在實例的構造函數中修改。
如下代碼:
using System;
class ImmutableBankAccount
{
public const int AccountNumber = 123456;
public readonly int Balance;
public ImmutableBankAccount(int InitialBalance)
{
Balance = InitialBalance;
}
public ImmutableBankAccount()
{
Balance = 0;
}
}
class App
{
static void Main(string[] args)
{
// create a bank account with the default balance
ImmutableBankAccount bankAccount1 = new ImmutableBankAccount();
Console.WriteLine("Account Number: {0}, Account Balance: {1}",
ImmutableBankAccount.AccountNumber, bankAccount1.Balance);
// create a bank account with a starting balance
ImmutableBankAccount bankAccount2 = new ImmutableBankAccount(200);
Console.WriteLine("Account Number: {0}, Account Balance: {1}",
ImmutableBankAccount.AccountNumber, bankAccount2.Balance);
// wait for input before exiting
Console.WriteLine("Press enter to finish");
Console.ReadLine();
}
}
數據不變的解決方案不是很常用,因為它對數據限制太大了。