问题 是否线程安全检查?


我有一些代码,在新线程上抛出异常,我需要在主线程上确认和处理。为了实现这一点,我通过使用保存抛出异常的字段来共享线程之间的状态。

我的问题是 检查null时是否需要使用锁 正如我在下面的代码示例中所做的那样?

public class MyClass
{
    readonly object _exceptionLock = new object();
    Exception _exception;

    public MyClass()
    {
        Task.Run(() =>
        {
            while (CheckIsExceptionNull())
            {
                // This conditional will return true if 'something has gone wrong'.
                if(CheckIfMyCodeHasGoneWrong())
                {
                    lock(_exceptionLock)
                    {
                        _exception = new GoneWrongException();
                    }
                }
            }
        });
    }

    bool CheckIsExceptionNull() // Is this method actually necessary?
    {
        lock (_exceptionLock)
        {
            return _exception == null;
        }
    }

    // This method gets fired periodically on the Main Thread.
    void RethrowExceptionsOnMainThread()
    {
        if (!CheckIsExceptionNull())
        {
            lock (_exceptionLock)
            {
                throw _exception; // Does this throw need to be in a lock?
            }
        }
    }
}

另外, 在主线程上抛出异常时是否需要使用锁?


6694
2018-06-15 10:56


起源

任何时候问题是“我是否需要使用锁”,这是可疑的。无争议的锁定需要十几纳秒。你的问题是“我的程序已经保证是正确的,但它是十几纳秒太慢,这个性能问题可归因于锁定,我知道它在市场上不会成功,所以我可以删除这个锁定并保留了我程序的正确性?“你的代码开始时甚至都不正确,我非常怀疑在无争议的锁中花费的纳秒将导致你的程序被认为太慢。 - Eric Lippert
一个更好的问题完全是“如何通过使用任务并行库来表达异步执行任务的成功和失败的概念,从而消除我对显式线程管理和共享内存的使用?”。 Task对象已经具有以下属性:在执行任务期间抛出的异常可以安全地缓存并重新抛出在管理任务的执行上下文上。 - Eric Lippert


答案:


首先要注意的是你的代码不是线程安全的,因为你有一个线程竞赛:你检查 CheckIsExceptionNull 在你投掷的不同锁定区域,但是: 测试和抛出之间的值可以改变

不保证字段访问是线程安全的。特别是,虽然参考 不能  被撕裂(保证很多 - 读取和写入引用是原子的),它是  保证不同的线程会看到最新的值,因为CPU缓存等。实际上不太可能咬你,但这是一般情况下线程问题的问题; p

就个人而言,我可能只是让这个领域变得不稳定,并使用本地。例如:

var tmp = _exception;
if(tmp != null) throw tmp;

以上没有线程竞赛。添加:

volatile Exception _exception;

确保该值不会缓存在寄存器中(尽管这在技术上是一种副作用,而不是预期/记录的效果 volatile


9
2018-06-15 11:04



“坦率地说,我不鼓励你做一个动荡的领域”,Eric Lippert :(stackoverflow.com/a/17530556/7122)。请不要鼓励使用它。 - David Arno
@DavidArno我不确定是否添加 lock, Thread.VolatileRead 或者一些 Interlocked 但是会提高可读性 - Marc Gravell♦
@DavidArno如果你知道自己在做什么,使用volatile没有任何问题,认为这对于那些在SO上提出线程问题的人来说并不常见。 - Yuval Itzchakov
@MarcGravell毫无疑问,这条路可能会受到质疑,尤其是当你使用的时候 副作用,并肯定使用 lock 能够大大避免他在路上犯错误,但是,这并不意味着它不是一个有效的答案。 - Yuval Itzchakov
我其实这么认为 Volatile.Read() 比制作​​变量更好 volatile,因为它更有针对性(你只在你需要它的地方做)并且更明显的是正在进行易失性读取。 (注意 Volatile.Read() 是不一样的 Thread.VolatileRead() 哪一个 不应该使用。) - Matthew Watson


关于这样的问题,可能会有很多有趣且有用的信息。您可以花几个小时阅读有关它的博客文章和书籍。即使是真正了解它的人也不同意。 (正如你在Marc Gravell和Eric Lippert的帖子中看到的那样)。

所以对我来说这个问题没有答案。只是一个建议: 始终使用锁。 只要多个线程访问一个字段。没有风险,没有猜测,没有讨论编译器和CPU技术。只需使用锁并进行保存,不仅可以在机器上的大多数情况下使用,而且可以在每台机器上使用。

进一步阅读:


0
2018-06-15 13:55



只是暂时“永远使用锁” - 我发现自己越来越多地使用 Interlocked 过度 lock  - 但我倾向于做一些相当有争议的系统。不过我的意思是: Interlocked 也是“没有风险,没有猜测,没有CPU技术”等 - Marc Gravell♦
应该是。我没有看到Interlocked实际上有用的很多情况,因为它非常有限。我想有一些区域适合完美。 - Stefan Steinegger