.NET多线程(六)并发同步
共享资源
说简单点,就是同一个实例,或同一个变量,被多个任务所拥有。
资源竞争
一个东西被多个任务拥有,谁先谁后处理,这就是资源竞争。
关键区域
指资源竞争发生的代码区块,这些代码区块是需要同步处理的关键。
这些代码区块不仅限于方法里的语句,也包含其他方法对共享资源的操作。
简单理解就是,方法1对共享A操作,方法2也对共享A操作,此时方法1和方法2就需要一个协调者(同步构造),进行同步处理。同理,推广至,类,文件等共享资源。
同步构造
4种类型
(1)原子操作(自增)
(2)内核模式(阻塞,上下文切换)
(3)用户模式(自旋)
(4)混合模式(用户+内核)
不理解没关系,后续详解
关键点
是否选择了正确的同步构造?
是否正确使用了选择的同步构造?
建议
尽量不同步(调整代码业务优先考虑),别同步太多,别同步太少,选择最廉价的同步构造,别自己写同步构造。
6、并发同步
6.1 并发同步基础
异步带来了以下问题
# 竞争条件 race conditions
# 死锁 deadlocks
# 数据损坏 data corruption
所有问题的核心是:数据
本节主要内容,
** 数据及状态转换,同步构造 Interlocked,MySpinLock,ReaderWriterLock ,ReaderWriterLockSlim
**
(1)数据及状态转换
共享的数据,多线程
只读的,不变状态的数据
原子状态转换
这通常是指1条CPU指令,32位就是32位类型,64位就是64位
32位的double就不是原子操作
非原子状态转换
多线程读写,可能读到错误的数据,因为写可能才写一半
也可能读写虽然都是原子性,但是读到的都是写之前的数据
【代码1】
static void Main(string[] args)
{
int taskCount = 500;
int count = 50;
int result = 0;
List<Task> tasks = new List<Task>();
for (int i = 0; i < taskCount; i++)
{
Task t = Task.Factory.StartNew(() =>
{
for (int j = 0; j < count; j++)
{
result++;
}
});
tasks.Add(t);
}
Task.WaitAll(tasks.ToArray());
Console.WriteLine(result);
Console.WriteLine(string.Format("期望的正确结果:{0}", taskCount * count));
Console.ReadLine();
}
分析 result++; 操作
(1)先把内存变量值拷贝到CPU寄存器
(2)CPU计算对寄存器执行+1
(3)在从寄存器把值拷贝到内存变量
结果很明显
如果2个线程
第1个线程执行步骤2寄存器+1之后
第2个线程执行步骤1拷贝内存变量值到寄存器
这时候,第1个线程再执行步骤3从寄存器读到数据就是错误的了
(2)CPU 缓存,比从内存读数据快
L1一级缓存
L2二级缓存
甚至L3三级缓存
(3)异步,并发同步
使用异步的目的是为了并发处理,提高性能
但异步带来的问题,共享数据因多线程竞争条件可能导致数据损坏
解决问题,就是对共享资源做并发同步处理,但这时候并发性能就会降低
同步构造
System.Threading.Interlocked
适用场景,同步单个字段,属性,比如计数器,需要多任务更新计数
# Interlocked.Increment
将【代码1】中
result++;
替换为
System.Threading.Interlocked.Increment(ref result);
# 使用这个同步构造后,执行时间会延长,但是不做同步处理,数据就会损坏
# Interlocked.Decrement
# Interlocked.Add
# Interlocked.Exchange
用来打造轻量级同步锁
// 例如 SpinLock,SpinLock 适用高并发短时操作
// .NET 已经提供 System.Threading.SpinLock,下面是我的 SpinLock 实现
public struct MySpinLock
{
private int locked;
public void Lock()
{
// 这里获取锁,如果locked=1,说明被其他线程锁住了,这时候就 while 循环 Spin 等待
while (Interlocked.Exchange(ref locked, 1) == 1) { }
}
public void Unlock()
{
locked = 0;
}
}
static void Main(string[] args) // .NET提供的SpinLock
{
System.Threading.SpinLock spinLock = new System.Threading.SpinLock();
int taskCount = 500;
int count = 50;
int result = 0;
List<Task> tasks = new List<Task>();
for (int i = 0; i < taskCount; i++)
{
Task t = Task.Factory.StartNew(() =>
{
bool lockTaken = false;
spinLock.Enter(ref lockTaken);
for (int j = 0; j < count; j++)
{
result++;
}
spinLock.Exit();
});
tasks.Add(t);
}
Task.WaitAll(tasks.ToArray());
Console.WriteLine(result);
Console.WriteLine(string.Format("期望的正确结果:{0}", taskCount * count));
Console.ReadLine();
}
# Interlocked.CompareExchange
用来打造单例模式
public class MySingleton
{
private MySingleton() { }
private static MySingleton instance;
public static MySingleton Instance
{
get
{
if (instance == null)
{
// instance 和 null 比较,相等就 new
return Interlocked.CompareExchange(ref instance, new MySingleton(), null);
}
return instance;
}
}
}
Mutex
适用场景,跨进程唯一,比如通常有需要保证相同exe只运行一个的需求。
(3)System.Threading.ReaderWriterLock 和 System.Threading.ReaderWriterLockSlim
适用:读线程多,写线程少
思想,因为并发读没有线程安全问题
我们可以让读同时进行,然后锁住,让写单独进行
ReaderWriterLock
当获取了读的锁,在执行时,发现需要进行写操作怎么办?
可以调用 UpgradeToWriteLock 方法升级为写锁
ReaderWriterLock 有2个问题,比较慢,还可能造成写饥饿
意思就是:
当所有的读操作获取锁,并且不断的读操作持续请求,
那么,写操作就会一直等
所以引入 ReaderWriterLockSlim
提高速度的同时,在写操作之后,对后续的读操作进行了排队,
这样写操作,就不会饥饿,一直获取不到锁
private List<NewsItem> newsList = new List<NewsItem>();
private ReaderWriterLockSlim lockSlim = new ReaderWriterLockSlim();
public IEnumerable<NewsItem> GetNews(string tag)
{
lockSlim.EnterReadLock();
try
{
return newsList.Where((news) => { return news.Tag == tag; }).ToList();
}
finally
{
lockSlim.ExitReadLock();
}
}
public void AddNews(NewsItem newsItem)
{
lockSlim.EnterWriteLock();
try
{
newsList.Add(newsItem);
}
finally
{
lockSlim.ExitWriteLock();
}
}