『7x24小时有问必答』
         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++;                        }                  }            }

      }}

CLock锁完全指南:线程同步的核心机制
在多线程编程中,线程安全是至关重要的。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>

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

上一主题上一主题         下一主题下一主题
QQ手机版小黑屋粤ICP备17165530号

关于我们·投诉举报· 用户帮助· 联系我们 · 本站服务 · 版权声明· 隐私政策 · 投搞指南

法律保护:PLC技术网,plcjs.com,plcjs.net等字样
Copyright 2010-2030. All rights reserved. 


微信公众号二维码 抖音二维码 百家号二维码 今日头条二维码哔哩哔哩二维码