关于C#中的Lock、InterLocked、Monitor.Enter()/Monitor.Exit()的性能比较(附代码示例)
在C#编程中,经常会碰到多线程,这个时候我们就需要考虑多线程的问题了,例如一个写日志的方法是否会被多个线程同一个时刻调用,对一个公共变量进行更改时,是否考虑到了多线程的情况,要保证同一时刻只有一个线程在操作一个变量或者一个方法,就必须加锁。
就我现在知道C#中的加锁有三种方式:
1.Lock
2.Monitor.Enter()/Monitor.Exit()
3.InterLocked
对于前面两种,大家都很熟悉,不过大家都认为Lock和Monitor比较耗费性能,InterLocked才是可选之道,但事实是这样的吗?我所理解的耗费性能指的时执行时间较长,下面让我们用代码来说话:本文示例代码下载
InterLocked怎么用,我这里引用了这篇文章https://blog.****.net/kkfdsa132/article/details/5474013
摘录里面的一个类:
namespace InterLockedTest
{
/// <summary>
/// 一个类似于自旋锁的类,也类似于对共享资源的访问机制
/// 如果资源已被占有,则等待一段时间再尝试访问,如此循环,直到能够获得资源的使用权为止
/// </summary>
public class SpinLock
{
//资源状态锁,0--未被占有, 1--已被占有
private int theLock = 0;
//等待时间
private int spinWait;
public SpinLock(int spinWait)
{
this.spinWait = spinWait;
}
/// <summary>
/// 访问
/// </summary>
public void Enter()
{
//如果已被占有,则继续等待
while (Interlocked.CompareExchange(ref theLock, 1, 0) == 1)
{
Thread.Sleep(spinWait);
}
}
/// <summary>
/// 退出
/// </summary>
public void Exit()
{
//重置资源锁
Interlocked.Exchange(ref theLock, 0);
}
}
/// <summary>
/// 自旋锁的管理类
/// </summary>
public class SpinLockManager : IDisposable //Disposable接口,实现一种非委托资源回收机制,可看作显示回收资源。任务执行完毕后,会自动调用Dispose()里面的方法。
{
private SpinLock spinLock;
public SpinLockManager(SpinLock spinLock)
{
this.spinLock = spinLock;
spinLock.Enter();
}
//任务结束后,执行Dispose()里面的方法
public void Dispose()
{
spinLock.Exit();
}
}
}
一、我们来写个测试的统一接口如下:
namespace InterLockedTest
{
interface ITest
{
/// <summary>
/// 测试的统一方法,将一个数用threadCount个线程同时累加,返回这个过程的毫秒数和最后的结果
/// </summary>
/// <param name="from"></param>
/// <param name="to"></param>
/// <param name="threadCount"></param>
/// <param name="milliseconds"></param>
/// <param name="result"></param>
void Test(int from, int to, int threadCount,out long milliseconds,out int result);
}
}
二、我们先来实现各个锁的测试方法:
InterLocked锁的测试方法:
namespace InterLockedTest
{
public class InterLockedTest : ITest
{
static SpinLock spinLock = new SpinLock(1);
public void Test(int from, int to, int threadCount, out long milliseconds, out int result)
{
int num = from;
Task[] taskList = new Task[threadCount];
Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();
for (int i = 0; i < threadCount; i++)
{
taskList[i] = Task.Factory.StartNew(() =>
{
for (int j = from; j < to; j++)
{
spinLock.Enter();
num++;
spinLock.Exit();
}
});
}
Task.WaitAll(taskList);
milliseconds = stopwatch.ElapsedMilliseconds;
result = num;
}
}
}
InterLock实现的IDispose接口暂时叫InterLockEx测试方法:
namespace InterLockedTest
{
class InterLockTestEx:ITest
{
static SpinLock spinLock = new SpinLock(1);
public void Test(int from, int to, int threadCount, out long milliseconds, out int result)
{
int num = from;
Task[] taskList = new Task[threadCount];
Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();
for (int i = 0; i < threadCount; i++)
{
taskList[i] = Task.Factory.StartNew(() =>
{
for (int j = from; j < to; j++)
{
using (var m = new SpinLockManager(spinLock))
{
num++;
}
}
});
}
Task.WaitAll(taskList);
milliseconds = stopwatch.ElapsedMilliseconds;
result = num;
}
}
}
C#中的lock测试方法:
namespace InterLockedTest
{
public class LockTest : ITest
{
static object lockObj = new object();
public void Test(int from, int to, int threadCount, out long milliseconds, out int result)
{
int num = from;
Task[] taskList = new Task[threadCount];
Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();
for (int i = 0; i < threadCount; i++)
{
taskList[i] = Task.Factory.StartNew(() =>
{
for (int j = from; j < to; j++)
{
lock (lockObj)
{
num++;
}
}
});
}
Task.WaitAll(taskList);
milliseconds = stopwatch.ElapsedMilliseconds;
result = num;
}
}
}
C#中的Monitor.Enter和Monitor.Exit测试方法:
namespace InterLockedTest
{
public class MonitorTest : ITest
{
static object lockObj = new object();
public void Test(int from, int to, int threadCount, out long milliseconds, out int result)
{
int num = from;
Task[] taskList = new Task[threadCount];
Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();
for (int i = 0; i < threadCount; i++)
{
taskList[i] = Task.Factory.StartNew(() =>
{
for (int j = from; j < to; j++)
{
System.Threading.Monitor.Enter(lockObj);
num++;
System.Threading.Monitor.Exit(lockObj);
}
});
}
Task.WaitAll(taskList);
milliseconds = stopwatch.ElapsedMilliseconds;
result = num;
}
}
}
为了对比,我们来引入一个没有任何锁的测试,当然结果肯定不正确,因为多个线程同时对一个变量进行累加最后肯定得不到我们想要的结果,这里只是为了将结果和执行时间做对比:
namespace InterLockedTest
{
class NonLockTest:ITest
{
public void Test(int from, int to, int threadCount, out long milliseconds, out int result)
{
int num = from;
Task[] taskList = new Task[threadCount];
Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();
for (int i = 0; i < threadCount; i++)
{
taskList[i] = Task.Factory.StartNew(() =>
{
for (int j = from; j < to; j++)
{
num++;
}
});
}
Task.WaitAll(taskList);
milliseconds = stopwatch.ElapsedMilliseconds;
result = num;
}
}
}
三、上面执行执行了一次的测试时间和测试结果,但是我们需要多次的平均结果和时间来说明,于是写了下面的方法:
static void GetAverageData(ITest tester, int repeatCount,out int result_AVG,out long milliseconds_AVG)
{
int[] resultList = new int[repeatCount];
long[] milisecondList = new long[repeatCount];
int from = 0;
int to = 1000000;
int result = 0;
//用于储存结果数据
int threadCount = 10;
long milliseconds = 0;
for (int i = 0; i < repeatCount; i++)
{
tester.Test(from,to,threadCount,out milliseconds,out result);
resultList[i] = result;
milisecondList[i] = milliseconds;
}
result_AVG = (int)resultList.Average();
milliseconds_AVG = (long)milisecondList.Average();
}
最后奉上Main中的调用测试的全部代码:
namespace InterLockedTest
{
class Program
{
static void Main(string[] args)
{
int result_AVG;
long milliseconds_AVG;
ITest lockTest = new LockTest();
GetAverageData(lockTest,10,out result_AVG,out milliseconds_AVG);
Console.WriteLine("lockTest的结果是:{0},耗时{1}ms", result_AVG, milliseconds_AVG);
ITest monitorTest = new MonitorTest();
GetAverageData(monitorTest, 10, out result_AVG, out milliseconds_AVG);
Console.WriteLine("monitorTest的结果是:{0},耗时{1}ms", result_AVG, milliseconds_AVG);
ITest interLock = new InterLockedTest();
GetAverageData(interLock, 10, out result_AVG, out milliseconds_AVG);
Console.WriteLine("interLock的结果是:{0},耗时{1}ms", result_AVG, milliseconds_AVG);
ITest interLockEx = new InterLockedTest();
GetAverageData(interLockEx, 10, out result_AVG, out milliseconds_AVG);
Console.WriteLine("interLockEx的结果是:{0},耗时{1}ms", result_AVG, milliseconds_AVG);
ITest nonLock = new NonLockTest();
GetAverageData(nonLock, 10, out result_AVG, out milliseconds_AVG);
Console.WriteLine("nonLock的结果是:{0},耗时{1}ms", result_AVG, milliseconds_AVG);
Console.Read();
}
static void GetAverageData(ITest tester, int repeatCount,out int result_AVG,out long milliseconds_AVG)
{
int[] resultList = new int[repeatCount];
long[] milisecondList = new long[repeatCount];
int from = 0;
int to = 1000000;
int result = 0;
//用于储存结果数据
int threadCount = 10;
long milliseconds = 0;
for (int i = 0; i < repeatCount; i++)
{
tester.Test(from,to,threadCount,out milliseconds,out result);
resultList[i] = result;
milisecondList[i] = milliseconds;
}
result_AVG = (int)resultList.Average();
milliseconds_AVG = (long)milisecondList.Average();
}
}
}
下面我们来看几组结果:
1.10个线程,同时将一个为0的变量进行累加十万次,这个过程进行10次取平均值,最后的结果应该是一百万,我们来看看平均消耗的时间和结果:
从上面可以看到,在结果正确的情况下Monitor.Enter/Monitor.Exit的耗时最短,Lock和InterLocked消耗的时间差别不大
2.100个线程,同时将一个为0的变量进行累加十万次,这个过程进行10次取平均值,最后的结果应该是一千万,我们来看看平均消耗的时间和结果:
从上面的结果我们可以看到,保证结果正确依然是Monitor.Enter/Monitor.Exit耗时最短,此时Lock比InterLock快大约50ms
3.10个线程,同时将一个为0的变量进行累加一百万次,这个过程进行10次取平均值,最后的结果应该是一千万,我们来看看平均消耗的时间和结果:
从上面的结果我们可以看到,保证结果正确依然是Monitor.Enter/Monitor.Exit耗时最短,此时Lock比InterLock快大约50ms
4.100个线程,同时将一个为0的变量进行累加一百万次,这个过程进行10次取平均值,最后的结果应该是一亿,我们来看看平均消耗的时间和结果:
结果仍然是Monitor.Enter和Monitor.Exit表现最好。在并发越大表现越明显。
本文示例打包下载
如果对以上有任何疑问的,欢迎留言,以上仅为个人的测试结果,有错误的地方欢迎指正。