C#之Lock锁

如果多个线程,访问同一个资源,电脑不支持在写(修改)的时候,去写(修改),所以要加锁

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 的使用方法、最佳实践以及常见陷阱。

一、为什么需要锁?

在多线程环境中,多个线程可能同时访问共享资源(如变量、集合、文件等)。如果没有适当的同步机制,可能会导致以下问题:

  1. 竞态条件(Race Condition):多个线程同时修改共享数据,导致不可预测的结果
  2. 数据不一致:共享数据被部分修改,导致状态不一致
  3. 死锁:线程互相等待对方释放资源,导致程序卡死

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 关键字的工作原理

  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.EnterMonitor.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方法中使用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();
    }
}

七、性能考虑

  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#中最简单、最常用的线程同步机制,适用于大多数需要互斥访问共享资源的场景。然而,它也有其局限性,特别是在异步编程和高竞争场景下。

最佳实践总结

  1. 使用专用的 readonly 对象作为锁
  2. 尽量缩小锁定的代码范围
  3. 避免在锁内调用可能阻塞或抛出异常的方法
  4. 考虑使用其他同步原语(如 SemaphoreSlim)来替代 lock 在特定场景下
  5. 在异步代码中使用 SemaphoreSlim 替代 lock

通过合理使用锁和其他同步机制,您可以构建出线程安全、高效的多线程C#应用程序。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值