0

0

C#的lock关键字如何实现线程同步?适用场景是什么?

畫卷琴夢

畫卷琴夢

发布时间:2025-08-30 08:17:01

|

773人浏览过

|

来源于php中文网

原创

lock关键字通过Monitor类实现线程互斥,确保同一时间仅一个线程执行临界区代码,防止竞态条件;推荐使用私有、静态、只读的引用类型对象作为锁,以避免死锁和同步失效;适用于保护共享数据、单例初始化、外部资源访问等场景,但在高并发下存在性能瓶颈、死锁风险及无法区分读写等问题;可选用ReaderWriterLockSlim、SemaphoreSlim、Interlocked、并发集合等替代方案以提升性能和灵活性。

c#的lock关键字如何实现线程同步?适用场景是什么?

C# 中的

lock
关键字本质上提供了一种方便且强大的机制来实现线程同步,它通过确保在任何给定时刻,只有一个线程能够访问代码的特定关键部分,从而有效地避免了多线程环境下的竞态条件和数据不一致问题。说白了,它就像给一段代码加了一把锁,谁先拿到钥匙谁就能进去,别人就得在外面等着。

解决方案

lock
关键字是基于
System.Threading.Monitor
类实现的语法糖。当你使用
lock (expression)
语句时,编译器会将其转换为
Monitor.Enter(expression)
和一个
try/finally
块中的
Monitor.Exit(expression)
调用。这个
expression
必须是一个引用类型的对象(例如
object
实例,或者任何类的实例),它充当了锁的标志。

工作原理是这样的:当一个线程尝试进入

lock
块时,它会尝试获取
expression
对象上的互斥锁。如果这个锁当前没有被其他线程持有,当前线程就能成功获取锁并进入
lock
块执行代码。在
lock
块执行完毕(无论是正常退出还是因为异常)后,锁会被自动释放。如果锁已经被其他线程持有,那么尝试获取锁的线程就会被阻塞,直到持有锁的线程释放它为止。这种机制保证了在同一时间,只有一个线程能够执行被
lock
保护的代码段,从而实现了所谓的“互斥访问”。

一个非常重要的实践是,你锁定一个私有的、静态的、只读的

object
实例为什么是这样?

  • 私有 (private):防止外部代码获取到你的锁对象,从而导致意外的死锁或破坏你的同步机制。如果外部代码也能拿到这个对象并对其加锁,那你的内部逻辑就可能被外部的锁行为影响。
  • 静态 (static):如果你的锁是针对类级别的共享资源,那么它应该是静态的,这样所有实例都共享同一个锁。如果是非静态的,每个实例都会有自己的锁,这通常不是我们想要的线程同步。
  • 只读 (readonly):一旦初始化,锁对象就不会被改变。这避免了在运行时不小心将锁对象替换掉,导致同步失效。

来看一个简单的例子:

public class Counter
{
    private readonly object _lockObject = new object(); // 推荐的锁对象
    private int _count;

    public void Increment()
    {
        lock (_lockObject) // 确保每次只有一个线程能修改 _count
        {
            _count++;
            Console.WriteLine($"Current count: {_count}");
        }
    }

    public int GetCount()
    {
        // 读取操作也可能需要锁定,取决于业务逻辑和对数据一致性的要求
        // 如果_count的读取和写入是分离的,且读取不要求最新状态,可以不加锁
        // 但如果要求读取的是最新写入的值,或者读取本身涉及复杂操作,则仍需加锁
        lock (_lockObject)
        {
            return _count;
        }
    }
}

lock
关键字如何避免常见的线程安全问题?

在我看来,

lock
关键字最核心的价值就在于它能直接且有效地解决多线程编程中最让人头疼的“竞态条件”(Race Condition)问题。竞态条件通常发生在多个线程尝试同时访问和修改共享资源时,由于操作的非原子性,最终结果变得不可预测。

比如说,一个简单的

i++
操作,在C#底层它可能不是一个原子操作。它通常分为三步:1. 读取
i
的当前值;2. 将值加1;3. 将新值写回
i
。如果两个线程几乎同时执行
i++

  • 线程A读取
    i
    (假设为0)
  • 线程B读取
    i
    (也为0)
  • 线程A将
    i
    加1,并写回 (
    i
    变为1)
  • 线程B将
    i
    加1,并写回 (
    i
    变为1)

结果

i
最终变成了1,而不是期望的2。这就是典型的丢失更新。

lock
关键字通过将这段“读-改-写”的操作封装在一个互斥锁内部,强制这些步骤作为一个不可分割的整体(原子操作)来执行。当一个线程进入
lock
块时,它就“霸占”了这块代码,其他线程只能在外面干等着,直到当前线程完成所有操作并释放锁。这样,每个线程都能确保它在操作
i
时,不会有其他线程来“插队”或“捣乱”,从而保证了共享数据的完整性和一致性。它就像给关键操作加上了一个独占的“通行证”,一次只发一张,谁拿到谁先走,其他人排队。

在哪些具体场景下,使用
lock
关键字是最佳实践?

我个人觉得,

lock
关键字在很多场景下都是一种直观且高效的同步手段,尤其是在以下几种情况:

  1. 保护共享内存中的数据结构: 这是最常见的场景。比如,你有一个静态的

    Dictionary
    或者一个
    List
    ,多个线程可能同时向其中添加、删除或修改元素。由于这些集合类型本身不是线程安全的,直接并发操作会导致数据损坏或运行时异常。这时候,用
    lock
    包裹对这些集合的所有修改操作,就能确保数据的一致性。

    private static readonly object _cacheLock = new object();
    private static Dictionary _dataCache = new Dictionary();
    
    public static void AddOrUpdateCache(string key, string value)
    {
        lock (_cacheLock)
        {
            _dataCache[key] = value;
        }
    }
    
    public static string GetFromCache(string key)
    {
        lock (_cacheLock)
        {
            return _dataCache.TryGetValue(key, out var value) ? value : null;
        }
    }

    你看,无论是写入还是读取,都通过同一个锁对象来协调,避免了潜在的冲突。

  2. 管理单例模式的实例创建: 在多线程环境下,确保单例模式只创建一个实例是很有挑战性的。虽然现在有了

    Lazy
    这种更优雅的方案,但早期的双重检查锁定(Double-Checked Locking)模式就大量依赖
    lock
    来保证线程安全地创建单例。

    public class Singleton
    {
        private static Singleton _instance;
        private static readonly object _lock = new object();
    
        private Singleton() { } // 私有构造函数
    
        public static Singleton GetInstance()
        {
            if (_instance == null) // 第一次检查
            {
                lock (_lock) // 加锁
                {
                    if (_instance == null) // 第二次检查
                    {
                        _instance = new Singleton();
                    }
                }
            }
            return _instance;
        }
    }

    这种模式利用

    lock
    确保在实例真正创建时是互斥的。

  3. 对外部资源的独占访问: 当你的应用程序需要访问一个外部资源,比如文件、数据库连接池中的某个连接,或者一个串口设备时,如果这个资源不支持并发访问,那么你就需要用

    lock
    来确保在任何时候只有一个线程能操作它。这可以防止资源争用导致的错误或数据损坏。

  4. 控制特定逻辑流程的原子性: 有时候,你可能不只是要保护一个变量,而是要确保一系列相关的操作作为一个整体,不被其他线程打断。比如,一个复杂的业务逻辑涉及到多个步骤,这些步骤必须连续执行才能保证数据状态的正确性。

    lock
    可以把这些步骤打包成一个原子操作单元。

总的来说,当并发操作涉及对共享状态的修改,且这些修改必须是互斥的、原子性的,并且你对性能要求不是极端苛刻,同时锁的粒度可以接受时,

lock
关键字就是你的首选。它简单、直接、易于理解和使用。

lock
关键字的局限性与替代方案有哪些?

尽管

lock
关键字简单好用,但它也不是万能的,它有一些固有的局限性,这些局限性促使我们在某些特定场景下需要考虑更高级或更专业的同步机制。

稿定AI社区
稿定AI社区

在线AI创意灵感社区

下载

lock
的局限性:

  1. 性能瓶颈: 这是最显而易见的。

    lock
    是一种粗粒度的锁,它强制所有尝试进入临界区的线程排队等待。在高并发、高竞争的场景下,大量线程频繁地争抢同一个锁会导致严重的性能开销,因为线程上下文切换和锁的获取/释放操作本身就是耗时的。想象一下,如果一个锁被持有时间很长,或者有非常多的线程在等待,整个系统的吞吐量会急剧下降。

  2. 死锁风险:

    lock
    最让人头疼的问题之一就是容易引发死锁。当两个或多个线程各自持有一个锁,并尝试获取对方持有的锁时,就会发生死锁,它们会无限期地互相等待,导致程序停滞。例如:

    object lockA = new object();
    object lockB = new object();
    
    // 线程1
    lock (lockA)
    {
        Thread.Sleep(100); // 模拟工作
        lock (lockB) { /* ... */ }
    }
    
    // 线程2
    lock (lockB)
    {
        Thread.Sleep(100); // 模拟工作
        lock (lockA) { /* ... */ }
    }

    这种交叉锁定很容易导致死锁。解决死锁通常需要严格遵循锁的获取顺序,或者使用更复杂的策略。

  3. 无法区分读写操作:

    lock
    提供的是排他锁,也就是说,无论是读取还是写入共享资源,都需要获取独占锁。在读多写少的场景下,这会大大降低并发性。比如,100个线程要读取一个共享数据,如果每次读取都需要独占锁,那么99个线程都得等着,这显然效率不高。

  4. 不支持超时:

    lock
    是一种阻塞式操作,线程会无限期地等待直到获取到锁。如果锁永远无法释放(比如持有锁的线程崩溃了),等待的线程也会永远阻塞,这在某些需要响应性的应用中是不可接受的。

替代方案:

面对

lock
的局限性,.NET 提供了多种更灵活、更高效的同步原语和并发工具

  1. Monitor
    类:
    lock
    的底层就是
    Monitor
    。直接使用
    Monitor.Enter()
    ,
    Monitor.Exit()
    ,
    Monitor.TryEnter()
    可以提供更细粒度的控制,比如
    TryEnter
    允许你设置一个超时时间,避免无限等待。
    Monitor.Wait()
    Monitor.Pulse()
    /
    PulseAll()
    则可以实现线程间的协作(一个线程等待某个条件满足,另一个线程通知它)。

  2. ReaderWriterLockSlim
    这是解决读写锁问题的好方案。它允许多个线程同时获取读锁,但在写入时,只有当没有其他读锁或写锁被持有时,才能获取写锁。这大大提高了读多写少场景下的并发性能。

  3. SemaphoreSlim
    信号量,用于限制同时访问某个资源的线程数量。比如,你有一个数据库连接池,只想允许最多N个线程同时使用连接,就可以用
    SemaphoreSlim
    来控制。它比
    lock
    更灵活,因为它不要求独占访问,而是限制并发数量。

  4. Mutex
    互斥体,与
    lock
    类似,但它可以在进程之间进行同步。如果你的同步需求跨越多个进程,
    Mutex
    是一个选择。

  5. Interlocked
    类: 对于简单的原子操作,比如递增/递减整数、交换值等,
    Interlocked
    类提供了高性能的原子操作方法,无需使用锁。它直接利用CPU的原子指令,效率非常高,且不会引起上下文切换。

  6. 并发集合(

    System.Collections.Concurrent
    命名空间): 这是在多线程环境中处理集合的首选。例如
    ConcurrentDictionary
    ConcurrentQueue
    ConcurrentBag
    等。这些集合内部已经实现了线程安全,你无需自己加锁,它们通常采用无锁(lock-free)或细粒度锁的算法,性能远超手动加
    lock

  7. 任务并行库 (TPL) 和

    async/await
    虽然这不是直接的同步原语,但它们改变了我们编写并发代码的方式。通过使用异步编程,可以避免阻塞线程,从而提高应用程序的响应性和吞吐量。当然,即使使用
    async/await
    ,对于共享状态的访问,你仍然需要上述的同步机制。

选择哪种同步机制,往往取决于具体的场景、对性能的要求以及对复杂度的容忍度。

lock
简单直接,适用于保护小范围、低竞争的共享资源;而面对高并发、复杂协作或读写分离的场景,就得考虑
ReaderWriterLockSlim
、并发集合或更底层的
Monitor
等了。

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

通义千问
通义千问

阿里巴巴推出的全能AI助手

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
string转int
string转int

在编程中,我们经常会遇到需要将字符串(str)转换为整数(int)的情况。这可能是因为我们需要对字符串进行数值计算,或者需要将用户输入的字符串转换为整数进行处理。php中文网给大家带来了相关的教程以及文章,欢迎大家前来学习阅读。

523

2023.08.02

c++怎么把double转成int
c++怎么把double转成int

本专题整合了 c++ double相关教程,阅读专题下面的文章了解更多详细内容。

133

2025.08.29

C++中int、float和double的区别
C++中int、float和double的区别

本专题整合了c++中int和double的区别,阅读专题下面的文章了解更多详细内容。

103

2025.10.23

treenode的用法
treenode的用法

​在计算机编程领域,TreeNode是一种常见的数据结构,通常用于构建树形结构。在不同的编程语言中,TreeNode可能有不同的实现方式和用法,通常用于表示树的节点信息。更多关于treenode相关问题详情请看本专题下面的文章。php中文网欢迎大家前来学习。

539

2023.12.01

C++ 高效算法与数据结构
C++ 高效算法与数据结构

本专题讲解 C++ 中常用算法与数据结构的实现与优化,涵盖排序算法(快速排序、归并排序)、查找算法、图算法、动态规划、贪心算法等,并结合实际案例分析如何选择最优算法来提高程序效率。通过深入理解数据结构(链表、树、堆、哈希表等),帮助开发者提升 在复杂应用中的算法设计与性能优化能力。

21

2025.12.22

深入理解算法:高效算法与数据结构专题
深入理解算法:高效算法与数据结构专题

本专题专注于算法与数据结构的核心概念,适合想深入理解并提升编程能力的开发者。专题内容包括常见数据结构的实现与应用,如数组、链表、栈、队列、哈希表、树、图等;以及高效的排序算法、搜索算法、动态规划等经典算法。通过详细的讲解与复杂度分析,帮助开发者不仅能熟练运用这些基础知识,还能在实际编程中优化性能,提高代码的执行效率。本专题适合准备面试的开发者,也适合希望提高算法思维的编程爱好者。

32

2026.01.06

线程和进程的区别
线程和进程的区别

线程和进程的区别:线程是进程的一部分,用于实现并发和并行操作,而线程共享进程的资源,通信更方便快捷,切换开销较小。本专题为大家提供线程和进程区别相关的各种文章、以及下载和课程。

546

2023.08.10

Python 多线程与异步编程实战
Python 多线程与异步编程实战

本专题系统讲解 Python 多线程与异步编程的核心概念与实战技巧,包括 threading 模块基础、线程同步机制、GIL 原理、asyncio 异步任务管理、协程与事件循环、任务调度与异常处理。通过实战示例,帮助学习者掌握 如何构建高性能、多任务并发的 Python 应用。

234

2025.12.24

AO3官网入口与中文阅读设置 AO3网页版使用与访问
AO3官网入口与中文阅读设置 AO3网页版使用与访问

本专题围绕 Archive of Our Own(AO3)官网入口展开,系统整理 AO3 最新可用官网地址、网页版访问方式、正确打开链接的方法,并详细讲解 AO3 中文界面设置、阅读语言切换及基础使用流程,帮助用户稳定访问 AO3 官网,高效完成中文阅读与作品浏览。

53

2026.02.02

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
React 教程
React 教程

共58课时 | 4.5万人学习

Pandas 教程
Pandas 教程

共15课时 | 1万人学习

ASP 教程
ASP 教程

共34课时 | 4.4万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号