|
1、如果多个线程,访问同一个资源,电脑不支持在写(修改)的时候,去写(修改),所以要加锁。 namespace C_之Lock{ public partial class Form1 : Form { //锁:object对象 //static:静态:这个对象在程序启动之前就生成,在程序消失之后在销毁 //全局唯一存在 public static object lockObj = new object(); /// <summary> /// 计数变量 /// public int Count = 0; public Form1() { InitializeComponent(); } private void button1_Click(object sender, EventArgs e) { Thread thread1 = new Thread(method); Thread thread2 = new Thread(method); Thread thread3 = new Thread(method); thread1.Start(); thread2.Start(); thread3.Start(); //等待现线程结束 thread1.Join(); thread2.Join(); thread3.Join(); MessageBox.Show($"最后的数字 {Count}"); } private void method(object? obj) { for (int i = 0; i < 100000; i++) { //锁,当一个线程在访问下面代码的时候,另一个线程会被阻挡在外面 //不加锁,不能正常完成计数 lock (lockObj) { Count++; } } } }} 在多线程编程中,线程安全是至关重要的。C提供了多种同步机制,其中lock关键字是最简单、最常用的同步工具之一。本教程将深入探讨lock的使用方法、最佳实践及常见陷阱。 一、为什么需要锁? 在多线程环境中,多个线程可能同时访问共享资源(如变量、集合、文件等)。如果没有适当的同步机制,可能会导致以下问题: 1、竟态条件;多个线程同步修改共享数据,导致不可预测的结果。 2、数据不一致:共享数据被部分修改,导致状态不一致。 3、死锁:线程相互等待对方释放资源,导致程序卡死。 lock关键字通过提供互坼锁(Mutex)机制来解决这些问题。 二、Lock基本语法 lock关键字用于确保一个代码块在同一事件只由一个线程执行: lock (lockObject){ // 临界区代码 // 只有获得锁的线程才能执行这部分代码} 2.1 基本示例 private void button2_Click(object sender, EventArgs e){ 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); MessageBox.Show($"最终计数:{counter.GetCount()}");//应该输出10000}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; } }} 三、Lock关键字的工作原理 1、互坼性:同一时间只有一个线程可以持有锁。 2、重入性:同一个线程可以多次获取同一个锁(递归锁) 3、阻塞机制:未获得锁的线程会被阻塞,直到锁被释放。 四、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防止死锁 死锁发生时,两个或多个线程相互等待对方释放锁,避免死锁的策略: 1、保持锁定顺序一致:如果多个线程需要获取多个锁,确保它们以相同的顺序获取。 2、避免嵌套锁:尽量减少锁的嵌套。 3、使用超时:考虑使用Monitor.TryEnter设置超时时间。 // 使用TryEnter设置超时if (Monitor.TryEnter(_lockObj, TimeSpan.FromSeconds(1))){ try { // 临界区代码 } finally { Monitor.Exit(_lockObj); }}else{ // 处理获取锁失败的情况} 五、Lock的代替方案 虽然lock是最常用的同步机制,但在某些情况下,其他同步原语可能更适合: 1.Monitor类:lock实际上是Monitor.Enter和Monitor.Exit的语法糖 Monitor.Enter(_lockObj);try{ // 临界区代码}finally{ Monitor.Exit(_lockObj);} 2.Mutex类:跨进程同步 using (var mutex = new Mutex(false, "Global\\MyMutex")){ mutex.WaitOne(); try { // 临界区代码 } finally { mutex.ReleaseMutex(); }} 3、Semaphore/SemaphoreSlim:限制同时访问资源的线程数 private static SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1);await _semaphore.WaitAsync();try{ // 临界区代码}finally{ _semaphore.Release();} 4.ReaderWriterLockSlim:读多写少的场景 private readonly ReaderWriterLockSlim _rwLock = new ReaderWriterLockSlim();// 读操作_rwLock.EnterReadLock();try{ // 读取数据}finally{ _rwLock.ExitReadLock();}// 写操作_rwLock.EnterWriteLock();try{ // 修改数据}finally{ _rwLock.ExitWriteLock();} 六、高级主题:锁与异步编程 在异步代码中使用锁需要特别注意,因为lock不能与async/await一起使用: // 错误示例 - 不能在async方法中使用lockpublic 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(); }} 七、性能考虑 1.锁的粒度:锁定的代码越小越好,但也要平衡代码复杂度 2.锁的竞争:高竞争的锁会成为性能瓶颈 3.代替方案:对于读多写少的场景,考虑使用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应用程序 </tkey,></tkey,></tkey,></summary> 免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |