如果多个线程,访问同一个资源,电脑不支持在写(修改)的时候,去写(修改),所以要加锁
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace C_之Lock锁
{
public partial class Form1 : Form
{
// 锁:object对象
// static : 静态:这个对象在程序启动之前就生成,在程序消失之后再销毁
// 全局唯一存在
public static object lockObj = new object();
// 计数变量
public int Count = 0;
public Form1()
{
InitializeComponent();
}
private void button1_Click(object sender, EventArgs e)
{
Thread thread1 = new Thread(Inc);
Thread thread2 = new Thread(Inc);
Thread thread3 = new Thread(Inc);
thread1.Start();
thread2.Start();
thread3.Start();
// 等待线程结束
thread1.Join();
thread2.Join();
thread3.Join();
// 使用美元符号$和{} 可以在字符串中加变量
MessageBox.Show($"最后的数字 {Count}");
MessageBox.Show("最后的数字"+ Count);
}
/// <summary>
/// 程序就不会崩溃,因为电脑不支持,在修改的时候,还要修改
/// </summary>
public void Inc()
{
for (int i=0; i<10;i++)
{
// 锁,当一个线程在访问下面代码的时候,另一个线程会被阻挡在外面
lock(lockObj)
{
// 加1
Count++;
}
}
}
}
}
C# Lock 锁完全指南:线程同步的核心机制
在多线程编程中,线程安全是至关重要的。C# 提供了多种同步机制,其中 lock
关键字是最简单、最常用的同步工具之一。本教程将深入探讨 lock
的使用方法、最佳实践以及常见陷阱。
一、为什么需要锁?
在多线程环境中,多个线程可能同时访问共享资源(如变量、集合、文件等)。如果没有适当的同步机制,可能会导致以下问题:
- 竞态条件(Race Condition):多个线程同时修改共享数据,导致不可预测的结果
- 数据不一致:共享数据被部分修改,导致状态不一致
- 死锁:线程互相等待对方释放资源,导致程序卡死
lock
关键字通过提供互斥锁(Mutex)机制来解决这些问题。
二、Lock 基本语法
lock
关键字用于确保一个代码块在同一时间只由一个线程执行:
lock (lockObject)
{
// 临界区代码
// 只有获得锁的线程才能执行这部分代码
}
2.1 基本示例
using System;
using System.Threading;
class Counter
{
private int _count = 0;
private readonly object _lockObj = new object(); // 专用锁对象
public void Increment()
{
lock (_lockObj) // 获取锁
{
_count++; // 临界区
} // 自动释放锁
}
public int GetCount()
{
lock (_lockObj)
{
return _count;
}
}
}
class Program
{
static void Main()
{
Counter counter = new Counter();
// 创建多个线程同时增加计数器
for (int i = 0; i < 10; i++)
{
new Thread(() =>
{
for (int j = 0; j < 1000; j++)
{
counter.Increment();
}
}).Start();
}
// 等待所有线程完成
Thread.Sleep(1000);
Console.WriteLine($"最终计数: {counter.GetCount()}"); // 应该输出10000
}
}
三、Lock 关键字的工作原理
- 互斥性:同一时间只有一个线程可以持有锁
- 重入性:同一个线程可以多次获取同一个锁(递归锁)
- 阻塞机制:未获得锁的线程会被阻塞,直到锁被释放
四、Lock 的最佳实践
4.1 使用专用锁对象
错误做法:直接锁定值类型或字符串(因为值类型会被装箱,导致不同的锁对象)
// 错误示例 - 不要这样做!
lock (1) { ... } // 1会被装箱,每次都是不同的对象
lock ("string") { ... } // 字符串驻留可能导致意外行为
正确做法:使用专用的 readonly
引用类型对象作为锁
private readonly object _lockObj = new object();
4.2 缩小锁的范围
只锁定必要的代码块,避免长时间持有锁:
// 错误示例 - 锁范围过大
lock (_lockObj)
{
// 准备数据...
// 长时间运行的操作...
// 更新共享数据...
}
// 正确示例 - 缩小锁范围
// 准备数据(不需要锁)
var data = PrepareData();
lock (_lockObj)
{
// 仅锁定共享数据的更新
UpdateSharedData(data);
}
// 后续处理(不需要锁)
ProcessData(data);
4.3 避免在锁内调用未知代码
不要在锁定的代码块中调用可能阻塞或抛出异常的方法:
// 错误示例
lock (_lockObj)
{
// 如果ExternalMethod抛出异常,锁将不会被释放!
ExternalMethod();
}
// 正确做法 - 使用try-finally确保锁释放
lock (_lockObj)
{
try
{
ExternalMethod();
}
catch
{
// 处理异常,但锁仍然会被释放
throw;
}
}
4.4 防止死锁
死锁发生时,两个或多个线程互相等待对方释放锁。避免死锁的策略:
- 保持锁定顺序一致:如果多个线程需要获取多个锁,确保它们以相同的顺序获取
- 避免嵌套锁:尽量减少锁的嵌套使用
- 使用超时:考虑使用
Monitor.TryEnter
设置超时时间
// 使用TryEnter设置超时
if (Monitor.TryEnter(_lockObj, TimeSpan.FromSeconds(1)))
{
try
{
// 临界区代码
}
finally
{
Monitor.Exit(_lockObj);
}
}
else
{
// 处理获取锁失败的情况
}
五、Lock 的替代方案
虽然 lock
是最常用的同步机制,但在某些情况下,其他同步原语可能更合适:
-
Monitor 类:
lock
实际上是Monitor.Enter
和Monitor.Exit
的语法糖Monitor.Enter(_lockObj); try { // 临界区代码 } finally { Monitor.Exit(_lockObj); }
-
Mutex 类:跨进程同步
using (var mutex = new Mutex(false, "Global\\MyMutex")) { mutex.WaitOne(); try { // 临界区代码 } finally { mutex.ReleaseMutex(); } }
-
Semaphore/SemaphoreSlim:限制同时访问资源的线程数
private static SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1); await _semaphore.WaitAsync(); try { // 临界区代码 } finally { _semaphore.Release(); }
-
ReaderWriterLockSlim:读多写少的场景
private readonly ReaderWriterLockSlim _rwLock = new ReaderWriterLockSlim(); // 读操作 _rwLock.EnterReadLock(); try { // 读取数据 } finally { _rwLock.ExitReadLock(); } // 写操作 _rwLock.EnterWriteLock(); try { // 修改数据 } finally { _rwLock.ExitWriteLock(); }
六、高级主题:锁与异步编程
在异步代码中使用锁需要特别注意,因为 lock
不能与 async/await
一起使用:
// 错误示例 - 不能在async方法中使用lock
public async Task BadAsyncMethod()
{
lock (_lockObj) // 编译错误!
{
await SomeAsyncOperation();
}
}
解决方案:使用 SemaphoreSlim
替代
private static SemaphoreSlim _asyncLock = new SemaphoreSlim(1, 1);
public async Task SafeAsyncMethod()
{
await _asyncLock.WaitAsync();
try
{
await SomeAsyncOperation();
}
finally
{
_asyncLock.Release();
}
}
七、性能考虑
- 锁的粒度:锁定的代码块越小越好,但也要平衡代码复杂度
- 锁的竞争:高竞争的锁会成为性能瓶颈
- 替代方案:对于读多写少的场景,考虑使用
ReaderWriterLockSlim
或无锁数据结构
八、实际案例:线程安全的缓存
using System;
using System.Collections.Generic;
using System.Threading;
public class ThreadSafeCache<TKey, TValue>
{
private readonly Dictionary<TKey, TValue> _cache = new Dictionary<TKey, TValue>();
private readonly object _lockObj = new object();
public TValue GetOrAdd(TKey key, Func<TKey, TValue> valueFactory)
{
// 先尝试不加锁读取
if (_cache.TryGetValue(key, out var value))
{
return value;
}
// 加锁确保只有一个线程添加新值
lock (_lockObj)
{
// 再次检查,防止其他线程已经添加了值
if (_cache.TryGetValue(key, out value))
{
return value;
}
// 计算新值并添加到缓存
value = valueFactory(key);
_cache[key] = value;
return value;
}
}
public void Remove(TKey key)
{
lock (_lockObj)
{
_cache.Remove(key);
}
}
}
// 使用示例
class Program
{
static void Main()
{
var cache = new ThreadSafeCache<string, int>();
// 多个线程同时访问缓存
for (int i = 0; i < 5; i++)
{
new Thread(() =>
{
var result = cache.GetOrAdd("test", key =>
{
Console.WriteLine($"计算值 for {key}");
return key.Length;
});
Console.WriteLine($"线程 {Thread.CurrentThread.ManagedThreadId} 获取的值: {result}");
}).Start();
}
Thread.Sleep(1000);
}
}
九、常见问题解答
Q1: 锁定的对象可以是任何类型吗?
A1: 锁定对象必须是引用类型(通常是 object
),不能是值类型。最佳实践是使用专用的 readonly
对象作为锁。
Q2: 锁会导致死锁吗?如何避免?
A2: 是的,不正确的锁使用会导致死锁。避免方法包括:
- 保持锁定顺序一致
- 缩小锁的范围
- 使用超时机制
- 避免嵌套锁
Q3: 锁会影响性能吗?
A3: 锁会引入一定的性能开销,特别是在高竞争场景下。对于读多写少的场景,考虑使用 ReaderWriterLockSlim
或无锁数据结构。
Q4: 可以在锁内调用 await
吗?
A4: 不能直接在 lock
块内使用 await
,但可以使用 SemaphoreSlim
等异步友好的同步原语。
十、总结
lock
关键字是C#中最简单、最常用的线程同步机制,适用于大多数需要互斥访问共享资源的场景。然而,它也有其局限性,特别是在异步编程和高竞争场景下。
最佳实践总结:
- 使用专用的
readonly
对象作为锁 - 尽量缩小锁定的代码范围
- 避免在锁内调用可能阻塞或抛出异常的方法
- 考虑使用其他同步原语(如
SemaphoreSlim
)来替代lock
在特定场景下 - 在异步代码中使用
SemaphoreSlim
替代lock
通过合理使用锁和其他同步机制,您可以构建出线程安全、高效的多线程C#应用程序。