You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

多线程无保护读写对象的安全性、风险及异常捕获问题

线程安全问题分析与解答

问题描述

我了解到从不同线程访问同一对象并非线程安全,应加以保护,可通过Locking、Interlocked.Exchange、不可变对象(Immutables)或其他方式实现。这意味着以下代码因未对共享的test对象的访问加以保护,可能存在线程安全问题。我的问题如下:

  • 以下代码是否安全?
  • 若不安全,可能出现的最严重后果是什么?
  • 若不安全,脏读或脏写是否会抛出可捕获的异常以规避最坏情况?

待分析代码

class Test
{
    public Test()
    {
        Foo = new Random().Next(10000);
    }

    public int Foo;
}

internal class Program
{
    public static async Task Main(string[] args)
    {
        var test = new Test();

        var exitToken = new CancellationTokenSource(TimeSpan.FromSeconds(120)).Token;

        var readerTask = Task.Run(async () =>
        {
            while (!exitToken.IsCancellationRequested)
            {
                Console.WriteLine("Random Foo: " + test.Foo);

                await Task.Delay(TimeSpan.FromSeconds(5));
            }
        });

        var writerTask = Task.Run(async () =>
        {
            while (!exitToken.IsCancellationRequested)
            {
                test = new Test();

                await Task.Delay(TimeSpan.FromSeconds(5));
            }
        });

        await Task.WhenAll(readerTask, writerTask);
    }
}

问题解答

1. 代码是否安全?

不安全。核心问题在于共享的引用变量test的读写没有同步机制:

  • writer线程直接对test赋值,reader线程直接读取test.Foo;
  • 受CPU缓存和JIT编译器优化影响,reader线程可能无法及时感知到writer线程更新后的test引用,存在可见性问题;
  • 虽然int类型读写是原子操作,但引用变量的赋值缺乏内存屏障,存在指令重排序风险。

2. 最严重的后果是什么?

最严重的后果是reader线程长期读取过期的test引用:

  • 极端情况下(比如JIT将test缓存到寄存器),reader线程可能永远看不到writer更新后的对象,持续输出旧的Foo值;
  • 即使偶尔能看到更新,也会出现输出结果与预期不符的情况;
  • 当前Test构造仅初始化一个int,不会出现半初始化对象问题,但如果Test包含复杂成员(如引用类型),可能读取到未完全构造的对象,引发更诡异的行为。

3. 脏读/脏写是否会抛出可捕获的异常?

不会抛出任何可捕获的异常。线程安全问题导致的脏读、可见性问题大多是静默的逻辑错误,程序不会崩溃也不会触发异常,但输出结果不符合预期。这类问题极难排查,因为其出现依赖线程调度、CPU缓存状态等不确定因素,无法稳定重现。

修复方案

可以通过以下几种方式修复线程安全问题:

方案1:使用Interlocked类操作引用

class Test
{
    public Test()
    {
        Foo = new Random().Next(10000);
    }

    public int Foo;
}

internal class Program
{
    private static Test _test = new Test();

    public static async Task Main(string[] args)
    {
        var exitToken = new CancellationTokenSource(TimeSpan.FromSeconds(120)).Token;

        var readerTask = Task.Run(async () =>
        {
            while (!exitToken.IsCancellationRequested)
            {
                // 安全读取当前引用
                var currentTest = Interlocked.CompareExchange(ref _test, null, null);
                Console.WriteLine("Random Foo: " + currentTest.Foo);

                await Task.Delay(TimeSpan.FromSeconds(5));
            }
        });

        var writerTask = Task.Run(async () =>
        {
            while (!exitToken.IsCancellationRequested)
            {
                // 安全更新引用
                Interlocked.Exchange(ref _test, new Test());

                await Task.Delay(TimeSpan.FromSeconds(5));
            }
        });

        await Task.WhenAll(readerTask, writerTask);
    }
}

方案2:使用lock同步读写操作

class Test
{
    public Test()
    {
        Foo = new Random().Next(10000);
    }

    public int Foo;
}

internal class Program
{
    private static Test _test = new Test();
    private static readonly object _lockObj = new object();

    public static async Task Main(string[] args)
    {
        var exitToken = new CancellationTokenSource(TimeSpan.FromSeconds(120)).Token;

        var readerTask = Task.Run(async () =>
        {
            while (!exitToken.IsCancellationRequested)
            {
                lock (_lockObj)
                {
                    Console.WriteLine("Random Foo: " + _test.Foo);
                }

                await Task.Delay(TimeSpan.FromSeconds(5));
            }
        });

        var writerTask = Task.Run(async () =>
        {
            while (!exitToken.IsCancellationRequested)
            {
                lock (_lockObj)
                {
                    _test = new Test();
                }

                await Task.Delay(TimeSpan.FromSeconds(5));
            }
        });

        await Task.WhenAll(readerTask, writerTask);
    }
}

方案3:使用不可变对象(Immutables)

由于Test类的Foo字段构造后不会修改,可将其声明为只读字段,确保对象不可变。此时即使引用更新无同步,reader至多读取到旧的不可变对象,不会出现不一致状态:

class Test
{
    public Test()
    {
        Foo = new Random().Next(10000);
    }

    public readonly int Foo; // 声明为只读
}

内容的提问来源于stack exchange,提问作者rudimenter

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.09 11:25:16