聊透多线程编程-线程互斥与同步-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:适用于读多写少的场景。
相关推荐
Dm_dotnet10 小时前
在Avalonia/C#中使用依赖注入过程记录
c#
FAREWELL0007513 小时前
C#进阶学习(八)常见的泛型数据结构类(3)SortedDictionary<TKey, TValue>与SortedList<TKey, TValue>
数据结构·学习·c#·sorteddictioary·sortedlist
许_安13 小时前
leetcode刷题日记——单词规律
算法·leetcode·c#
o0向阳而生0o16 小时前
23、.NET和C#有什么区别?
开发语言·c#·.net
爱编程的鱼18 小时前
什么是 IDE?集成开发环境的功能与优势
开发语言·ide·python·学习·microsoft·c#
浅陌sss19 小时前
设计模式 --- 外观模式
设计模式·c#
ghost1431 天前
C#学习第17天:序列化和反序列化
开发语言·学习·c#
爱编程的鱼1 天前
C# 变量||C# 常量
java·开发语言·c#
DanmF--1 天前
详解与HTTP服务器相关操作
服务器·网络·网络协议·http·unity·c#