聊透多线程编程-线程互斥与同步-11. C# lock关键字实现线程互斥

目录

一、什么是临界区?

二、lock关键字的用途

三、lock的基本用法

四、lock关键字的工作原理

五、示例1-保护共享变量

六、示例2-与Monitor类配合实现线程间信号传递

七、注意事项

[八、 常见误区](#八、 常见误区)

九、替代方案


一、 什么是临界区?

在多线程编程中,临界区是指一段需要互斥访问的代码块,通常涉及对共享资源的操作。为了避免多个线程同时操作共享资源而导致数据竞争或状态不一致,我们需要对临界区代码进行保护。

下面通过一个简单的例子来说明什么是临界区代码,以及如何使用 lock 来保护它。

示例:两个线程操作共享变量

假设我们有一个共享变量 _counter,两个线程分别对其进行递增操作。如果不对临界区代码进行保护,可能会导致数据竞争和错误结果。

二、 lock关键字的用途

lock关键字在C#中用于确保当一个线程访问某个资源时,其他线程不能同时访问该资源,从而避免了数据竞争和不一致性的问题。它通过提供一种简单的方式实现线程同步,保证多线程环境下共享资源的安全访问。

三、 lock的基本用法

cs 复制代码
private static readonly object _lock = new object(); // 锁对象
lock (object)
{
    // 需要同步的代码块
}
  • object 是一个引用类型的对象,通常称为锁对象。
  • 当线程进入lock语句时,会尝试获取锁对象的互斥锁。如果成功获取锁,则执行代码块;否则,线程会被阻塞,直到锁被释放。

四、 lock关键字的工作原理

lock实际上是Monitor.Enter和Monitor.Exit的语法糖

当线程进入lock语句时:

  • 调用Monitor.Enter(object)获取锁。
  • 如果锁已被其他线程占用,则当前线程会被挂起,直到锁被释放。

当线程退出lock语句时:

  • 调用Monitor.Exit(object)释放锁。

五、 示例1- 保护共享变量

假设有一个共享变量_counter,多个线程可能会同时修改它的值。为了确保线程安全,可以使用lock来保护对_counter的访问。

cs 复制代码
using System;
using System.Threading;

class Program
{
    private static int _counter = 0;
    private static readonly object _lock = new object();

    static void Main()
    {
        Thread t1 = new Thread(IncrementCounter);
        Thread t2 = new Thread(IncrementCounter);

        t1.Start();
        t2.Start();

        t1.Join();
        t2.Join();

        Console.WriteLine($"Final Counter Value: {_counter}");
    }

    static void IncrementCounter()
    {
        for (int i = 0; i < 100000; i++)
        {
            lock (_lock)
            {
                _counter++;
            }
        }
    }
}

解释:

  • _lock是一个静态对象,作为锁对象。
  • 每次访问_counter时,都会通过lock确保只有一个线程能够修改它。
  • 最终输出的结果是200000,因为所有线程的操作都被正确同步了。

六、 示例2-与Monitor类配合实现线程间信号传递

假设我们有两个线程:

  • 线程A:等待某个条件满足后再继续执行。
  • 线程B:负责触发这个条件,并通知线程A继续执行。
cs 复制代码
using System;
using System.Threading;

class Program
{
    private static readonly object _lock = new object();
    private static bool _isReady = false; // 共享的状态变量

    static void Main(string[] args)
    {
        Thread threadA = new Thread(DoWorkA);
        Thread threadB = new Thread(DoWorkB);

        threadA.Start();
        threadB.Start();

        threadA.Join();
        threadB.Join();
    }

    static void DoWorkA()
    {
        lock (_lock)
        {
            Console.WriteLine("Thread A: Waiting for signal...");

            // 等待条件满足
            while (!_isReady)
            {
                Monitor.Wait(_lock); // 释放锁并等待
            }

            Console.WriteLine("Thread A: Received signal. Continuing work.");
        }
    }

    static void DoWorkB()
    {
        Thread.Sleep(2000); // 模拟一些工作

        lock (_lock)
        {
            Console.WriteLine("Thread B: Preparing to signal...");

            // 设置条件为 true
            _isReady = true;

            // 通知等待的线程
            Monitor.Pulse(_lock);
            Console.WriteLine("Thread B: Signal sent.");
        }
    }
}

****注意:****即使线程A和线程B中的临界区代码不同,只要它们使用的是同一个锁对象 _lock,系统就会保证这两个线程不会同时执行这些临界区代码。

七、 注意事项

(1) 锁对象的选择

  • 锁对象必须是引用类型(如object、string等),不能是值类型(如int、struct等)。
  • 推荐使用专用的私有对象作为锁对象,而不是公共对象或字符串常量。例如:
cs 复制代码
private static readonly object _lock = new object();
  • 不要使用this作为锁对象,因为外部代码可能也会锁定该对象,导致死锁风险。
  • 锁对象尽量设置为readonly,如果 _lock 是一个普通的对象(非 readonly),它可能会被意外修改或重新赋值,在这种情况下,其他线程可能尝试锁定一个新的 _lock 对象,而不是原来的对象,从而破坏了同步逻辑。

(2) 避免死锁

  • 死锁是指两个或多个线程互相等待对方释放锁,导致程序无法继续运行。
  • 避免死锁的方法包括:
    • 确保锁的获取顺序一致。
    • 尽量减少锁的范围,只锁定必要的代码块。

(3) 性能影响

  • 使用lock会导致线程阻塞,因此可能会影响性能。
  • 对于高并发场景,可以考虑使用其他同步机制(如SemaphoreSlim、ReaderWriterLockSlim等)。

八、 常见误区

(1) 锁的范围过大

  • 如果锁的范围过大,会导致线程阻塞时间过长,降低程序的并发性能。
  • 应尽量缩小锁的范围,只锁定需要同步的代码部分。

(2) 忘记释放锁

  • 在正常情况下,lock语句会自动释放锁。但如果在 lock 代码块中抛出异常且未正确处理,可能会导致锁无法释放
  • 确保lock代码块中的逻辑是安全的,避免抛出未捕获的异常。

九、 替代方案

对于某些特定的应用场景,可以考虑使用以下替代方案:

  • Monitor :直接使用Monitor可以提供更细粒度的控制。
  • Mutex:适用于跨进程的同步。
  • SemaphoreSlim:适用于限制并发线程数。
  • ReaderWriterLockSlim:适用于读多写少的场景。
相关推荐
敲代码的 蜡笔小新1 小时前
【行为型之中介者模式】游戏开发实战——Unity复杂系统协调与通信架构的核心秘诀
unity·设计模式·c#·中介者模式
程序猿多布1 小时前
使用Visual Studio将C#程序发布为.exe文件
c#·visual studio
老衲有点帅3 小时前
C#多线程Thread
开发语言·c#
PascalMing4 小时前
C# 通过脚本实现接口
c#·codeanalysis·接口派生
敲代码的 蜡笔小新8 小时前
【行为型之观察者模式】游戏开发实战——Unity事件驱动架构的核心实现策略
观察者模式·unity·设计模式·c#
向宇it8 小时前
【unity游戏开发——编辑器扩展】使用EditorGUI的EditorGUILayout绘制工具类在自定义编辑器窗口绘制各种UI控件
开发语言·ui·unity·c#·编辑器·游戏引擎
FAREWELL0007514 小时前
Unity基础学习(九)输入系统全解析:鼠标、键盘与轴控制
学习·unity·c#·游戏引擎
码观天工15 小时前
【.NET必读】RabbitMQ 4.0+重大变更!C#开发者必须掌握的6大升级要点
c#·rabbitmq·.net·mq
绿龙术士17 小时前
构建现代化WPF应用:数据驱动开发与高级特性解析
c#·wpf
o0向阳而生0o18 小时前
43、Server.UrlEncode、HttpUtility.UrlDecode的区别?
c#·.net