原子操作
在多线程的环境中有可能多个线程同时访问同一个资源时,我们需要一些方法保证多个线程访问不发生冲突,其中最基本的就是原子操作。
原子操作指的是不可分割,并且与其它原子操作互斥的操作。
示例:
public class demo9
{
public static int a = 0;
public static void test()
{
//给变量a自增1,返回变量a增加后的值
var b1 = Interlocked.Increment(ref a);
Console.WriteLine("a={0},b={1}",a,b1);
//给变量a自减1,返回变量a减少后的值
var b2 = Interlocked.Decrement(ref a);
Console.WriteLine("a={0},b={1}", a, b2);
//给变量a的值加上第二个参数的值,返回a增加后的值
var b3 = Interlocked.Add(ref a, 1);
Console.WriteLine("a={0},b={1}", a, b3);
//将变量a的值修改为第二个参数的值,返回的值修改的值
var b4 = Interlocked.Exchange(ref a, 1);
Console.WriteLine("a={0},b={1}", a, b4);
//如果变量a的值等于第三个参数的值,就把它修改为第二个参数的值,返回原始值
var b5 = Interlocked.CompareExchange(ref a, 1, 0);
Console.WriteLine("a={0},b={1}", a, b5);
}
}
无锁算法
示例:
public class demo8
{
public static Counter Counter = new Counter(0, 0);
public static void test()
{
ThreadIncrement();
Thread.Sleep(1000);
PrintCounter();
}
public static void ThreadIncrement()
{
var thread1 = new Thread(IncrementCounter);
var thread2 = new Thread(IncrementCounter);
thread1.Start();
thread2.Start();
}
public static void IncrementCounter()
{
var result = Increment(ref Counter);
Console.WriteLine("TimesA={0},TimesB={1}",result.TimesA,result.TimesB);
}
public static void PrintCounter()
{
var c = Counter;
Console.WriteLine("Counter={0},{1}",c.TimesA,c.TimesB);
}
/// <summary>
/// 递增计数器,返回递增后的值
/// </summary>
/// <param name="counter"></param>
/// <returns></returns>
public static Counter Increment(ref Counter counter)
{
Counter oldCounter, newCounter;
do
{
oldCounter = counter;
newCounter = new Counter(oldCounter.TimesA + 1, oldCounter.TimesB + 1);
//使用循环确保成功
} while (Interlocked.CompareExchange(ref counter, newCounter, oldCounter) != oldCounter);
return newCounter;
}
}
public class Counter
{
public int TimesA { get; set; }
public int TimesB { get; set; }
public Counter(int timesA, int timesB)
{
TimesA = timesA;
TimesB = timesB;
}
}
.Net提供了大量的线程安全的对象,其中使用了大量的复杂的无锁算法。如System.Collections.Concurrent.* 中的数据对象。
线程锁
线程锁主要包含两个操作:
- 获取锁
- 释放锁
在获取锁之后和释放锁之前进行的操作都会保证在同一时间只有一个线程在执行。
自旋锁
自旋锁是最简单的线程锁,基于原子操作实现的。
当一个线程在获取锁对象的时候,如果锁被其它线程获取,那么这个线程会循环等待,然后不断的判断锁是否能够成功获取,直到获取到锁,才会退出循环。
自旋锁通过一个数值表示自旋锁是否被获取。
优点:减少了不必要的线程切换,性能较高。
缺点:占用资源高。
示例:
public static class SpinlockSample
{
private static int _lock = 0;
private static int _counterA = 0;
private static int _counterB = 0;
public static void IncrementCounter()
{
var spinWait = new SpinWait();
//获取锁
while (Interlocked.Exchange(ref _lock, 1) != 0)
{
//使线程等待。
//方式二
Thread.SpinWait(1);
//方式一
spinWait.SpinOnce();
//一定次数以内,或核心数大于1,使用Thread.SpinWait
//超过一定次数,或核心数等于1,交替使用Thread.Sleep(0)和Thread.Yield方法
//在超过一定次数,Thread.Sleep(1)
//Sleep和Yield的区别在于:
//Yield会切换到当前逻辑核心待运行的线程队列中,不会切换到其它核心待运行的的线程队列。
//Sleep会随机切换到其它逻辑核心的待运行的线程队列中。休眠时间为0会立马放到其它核心待运行队列,不为0则会等待再放。
}
//锁保护区域
{
_counterA++;
_counterB++;
}
//释放锁
Interlocked.Exchange(ref _lock, 0);
}
}
注意:
- 自旋锁在锁保护区域的代码应该要尽量短。若保护区域内的代码过长,会导致其它等待获取锁的线程不断的重试,并且占用逻辑核心。如果使用不当会导致CPU使用过高。
- 这里的自旋锁是不公平的。有的线程会因长时间获取不到锁,而一直处于循环等待锁的阶段,导致迟迟无法开始正常工作。(引出了自旋锁的变种:排队自旋锁。按照先来后到的顺序给线程分号,然后按照号给线程分配锁)
- SpinWait是.Net Core对线程等待的封装(源码位置:runtime\src\libraries\System.Private.CoreLib\src\System\Threading\SpinWait.cs)。
.Net Core通过SpinLock类提供自旋锁。
源码位置:runtime\src\libraries\System.Private.CoreLib\src\System\Threading\SpinLock.cs
示例:
public static class SpinlockSampleNet
{
private static SpinLock _lock = new SpinLock();
private static int _counterA = 0;
private static int _counterB = 0;
public static void IncrementCounter()
{
var lockTaken = false;
try
{
//获取锁
_lock.Enter(ref lockTaken);
//锁保护区域
{
_counterA++;
_counterB++;
}
}
finally
{
//释放锁
if (lockTaken)
_lock.Exit();
}
}
}
互斥锁
互斥锁基于原子操作和线程调度实现的。
互斥锁不像自旋锁会反复重试,而是安排获取锁的线程进入等待状态,把线程对象添加到锁关联的队列中,当锁被释放时就会检查这个队列中是否有线程对象,有的话就通知操作系统唤醒该线程。
优点:线程等待节约资源。
缺点:线程从等待到唤醒耗时较长效率低。
.Net Core通过Mutex类提供互斥锁。
源码位置:runtime\src\libraries\System.Private.CoreLib\src\System\Threading\Mutex.cs
示例:
public static class MutexSampleNet
{
private static Mutex _lock = new Mutex();
private static int _counterA = 0;
private static int _counterB = 0;
public static void IncrementCounterA()
{
//获取锁
_lock.WaitOne();
try
{
//锁保护区域
{
_counterA++;
_counterB++;
}
}
finally
{
//释放锁
_lock.ReleaseMutex();
}
}
public static void IncrementCounterB()
{
//获取锁
_lock.WaitOne();
try
{
//锁保护区域
{
_counterA++;
_counterB++;
}
}
finally
{
//释放锁
_lock.ReleaseMutex();
}
}
public static void IncrementCounter()
{
//获取锁
_lock.WaitOne();
try
{
//锁保护区域
{
IncrementCounterA();
IncrementCounterB();
}
}
finally
{
//释放锁
_lock.ReleaseMutex();
}
}
}
注意:
- Mutex提供的线程锁是支持重入的。即已经获取锁的线程可以再次执行获取锁的操作。对应的释放锁的操作也要执行相同的次数。这种可重入的锁也叫递归锁。这种锁是在内部使用一个计数器来记录进入的次数,同一个线程每获取一次锁,这个计数器就会加1。每释放一次,这个计数器就会减1。减1后如果计数器为0才会执行真正的释放操作。所以递归锁在单个的函数中是没有太大意义的,一般是用在嵌套的多个函数中。
- Mutex支持跨进程之间的调用。在.Net Core的实现中是通过命名参数的形式确定锁。
混合锁
由于自旋锁和互斥锁各有优缺点,所以.Net Core提供了更通用,性能更好的锁。
混合锁获取失败后会像自旋锁一样重试一定次数,超过一定次数后再安排当前的线程进入等待状态。
混合锁获取锁流程:
混合锁释放锁流程:
.Net Core通过Monitor类提供混合锁。
源码位置:runtime\src\libraries\System.Private.CoreLib\src\System\Threading\Monitor.cs
示例:
public static class MonitorSample
{
private static readonly object _lock = new object();
private static int _counterA = 0;
private static int _counterB = 0;
/// <summary>
/// 方式一
/// </summary>
public static void IncrementCounterA()
{
var lockObj = _lock;
var lockTaken = false;
try
{
//获取锁
Monitor.Enter(lockObj, ref lockTaken);
//锁保护区域
{
_counterA++;
_counterB++;
}
}
finally
{
//释放锁
if (lockTaken)
Monitor.Exit(lockObj);
}
}
/// <summary>
/// 方式二
/// </summary>
public static void IncrementCounterB()
{
lock (_lock)
{
_counterA++;
_counterB++;
}
}
}
注意:
- 锁对象必须是引用类型。因为引用类型的对象有一个对象头,这个对象头可以存储获取该锁的id和进入的次数,以便实现可重入。
信号量
信号量是具有特殊用途的线程同步对象。
信号量内部使用一个数值,来表示可用数量,每个线程都可以增加/减少这个数量来进行同步。
信号量在执行减少操作的时候,如果减少的数量大于现有的数量,就会进入等待状态,直到其它的线程去增加数量。
.Net Core通过Semaphore类提供信号量。
源码位置:runtime\src\libraries\System.Private.CoreLib\src\System\Threading\Semaphore.cs
示例:
public static class SemaphoreSample
{
//初始信号量为0,最大数量为3
private static readonly Semaphore semaphore = new Semaphore(0,3);
public static void DoWork()
{
while (true)
{
//减少1个信号量
//如果信号量为0,则等待
semaphore.WaitOne();
Console.WriteLine("Do Work");
}
}
public static void Run()
{
for (var i = 0; i < 5; i++)
{
var thread = new Thread(DoWork);
thread.Start();
}
while (true)
{
//增加3个信号量
semaphore.Release(3);
Thread.Sleep(1000);
}
}
}
注意:
- 信号量如果给它命名,是可以做到跨进程的。
- SemaphoreSlim是不带跨进程的,用法和Semaphore是一样的。
读写锁
读写锁是一个具有特殊用途的线程锁,适用于频繁读取/写入或则说读取需要一定时间的场景。
读写锁分为读取锁和写入锁。读取锁可以被多个线程同时获取。写入锁不可以被多个线程获取。
.Net Core通过ReaderWriterLockSlim类提供读写锁。
源码位置:runtime\src\libraries\System.Private.CoreLib\src\System\Threading\ReaderWriterLockSlim.cs
示例:
public static class ReaderWriterLockSample
{
private static readonly ReaderWriterLockSlim _lock = new ReaderWriterLockSlim();
private const string FilePath = "log.txt";
private const int LogCount = 100;
private static int _writedCount;
private static int _failedCount;
public static void test()
{
Parallel.For(0, LogCount, e =>
{
WriteLog();
});
Console.WriteLine($"Write Count:{_writedCount};Failed Count:{_failedCount}");
Console.ReadLine();
}
private static void WriteLog()
{
//获取写入锁
_lock.EnterWriteLock();
try
{
var now = DateTime.Now;
var logContent = $"ThreadId = {Thread.CurrentThread.ManagedThreadId},{now.ToLongTimeString()}\r\n";
File.AppendAllText(FilePath, logContent);
_writedCount++;
}
catch (Exception)
{
_failedCount++;
}
finally
{
//释放写入锁
_lock.ExitWriteLock();
}
}
}