在之前我有写一篇关於资料库的ACID分享RDBMS资料库基本原则
假如我们系统是一个多执行续高并发系统也要注意Atomic不然会造成资料会有Data Racing导致bug产生..
本文使用范例在我github上CAS_Lock有兴趣的人可以参考.
本篇同步发布在我的Blog 高并发系统系列-使用lock & Interlocked CAS(compare and swap)
我是我们公司担任技术面试官之一,假如面试者说他实作过高并发系统。
我就会先问以下问题来辨别是否要更深入面试高并发系统相关问题.
下面Sample Code是这样.
我用Task
假装高并发,下面有一个Member
类别预设给100000元的Balance
有一个UpdateBalance
每呼叫一次就扣10元,我透过For
跑10000次
理论上我预期在跑完下面显示Balance会员余额是0 (因为 10000*10 = 100000).
class Program
{
static void Main(string[] args)
{
Member member = new Member() { Balance = 100000 };
List<Task> tasks = new List<Task>();
for (int i = 0; i < 10000; i++)
{
tasks.Add(Task.Run(() => member.UpdateBalance()));
}
Task.WaitAll(tasks.ToArray());
Console.WriteLine($"member remaining balance is {member.Balance}");
Console.ReadKey();
}
}
public class Member {
public decimal Balance { get; set; }
public void UpdateBalance()
{
Balance -= 10;
}
}
但执行後蛮常会是不等於0!!
这时我会问面试者两个问题
如果知道的人可以跳到CAS部分,如果不知原因我下面会跟大家分享
这个问题牵扯到Thread是如何对於变数操作的,Thread在操作变数之前会把资料复制一份进Thread Context中在操作我们要步骤
所以在Balance -= 10;
这段程序码会拆成下面动作
因为以上步骤,假如同一时间有两个不同的Thread取到的Balance都是1000,并个别对於Balance减10元,我们原本预期这是两个操作(预期资料为980)
但因为取的瞬间都是1000-10=990把数值放回变数中就导致少扣一个10元...
概念如下图.
知道原因了要解决就简单了.
因为这段程序码遇到Data Raceing在同一个时间有两个不同的Thread对於资料竞争
如果要避免竞争lock是一个比较方便的方式,他可以保证一瞬间只有一个Thread(Session)来执行某段程序码(通常会放在异动资料那部分)来保证Isolation.
下面是使用lock版本的程序码,能看到我在Balance -= 10;
这一段使用lock来确保每一个瞬间只有一个Thread可以异动资料,其他的Thread需要blocking等他完成
class Program
{
static object _lock = new object();
static void Main(string[] args)
{
Member member = new Member() { Balance = 100000 };
List<Task> tasks = new List<Task>();
for (int i = 0; i < 10000; i++)
{
tasks.Add(Task.Run(() => member.UpdateBalance()));
}
Task.WaitAll(tasks.ToArray());
Console.WriteLine($"member remaining balance is {member.Balance}");
Console.ReadKey();
}
}
public class Member {
//here
object _lock = new object();
public decimal Balance { get; set; }
public void UpdateBalance()
{
lock (_lock)
{
Balance -= 10;
}
}
}
使用lock之後能发现不管执行几次资料都会如我们预期显示.
使用lock执行概念图如下.
在同一时间我们会把要执行程序 利用一个类似保护网的方式,确保同一时间只有一个Thread来做操作.
Thread2取得lock在操作Thread1就必须等待Thread2执行完,在取值=>改值..等等动作
只是使用lock会降低些许吞吐量(但资料正确性是最重要的),所以要注意使用lock范围大小
CAS是利用compare and swap来确保资料Atomic.
因为CAS可以取得数值记忆体空间来比较给值并且他也是一条CPU原语具有原子性 cpu 硬体同步原语
前面有说过在Balance -= 10;
这段程序码会拆成下面动作
balance -= 10;
会拆解成类似下面动作
int temp = balance;
temp = temp -10;
balance = temp;
假如在取balance(附值给temp)跟把值重新写入balance中间有其他Thread来操作,就会造成所谓Data Racing,因为对於我们来说上面後两部有不可分割性(Atomic).
而这时候我们就可以使用CAS算法来帮我们解决问题,在C#如果我们想要达成变数修改的Atomic可以透过Interlocked
类别
在C#中我们可以使用Interlocked这个类别
对於Int
,Long
相关操作都有封装成method.
下面这段虚拟码解释balance -= 10;
CAS中类似下面效果
//original
int temp = balance;
temp = temp -10;
balance = temp;
//CAS
int oldValue = balance;
int newValue = oldValue - 1;
//compare value source and set
Interlocked.CompareExchange(ref balance,newValue,oldValue);
//Interlocked.CompareExchange类似下面做法,但具有Atomic
//if(ref balance == oldValue){
// balance = newValue;
//}
主要是把compare value source and set这段包成一个不可分割区段达到Atomic.
> 我上面用ref
来表示从记忆体位置取得balance
原始资料
Exchange
:把值改成另一个数值 具有AtomicDecrement
:把数值-- 具有AtomicIncrement
:把数值++ 具有Atomic除了上面我们还可以针对Reference Type做Atomic有兴趣的人在自行了解
class Program
{
static object _lock = new object();
static void Main(string[] args)
{
Stopwatch sw = new Stopwatch();
int balanceValue = 10000000;
Member member = new Member() { Balance = balanceValue };
List<Task> tasks = new List<Task>();
sw.Start();
for (int i = 0; i < 1000000; i++)
{
tasks.Add(Task.Run(() => member.UpdateBalance()));
}
Task.WaitAll(tasks.ToArray());
sw.Stop();
Console.WriteLine("Lock Version");
Console.WriteLine($"member remaining balance is {member.Balance}");
Console.WriteLine($"Exec Time Cost : {sw.ElapsedMilliseconds}");
tasks.Clear();
member.Balance = balanceValue;
sw.Restart();
for (int i = 0; i < 1000000; i++)
{
tasks.Add(Task.Run(() => member.UpdateBalanceByInterlock()));
}
Task.WaitAll(tasks.ToArray());
sw.Stop();
Console.WriteLine("InterLocked Version:");
Console.WriteLine($"member remaining balance is {member.Balance}");
Console.WriteLine($"Exec Time Cost : {sw.ElapsedMilliseconds}");
Console.ReadKey();
}
}
public class Member {
object _lock = new object();
public int Balance { get; set; }
public void UpdateBalance()
{
lock (_lock)
{
Balance -= 10;
}
}
public void UpdateBalanceByInterlock()
{
int val = 0;
Balance = Interlocked.Exchange(ref val, Balance -= 10);
}
}
下图是比较两者执行时间还有并发计算数值,能发现数值两个都正确计算出0,但Interlocked执行时间少於lock版本.
Interlocked效率会比较高是因为block会造成Thread的blocking等待浪费,但Interlocked核心概念是在这段话Atomic取得资料跟原值比较(如果资料还没改就把值修改进Memory中)
所以效率就会比lock好很多
在有些情境适合使用Lock(例如许多操作需要有一致的Atomic)就比较适合.
Interlocked适合用在对於数值的Atomic.
在多执行绪的世界中要顾虑的点真的很多,稍有不甚就会造成很多错误.
因为多执行绪有许多地方需要注意,不然执行效率会不如单执行绪.
我慢慢可以理解为什麽Redis,Node.js一开始要使用sigel Thread来运作了...
>>: 最新UiPath常见入门问题大汇整║持续更新中...
Internxt Drive 是世界上最安全的云端储存服务之一,他采用客户端加密与分布布技术,使得所...
阿嬷都看得懂的 style 标签怎麽用 昨天我们介绍了 CSS 选择器,所以终於知道该怎麽把独立收整...
第二十六天 各位点进来的朋友,你们好阿 小的不才只能做这个系列的文章,但还是希望分享给点进来的朋友,...
不怎麽重要的前言 上一篇介绍了需要与if条件式结合且与回圈控制有关的语法,基本上我们已经把基础的程序...
调整ActivityLog 今天花了时间调整ActivityLog,也就是画面上方秀出讯息的地方。原...