线程锁是多线程并发共享数据,保证一致性的工具。多线程可以同时运行多个任务但是当多个线程同时访问共享数据时,可能导致数据不同步。当有多个线程访问同一对象的加锁方法或代码块时,同一时间只有一个线程在执行,其余线程必须要等待当前线程执行完之后才能执行该代码段。但其余线程是可以访问该对象中的非加锁代码块的。以下介绍.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);
- }
- }
- }