线程同步的意义

一、C# 线程同步的核心概念与作用

线程同步 是多线程编程中控制共享资源访问顺序 的技术,目的是解决竞态条件 (多个线程无序操作共享资源导致数据不一致),确保程序在多线程环境下的数据正确性行为可预测性

核心作用:
  1. 保证原子性 :将多步操作(如a = a + 1)变为 "不可分割" 的原子操作,避免中间状态被其他线程干扰。
  2. 保证可见性:确保一个线程对共享变量的修改,能被其他线程立即感知(避免 CPU 缓存导致的 "脏读")。
  3. 保证有序性:禁止编译器 / CPU 对指令的乱序优化,确保代码执行顺序符合预期。

二、C# 中常用的线程同步方式

1. 线程同步的意义(基础案例:未同步的问题)

问题场景:多个线程同时修改共享计数器,导致结果错误。

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

class ThreadSyncDemo
{
    // 共享资源:未同步的计数器
    private static int _counter = 0;

    static void Main()
    {
        Console.WriteLine("=== 未同步的计数器 ===");
        // 启动10个线程同时递增计数器
        for (int i = 0; i < 10; i++)
        {
            new Thread(IncrementCounter).Start();
        }

        // 等待所有线程完成(简单模拟,实际应使用Join)
        Thread.Sleep(1000);
        Console.WriteLine($"最终计数器值:{_counter}"); // 预期10,实际可能小于10
    }

    static void IncrementCounter()
    {
        // 非原子操作:读取→递增→写入
        int temp = _counter;
        Thread.Sleep(1); // 模拟耗时操作,放大竞态条件
        _counter = temp + 1;
        Console.WriteLine($"线程{Thread.CurrentThread.ManagedThreadId}:当前值={_counter}");
    }
}

输出(示例)

cs 复制代码
=== 未同步的计数器 ===
线程4:当前值=1
线程5:当前值=1
线程6:当前值=2
线程7:当前值=3
...
最终计数器值:8

原因 :多个线程同时读取_counter的旧值,导致递增操作被覆盖。

2. 监视器(Monitor)与 lock 关键字

lockMonitor的语法糖,保证临界区代码互斥执行

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

class MonitorLockDemo
{
    private static int _counter = 0;
    // 锁对象:必须是引用类型,且私有只读(避免外部干扰)
    private static readonly object _lockObj = new object();

    static void Main()
    {
        Console.WriteLine("=== 使用lock的计数器 ===");
        for (int i = 0; i < 10; i++)
        {
            new Thread(IncrementCounterWithLock).Start();
        }

        Thread.Sleep(1000);
        Console.WriteLine($"最终计数器值:{_counter}"); // 稳定输出10
    }

    static void IncrementCounterWithLock()
    {
        // lock自动包含try-finally,确保锁释放
        lock (_lockObj)
        {
            int temp = _counter;
            Thread.Sleep(1);
            _counter = temp + 1;
            Console.WriteLine($"线程{Thread.CurrentThread.ManagedThreadId}:当前值={_counter}");
        }
    }
}

输出

cs 复制代码
=== 使用lock的计数器 ===
线程4:当前值=1
线程5:当前值=2
线程6:当前值=3
...
最终计数器值:10

原理lock会调用Monitor.Enter(_lockObj)获取锁,finally中调用Monitor.Exit(_lockObj)释放锁,确保同一时间只有一个线程进入临界区。

3. Lock(C# 13 新特性)

C# 13 引入的System.Threading.Lock,通过作用域自动管理锁,比Monitor更简洁高效。

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

class NewLockDemo
{
    private static int _counter = 0;
    // C# 13的Lock类型
    private static readonly Lock _newLock = new Lock();

    static async Task Main()
    {
        Console.WriteLine("=== 使用C# 13 Lock的计数器 ===");
        // 启动10个并发任务
        Task[] tasks = new Task[10];
        for (int i = 0; i < 10; i++)
        {
            tasks[i] = Task.Run(IncrementCounterWithNewLock);
        }

        await Task.WhenAll(tasks);
        Console.WriteLine($"最终计数器值:{_counter}"); // 稳定输出10
    }

    static void IncrementCounterWithNewLock()
    {
        // using作用域结束时自动释放锁
        using (_newLock.EnterScope())
        {
            int temp = _counter;
            Thread.Sleep(1);
            _counter = temp + 1;
            Console.WriteLine($"线程{Thread.CurrentThread.ManagedThreadId}:当前值={_counter}");
        }
    }
}

优势 :无需手动处理try-finally,锁生命周期与代码块一致,降低死锁风险。

4. volatile 关键字

volatile确保变量的可见性 (禁止 CPU 缓存)和有序性(禁止指令重排),但不保证原子性。

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

class VolatileDemo
{
    // volatile标记:确保线程间可见
    private static volatile bool _isRunning = true;

    static void Main()
    {
        Console.WriteLine("=== volatile示例 ===");
        new Thread(Worker).Start();

        // 主线程修改_isRunning
        Thread.Sleep(1000);
        _isRunning = false;
        Console.WriteLine("主线程:已设置_isRunning=false");
    }

    static void Worker()
    {
        int count = 0;
        // 若_isRunning不标记volatile,Worker可能永远无法感知到修改
        while (_isRunning)
        {
            count++;
        }
        Console.WriteLine($"Worker线程:退出循环,执行次数={count}");
    }
}

输出

cs 复制代码
=== volatile示例 ===
主线程:已设置_isRunning=false
Worker线程:退出循环,执行次数=...

注意volatile仅适用于boolint等简单类型,复杂操作(如_counter++)仍需配合锁。

5. System.Threading.Interlocked

提供原子操作(如递增、交换),底层由 CPU 指令支持,比锁更高效。

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

class InterlockedDemo
{
    private static int _counter = 0;

    static void Main()
    {
        Console.WriteLine("=== Interlocked原子操作 ===");
        for (int i = 0; i < 10; i++)
        {
            new Thread(IncrementWithInterlocked).Start();
        }

        Thread.Sleep(1000);
        Console.WriteLine($"最终计数器值:{_counter}"); // 稳定输出10
    }

    static void IncrementWithInterlocked()
    {
        // 原子递增:读取→递增→写入是一个不可分割的操作
        Interlocked.Increment(ref _counter);
        Console.WriteLine($"线程{Thread.CurrentThread.ManagedThreadId}:当前值={_counter}");
        Thread.Sleep(1);
    }
}

常用方法

  • Increment(ref int):原子递增
  • Decrement(ref int):原子递减
  • Exchange(ref T, T):原子交换值
  • CompareExchange(ref T, T, T):原子比较并交换
6. System.Threading.Mutex

Mutex(互斥锁)支持跨进程同步 ,比lock更重(涉及系统调用)。

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

class MutexDemo
{
    private static int _counter = 0;
    // 命名Mutex:支持跨进程同步
    private static readonly Mutex _mutex = new Mutex(false, "MyAppMutex");

    static void Main()
    {
        Console.WriteLine("=== Mutex示例 ===");
        for (int i = 0; i < 10; i++)
        {
            new Thread(IncrementWithMutex).Start();
        }

        Thread.Sleep(1000);
        Console.WriteLine($"最终计数器值:{_counter}"); // 稳定输出10
    }

    static void IncrementWithMutex()
    {
        // 请求获取Mutex(阻塞直到获取)
        _mutex.WaitOne();
        try
        {
            int temp = _counter;
            Thread.Sleep(1);
            _counter = temp + 1;
            Console.WriteLine($"线程{Thread.CurrentThread.ManagedThreadId}:当前值={_counter}");
        }
        finally
        {
            // 必须释放Mutex,否则其他线程永久阻塞
            _mutex.ReleaseMutex();
        }
    }
}

场景:确保应用程序单实例运行、跨进程共享文件 / 硬件资源。

7. 重新发送事件(线程同步中的 "重试机制")

当线程获取锁失败时,通过重试 + 延迟避免频繁竞争,提升性能。

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

class RetryEventDemo
{
    private static int _counter = 0;
    private static readonly object _lockObj = new object();

    static void Main()
    {
        Console.WriteLine("=== 重新发送事件(重试机制) ===");
        for (int i = 0; i < 10; i++)
        {
            new Thread(IncrementWithRetry).Start();
        }

        Thread.Sleep(2000);
        Console.WriteLine($"最终计数器值:{_counter}");
    }

    static void IncrementWithRetry()
    {
        bool lockAcquired = false;
        // 重试最多5次,每次失败后延迟10ms
        for (int retry = 0; retry < 5 && !lockAcquired; retry++)
        {
            // 尝试获取锁(超时10ms)
            lockAcquired = Monitor.TryEnter(_lockObj, 10);
            if (lockAcquired)
            {
                try
                {
                    int temp = _counter;
                    Thread.Sleep(1);
                    _counter = temp + 1;
                    Console.WriteLine($"线程{Thread.CurrentThread.ManagedThreadId}:当前值={_counter}(第{retry+1}次尝试成功)");
                }
                finally
                {
                    Monitor.Exit(_lockObj);
                }
            }
            else
            {
                Console.WriteLine($"线程{Thread.CurrentThread.ManagedThreadId}:第{retry+1}次尝试获取锁失败,重试...");
            }
        }
    }
}

输出(示例)

cs 复制代码
=== 重新发送事件(重试机制) ===
线程4:第1次尝试获取锁失败,重试...
线程5:第1次尝试获取锁成功,当前值=1
线程4:第2次尝试获取锁成功,当前值=2
...
8. 线程本地存储(TLS)

为每个线程创建独立的变量副本 ,避免共享资源竞争(无需同步)。C# 中常用ThreadLocal<T>实现。

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

class ThreadLocalStorageDemo
{
    // 每个线程有独立的计数器副本
    private static ThreadLocal<int> _threadLocalCounter = new ThreadLocal<int>(() => 0);

    static void Main()
    {
        Console.WriteLine("=== 线程本地存储 ===");
        // 启动3个线程,每个线程递增自己的副本
        for (int i = 0; i < 3; i++)
        {
            new Thread(IncrementThreadLocalCounter).Start();
        }

        Thread.Sleep(1000);
        Console.WriteLine("主线程:所有线程执行完毕");
    }

    static void IncrementThreadLocalCounter()
    {
        for (int i = 0; i < 3; i++)
        {
            _threadLocalCounter.Value++;
            Console.WriteLine($"线程{Thread.CurrentThread.ManagedThreadId}:本地计数器={_threadLocalCounter.Value}");
            Thread.Sleep(100);
        }
        // 释放资源(可选)
        _threadLocalCounter.Dispose();
    }
}

输出

cs 复制代码
=== 线程本地存储 ===
线程4:本地计数器=1
线程5:本地计数器=1
线程6:本地计数器=1
线程4:本地计数器=2
线程5:本地计数器=2
...

优势:完全避免线程间竞争,适用于 "每个线程独立状态" 的场景(如数据库连接缓存)。

三、总结

C# 线程同步的核心是控制共享资源的访问顺序,不同技术的适用场景:

|-----------------------|---------------------|---------------------------------------|
| 技术 | | 核心作用 | |------| | | 适用场景 | |------| |
| lock/Monitor | 互斥访问临界区 | 大多数共享资源同步 |
| System.Threading.Lock | 简化锁管理 | |----------------| | C# 13 + 的高性能同步 | |
| volatile | 保证可见性 / 有序性 | 简单变量的线程间状态同步 |
| Interlocked | 原子操作 | 简单数值的递增 / 交换 |
| Mutex | 跨进程同步 | 多进程共享资源 |
| 重新发送事件 | |-------| | 减少锁竞争 | | 高并发场景的锁重试 |
| ThreadLocal<T> | 线程独立副本 | 每个线程的私有状态 |

补充说明(选型核心原则)

  1. 优先选轻量级方案:能不用锁就不用(如 ThreadLocal)→ 能用原子操作就不用锁(Interlocked)→ 能用用户态锁(lock/Lock)就不用内核态锁(Mutex);
  2. 避免过度同步:仅对 "真正共享的资源" 加同步,线程私有数据直接用 TLS;
  3. 死锁规避
    • lock/Mutex 需保证 "锁的获取顺序一致"(如先锁 A 再锁 B,所有线程都遵循);
    • 尽量缩短临界区代码(只包裹必要操作,避免在锁内做 IO / 耗时计算);
  4. 性能权衡
    • 高并发计数器:优先 Interlocked,而非 lock;
    • 跨进程同步:只能用 Mutex;
    • 简单状态标记:用 volatile 替代 lock。
相关推荐
yugi9878388 小时前
基于C#实现的WiFi信号强度扫描程序
开发语言·c#
sali-tec8 小时前
C# 基于halcon的视觉工作流-章70 深度学习-Deep OCR
开发语言·人工智能·深度学习·算法·计算机视觉·c#·ocr
武藤一雄8 小时前
C#中常见集合都有哪些?
开发语言·微软·c#·.net·.netcore
唐青枫8 小时前
C#.NET struct 全解析:什么时候该用值类型?
c#·.net
kaikaile199517 小时前
基于C#实现一维码和二维码打印程序
开发语言·c#
我不是程序猿儿18 小时前
【C#】画图控件的FormsPlot中的Refresh功能调用消耗时间不一致缘由
开发语言·c#
rit843249918 小时前
C# Socket 聊天室(含文件传输)
服务器·开发语言·c#
白衣衬衫 两袖清风1 天前
ABP框架+Dapper执行原生sql
sql·c#·.net
在路上看风景1 天前
1.15 并行编程
c#