.NET 4并行編程之共享數(shù)據(jù)問(wèn)題和解決概述
之前的文章介紹了了并行編程的一些基礎(chǔ)的知識(shí),從本篇開(kāi)始,將會(huì)講述并行編程中實(shí)際遇到一些問(wèn)題,接下來(lái)的幾篇將會(huì)講述數(shù)據(jù)共享問(wèn)題。
本篇的議題如下:
1.數(shù)據(jù)競(jìng)爭(zhēng)
2.解決方案提出
3.順序的執(zhí)行解決方案
4.數(shù)據(jù)不變解決方案
在開(kāi)始之前,首先,我們來(lái)看一個(gè)很有趣的例子:
- 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個(gè)task,每個(gè)task都是把BankAccount.Balance自增1000次。之后代碼就等到10個(gè)task執(zhí)行完畢,然后打印出Balance的值。大家猜想一下,上次的代碼執(zhí)行完成之后,打印出來(lái)的Balance的結(jié)果是多少?
J結(jié)果確實(shí)和大家猜想的一樣:結(jié)果不等于10000。每次執(zhí)行一次上面的代碼,都會(huì)得到不同的結(jié)果,而且這些結(jié)果值都在10000左右,如果運(yùn)氣好,可能看到有那么一兩次結(jié)果為10000.為什么會(huì)這樣?
下面就是本篇和接下來(lái)的幾篇文章要講述的內(nèi)容。
1.數(shù)據(jù)競(jìng)爭(zhēng)
如果大家對(duì)多線程編程比較熟悉,就知道上面情況的產(chǎn)生是因?yàn)?“共享數(shù)據(jù)競(jìng)爭(zhēng)”導(dǎo)致的(對(duì)多線程不熟悉不清楚的朋友也不用擔(dān)心)。當(dāng)有兩個(gè)或者更多的task在運(yùn)行并且操作同一個(gè)共享公共數(shù)據(jù)的時(shí)候,就存在潛在的競(jìng)爭(zhēng)。如果不合理的處理競(jìng)爭(zhēng)問(wèn)題,就會(huì)出現(xiàn)上面意想不到的情況。
下面就來(lái)分析一下:上面代碼的情況是怎么產(chǎn)生的。
當(dāng)在把a(bǔ)ccount對(duì)象的Balance進(jìn)行自增的時(shí)候,一般執(zhí)行下面的三個(gè)步驟:
- 讀取現(xiàn)在account對(duì)象的Balance屬性的值。
- 計(jì)算,創(chuàng)建一個(gè)臨時(shí)的新變量,并且把Balance屬性的值賦值給新的變量,而且把新變量的值增加1
- 把新變量的值再次賦給account的Balance屬性
在理論上面,上面的三個(gè)步驟是代碼的執(zhí)行步驟,但是實(shí)際中,由于編譯器,.NET 運(yùn)行時(shí)對(duì)自增操作的優(yōu)化操作,和操作系統(tǒng)等的因素,在執(zhí)行上面代碼的時(shí)候,并不一定是按照我們?cè)O(shè)想的那樣運(yùn)行的,但是為了分析的方便,我們還是假設(shè)代碼是按照上面的三個(gè)步驟運(yùn)行的。
之前的代碼每次執(zhí)行一次,執(zhí)行代碼的計(jì)算機(jī)就每次處于不同的狀態(tài):CPU的忙碌狀況不同,內(nèi)存的剩余多少不同,等等,所以每次代碼的運(yùn)行,計(jì)算機(jī)不可能處于完全一樣的環(huán)境中。
在下面的圖中,顯示了兩個(gè)task之間是如何發(fā)生競(jìng)爭(zhēng)的。當(dāng)兩個(gè)task啟動(dòng)了之后(雖然說(shuō)是并行運(yùn)算,但是不管這樣,兩個(gè)的task的執(zhí)行時(shí)間不可能完全一樣,也就是說(shuō),不可能恰好就是同時(shí)開(kāi)始執(zhí)行的,起碼在開(kāi)始執(zhí)行的時(shí)間上是有一點(diǎn)點(diǎn)的差異的)。
1. 首先Task1讀取到當(dāng)前的balance的值為0。
2. 然后,task2運(yùn)行了,并且也讀取到當(dāng)前的balance值為0。
3. 兩個(gè)task都把balance的值加1
4. Task1把balance的值加1后,把新的值保存到了balance中
5. Task2 也把新的保存到了balance中
所以,結(jié)果就是:雖然兩個(gè)task 都為balance加1,但是balance的值還是1。
通過(guò)這個(gè)例子,相信大家應(yīng)該清楚,為什么上面的10個(gè)task執(zhí)行1000,而執(zhí)行后的結(jié)果不是10000了。
2. 解決方案提出
數(shù)據(jù)競(jìng)爭(zhēng)就好比一個(gè)生日party。其中,每一個(gè)task都是參加party的人,當(dāng)生日蛋糕出來(lái)之后,每個(gè)人都興奮了。如果此時(shí),所有的人都一起沖過(guò)去拿屬于他們自己的那塊蛋糕,此時(shí)party就一團(tuán)糟了,沒(méi)有如何順序。
在之前的圖示例講解中,balance那個(gè)屬性就好比蛋糕,因?yàn)閠ask1,task2都要得到它,然后進(jìn)行運(yùn)算。當(dāng)我們來(lái)讓多個(gè)task共享一個(gè)數(shù)據(jù)時(shí)就可能出現(xiàn)問(wèn)題。下面列出了四種解決方案:
1. 順序執(zhí)行:也就是讓第一個(gè)task執(zhí)行完成之后,再執(zhí)行第二個(gè)。
2. 數(shù)據(jù)不變:我們讓task不能修改數(shù)據(jù)。
3. 隔離:我們不共享數(shù)據(jù),讓每個(gè)task都有一份自己的數(shù)據(jù)拷貝。
4. 同步:通過(guò)調(diào)整task的執(zhí)行,有序的執(zhí)行task。
注意:同步和以前多線程中的同步,或者數(shù)據(jù)庫(kù)操作時(shí)的同步概念不一樣
3.順序的執(zhí)行的解決方案
順序的執(zhí)行解決了通過(guò)每次只有一個(gè)task訪問(wèn)共享數(shù)據(jù)的方式解決了數(shù)據(jù)競(jìng)爭(zhēng)的問(wèn)題,其實(shí)在本質(zhì)上,這種解決方案又回到了之前的單線程編程模型。如果拿之前的party分蛋糕的例子,那么現(xiàn)在就是一次只能允許一個(gè)人去拿蛋糕。
4.數(shù)據(jù)不變解決方案
數(shù)據(jù)不變的解決方案就是通過(guò)讓數(shù)據(jù)不能被修改的方式來(lái)解決共享數(shù)據(jù)競(jìng)爭(zhēng)。如果拿之前的蛋糕為例子,那么此時(shí)的情況就是:現(xiàn)在蛋糕只能看,不能吃。
在C#中,可以同關(guān)鍵字 readonly 和 const來(lái)聲明一個(gè)字段不能被修改:
public const int AccountNumber=123456;
被聲明為const的字段只能通過(guò)類型來(lái)訪問(wèn):如,上面的AccountNumber是在Blank類中聲明的,那么訪問(wèn)的方式就是Blank. AccountNumber
readonly的字段可以在實(shí)例的構(gòu)造函數(shù)中修改。
如下代碼:
- 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();
- }
- }
數(shù)據(jù)不變的解決方案不是很常用,因?yàn)樗鼘?duì)數(shù)據(jù)限制太大了。
原文標(biāo)題:.NET 并行(多核)編程系列之七 共享數(shù)據(jù)問(wèn)題和解決概述
鏈接:http://www.cnblogs.com/yanyangtian/archive/2010/06/24/1764098.html
【編輯推薦】