线程锁是多线程并发共享数据,保证一致性的工具。多线程可以同时运行多个任务但是当多个线程同时访问共享数据时,可能导致数据不同步。当有多个线程访问同一对象的加锁方法或代码块时,同一时间只有一个线程在执行,其余线程必须要等待当前线程执行完之后才能执行该代码段。但其余线程是可以访问该对象中的非加锁代码块的。以下介绍.NET(C#)中 lock 和 ReaderWriterLock 的使用。
lock
语句获取给定对象的互斥 lock
,执行语句块,然后释放 lock
。 持有 lock
时,持有 lock
的线程可以再次获取并释放 lock
。 阻止任何其他线程获取 lock
并等待释放 lock
。ReaderWriterLock
支持单个写线程和多个读线程的锁。.NET Framework 有两个读取器-编写器锁, ReaderWriterLockSlim
和 ReaderWriterLock
。 建议对所有新开发的项目使用 ReaderWriterLockSlim
。 虽然 ReaderWriterLockSlim
类似于 ReaderWriterLock
,但不同之处在于,前者简化了递归规则以及锁状态的升级和降级规则。 ReaderWriterLockSlim
避免了许多潜在的死锁情况。 另外,ReaderWriterLockSlim
的性能显著优于 ReaderWriterLock
。ReaderWriterLock
长时间持有读取器锁或写入器锁将枯竭其他线程。 为了获得最佳性能,请考虑重构应用程序以最大程度地缩短写入的持续时间。
lock
的使用语法:
lock (x)
{
// 加锁代码
}
lock
是语法糖,代码相当于:
object __lockObj = x;
bool __lockWasTaken = false;
try
{
System.Threading.Monitor.Enter(__lockObj, ref __lockWasTaken);
// 加锁代码
}
finally
{
if (__lockWasTaken) System.Threading.Monitor.Exit(__lockObj);
}
注意:无论lock
锁定的是this
,还是obj
,只要关心多线程锁定的对象是不是为同一个对象。当同步对共享资源的线程访问时,最好使用锁定专用对象实例(例如,private readonly object balanceLock = new object();
),避免对不同的共享资源使用相同的 lock
对象实例,因为这可能导致死锁或锁争用。最好避免使用this、Type 实例、字符串实例作为lock锁定的对象。
lock
为互斥锁,lock 关键字将语句块标记为临界区,方法是获取给定对象的互斥锁,执行语句,然后释放该锁。当任何一个线程获取到锁后,其他线程如果需要使用该临界区内代码,则必须等待前一个线程使用完毕后释放锁。
例如,
using System;
using System.Threading.Tasks;
namespace ConsoleApplication
{
class Program
{
static void Main()
{
var account = new Account(1000);
var tasks = new Task[100];
for (int i = 0; i < tasks.Length; i++)
{
tasks[i] = Task.Run(() => Update(account));
}
Task.WhenAll(tasks).Wait();
Console.WriteLine($"Account balance : {account.GetBalance()}");
Console.ReadKey();
}
static void Update(Account account)
{
decimal[] amounts = { 0, 2, -3, 6, -2, -1, 8, -5, 11, -6 };
foreach (var amount in amounts)
{
if (amount >= 0)
{
account.Credit(amount);
}
else
{
account.Debit(Math.Abs(amount));
}
}
}
}
public class Account
{
private readonly object balanceLock = new object();
private decimal balance;
public Account(decimal initialBalance) => balance = initialBalance;
public decimal Debit(decimal amount)
{
if (amount < 0)
{
throw new ArgumentOutOfRangeException(nameof(amount), "The debit amount cannot be negative.");
}
decimal appliedAmount = 0;
lock (balanceLock)
{
if (balance >= amount)
{
balance -= amount;
appliedAmount = amount;
}
}
return appliedAmount;
}
public void Credit(decimal amount)
{
if (amount < 0)
{
throw new ArgumentOutOfRangeException(nameof(amount), "The credit amount cannot be negative.");
}
lock (balanceLock)
{
balance += amount;
}
}
public decimal GetBalance()
{
lock (balanceLock)
{
return balance;
}
}
}
}
ReaderWriterLock
为读写锁,ReaderWriterLock
定义支持单个写线程和多个读线程的锁。该锁主要是解决并发读的性能问题,使用该锁可以大大提高数据并发访问的性能,只有在写时,才会阻塞所有的读锁。建议对所有新开发的项目使用 ReaderWriterLockSlim
。 虽然 ReaderWriterLockSlim
类似于 ReaderWriterLock
,但不同之处在于,前者简化了递归规则以及锁状态的升级和降级规则。 ReaderWriterLockSlim
避免了许多潜在的死锁情况。 另外,ReaderWriterLockSlim
的性能显著优于 ReaderWriterLock
。
例如,
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace ConsoleApplication
{
class Program
{
static ReaderWriterLock rwl = new ReaderWriterLock();
// 定义ReaderWriterLock对象。
static int resource = 0;
const int numThreads = 8;
static bool running = true;
static int readerTimeouts = 0;
static int writerTimeouts = 0;
static int reads = 0;
static int writes = 0;
public static void Main()
{
// 启动多个线程,对共享资源进行随机读写。
Thread[] t = new Thread[numThreads];
for (int i = 0; i < numThreads; i++)
{
t[i] = new Thread(new ThreadStart(ThreadProc));
t[i].Name = new String(Convert.ToChar(i + 65), 1);
t[i].Start();
if (i > 10)
Thread.Sleep(300);
}
// 等待它们全部完成。
running = false;
for (int i = 0; i < numThreads; i++)
t[i].Join();
Console.WriteLine("\nread:{0} , write:{1} , reader time-out:{2}, writer time-out:{3}",
reads, writes, readerTimeouts, writerTimeouts);
Console.Write("Press ENTER to exit... ");
Console.ReadLine();
}
static void ThreadProc()
{
Random rnd = new Random();
//随机选择线程对共享资源进行读写的方式。
while (running)
{
double action = rnd.NextDouble();
if (action < .8)
ReadFromResource(10);
else if (action < .81)
ReleaseRestore(rnd, 50);
else if (action < .90)
UpgradeDowngrade(rnd, 100);
else
WriteToResource(rnd, 100);
}
}
// 请求和释放读取锁,并处理超时。
static void ReadFromResource(int timeOut)
{
try
{
rwl.AcquireReaderLock(timeOut);
try
{
// 这个线程从共享资源读取是安全的。
Display("reads resource value :" + resource);
Interlocked.Increment(ref reads);
}
finally
{
// 确保锁已释放。
rwl.ReleaseReaderLock();
}
}
catch (ApplicationException)
{
// 读取锁定请求超时处理
Interlocked.Increment(ref readerTimeouts);
}
}
// 请求和释放写入锁,并处理超时
static void WriteToResource(Random rnd, int timeOut)
{
try
{
rwl.AcquireWriterLock(timeOut);
try
{
// 这个线程从共享资源访问是安全的
resource = rnd.Next(500);
Display("writes resource value " + resource);
Interlocked.Increment(ref writes);
}
finally
{
// 确保锁已释放
rwl.ReleaseWriterLock();
}
}
catch (ApplicationException)
{
// 写锁请求超时处理
Interlocked.Increment(ref writerTimeouts);
}
}
// 请求读取锁,将读取锁升级为写入锁,然后再次将其降级为读取锁。
static void UpgradeDowngrade(Random rnd, int timeOut)
{
try
{
rwl.AcquireReaderLock(timeOut);
try
{
// 这个线程从共享资源读取是安全的
Display("reads resource value " + resource);
Interlocked.Increment(ref reads);
//要写资源,要么释放读锁,要么请求写锁,或升级读锁升级
//读取锁将线程放入写队列中,在any后面可能正在等待写入锁的其他线程。
try
{
LockCookie lc = rwl.UpgradeToWriterLock(timeOut);
try
{
//对这个线程来说,从共享资源读写是安全的。
resource = rnd.Next(500);
Display("writes resource value " + resource);
Interlocked.Increment(ref writes);
}
finally
{
// 确保锁已释放。
rwl.DowngradeFromWriterLock(ref lc);
}
}
catch (ApplicationException)
{
// 事件解释升级请求超时。
Interlocked.Increment(ref writerTimeouts);
}
// 如果锁被降级,从资源中读取仍然是安全的。
Display("reads resource value: " + resource);
Interlocked.Increment(ref reads);
}
finally
{
// 确保锁已释放
rwl.ReleaseReaderLock();
}
}
catch (ApplicationException)
{
// 读取锁定请求超时处理步骤
Interlocked.Increment(ref readerTimeouts);
}
}
//释放所有锁,之后恢复锁状态。
//使用序列号来确定另一个线程是否有
//获得了一个写锁,因为该线程最后一次访问资源。
static void ReleaseRestore(Random rnd, int timeOut)
{
int lastWriter;
try
{
rwl.AcquireReaderLock(timeOut);
try
{
//线程从共享资源中读取数据是安全的,
//读取和缓存资源的值。
int resourceValue = resource; // 缓存资源值。
Display("reads resource value " + resourceValue);
Interlocked.Increment(ref reads);
// 保存当前写入器序列号。
lastWriter = rwl.WriterSeqNum;
// 释放锁并保存一个cookie,以便稍后可以恢复锁。
LockCookie lc = rwl.ReleaseLock();
// 等待一个随机的时间间隔,然后恢复之前的锁状态。
Thread.Sleep(rnd.Next(250));
rwl.RestoreLock(ref lc);
//检查其他线程是否在这个时间间隔内获得写锁。
//如果不是,则资源的缓存值仍然有效。
if (rwl.AnyWritersSince(lastWriter))
{
resourceValue = resource;
Interlocked.Increment(ref reads);
Display("resource has changed: " + resourceValue);
}
else
{
Display("resource has not changed: " + resourceValue);
}
}
finally
{
// 确保锁已释放。
rwl.ReleaseReaderLock();
}
}
catch (ApplicationException)
{
// 读取锁定请求超时处理
Interlocked.Increment(ref readerTimeouts);
}
}
static void Display(string msg)
{
Console.Write("Thread {0} {1}. \r", Thread.CurrentThread.Name, msg);
}
}
}