在开始之前首先我们来看一个很有趣的例子
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[];
for (int i = ; i < ; i++)
{
// create a new task
tasks[i] = new Task(() =>
{
// enter a loop for balance updates
for (int j = ; j < ; j++)
{
// update the balance
accountBalance = accountBalance + ;
}
});
// start the new task
tasks[i]Start();
}
// wait for all of the tasks to complete
TaskWaitAll(tasks);
// write out the counter value
ConsoleWriteLine(Expected value {} Counter value: {}
accountBalance);
// wait for input before exiting
ConsoleWriteLine(Press enter to finish);
ConsoleReadLine();
}
}
个task每个task都是把BankAccountBalance自增次之后代码就等到个task执行完毕然后打印出Balance的值大家猜想一下上次的代码执行完成之后打印出来的Balance的结果是多少?
J结果确实和大家猜想的一样结果不等于每次执行一次上面的代码都会得到不同的结果而且这些结果值都在左右如果运气好可能看到有那么一两次结果为为什么会这样?
下面就是本篇和接下来的几篇文章要讲述的内容
数据竞争
如果大家对多线程编程比较熟悉就知道上面情况的产生是因为 共享数据竞争导致的(对多线程不熟悉不清楚的朋友也不用担心)当有两个或者更多的task在运行并且操作同一个共享公共数据的时候就存在潜在的竞争如果不合理的处理竞争问题就会出现上面意想不到的情况
下面就来分析一下上面代码的情况是怎么产生的
当在把account对象的Balance进行自增的时候一般执行下面的三个步骤
读取现在account对象的Balance属性的值
计算创建一个临时的新变量并且把Balance属性的值赋值给新的变量而且把新变量的值增加
把新变量的值再次赋给account的Balance属性
在理论上面上面的三个步骤是代码的执行步骤但是实际中由于编译器NET 运行时对自增操作的优化操作和操作系统等的因素在执行上面代码的时候并不一定是按照我们设想的那样运行的但是为了分析的方便我们还是假设代码是按照上面的三个步骤运行的
之前的代码每次执行一次执行代码的计算机就每次处于不同的状态CPU的忙碌状况不同内存的剩余多少不同等等所以每次代码的运行计算机不可能处于完全一样的环境中
在下面的图中显示了两个task之间是如何发生竞争的当两个task启动了之后(虽然说是并行运算但是不管这样两个的task的执行时间不可能完全一样也就是说不可能恰好就是同时开始执行的起码在开始执行的时间上是有一点点的差异的)
. 首先Task读取到当前的balance的值为
. 然后task运行了并且也读取到当前的balance值为
. 两个task都把balance的值加
. Task把balance的值加后把新的值保存到了balance中
. Task 也把新的保存到了balance中
所以结果就是虽然两个task 都为balance加但是balance的值还是
通过这个例子相信大家应该清楚为什么上面的个task执行而执行后的结果不是了
. 解决方案提出
数据竞争就好比一个生日party其中每一个task都是参加party的人当生日蛋糕出来之后每个人都兴奋了如果此时所有的人都一起沖过去拿属于他们自己的那块蛋糕此时party就一团糟了没有如何顺序
在之前的图示例讲解中balance那个属性就好比蛋糕因为tasktask都要得到它然后进行运算当我们来让多个task共享一个数据时就可能出现问题下面列出了四种解决方案
. 顺序执行也就是让第一个task执行完成之后再执行第二个
. 数据不变我们让task不能修改数据
. 隔离我们不共享数据让每个task都有一份自己的数据拷贝
. 同步通过调整task的执行有序的执行task
注意同步和以前多线程中的同步或者数据库操作时的同步概念不一样
顺序的执行的解决方案
顺序的执行解决了通过每次只有一个task访问共享数据的方式解决了数据竞争的问题其实在本质上这种解决方案又回到了之前的单线程编程模型如果拿之前的party分蛋糕的例子那么现在就是一次只能允许一个人去拿蛋糕
数据不变解决方案
数据不变的解决方案就是通过让数据不能被修改的方式来解决共享数据竞争如果拿之前的蛋糕为例子那么此时的情况就是现在蛋糕只能看不能吃
在C#中可以同关键字 readonly 和 const来声明一个字段不能被修改
public const int AccountNumber=;
被声明为const的字段只能通过类型来访问如上面的AccountNumber是在Blank类中声明的那么访问的方式就是Blank AccountNumber
readonly的字段可以在实例的构造函数中修改
如下代码
using System;
class ImmutableBankAccount
{
public const int AccountNumber = ;
public readonly int Balance;
public ImmutableBankAccount(int InitialBalance)
{
Balance = InitialBalance;
}
public ImmutableBankAccount()
{ Balance = ;
}
}
class App
{
static void Main(string[] args)
{
// create a bank account with the default balance
ImmutableBankAccount bankAccount = new ImmutableBankAccount();
ConsoleWriteLine(Account Number: {} Account Balance: {}
ImmutableBankAccountAccountNumber bankAccountBalance);
// create a bank account with a starting balance
ImmutableBankAccount bankAccount = new ImmutableBankAccount();
ConsoleWriteLine(Account Number: {} Account Balance: {}
ImmutableBankAccountAccountNumber bankAccountBalance);
// wait for input before exiting
ConsoleWriteLine(Press enter to finish);
ConsoleReadLine();
}
} 数据不变的解决方案不是很常用因为它对数据限制太大了