检查 null 值是否线程安全?

14

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

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


```java synchronized (lock) { if (thrownException != null) { // handle exception on main thread } } ```
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?
            }
        }
    }
}

此外,在主线程上抛出异常时,我需要使用锁吗?


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

10

需要注意的第一点是,你的代码不是线程安全的,因为你有一个线程竞争:你在不同的锁定区域中检查CheckIsExceptionNull与你抛出异常的地方,但是:在测试和抛出异常之间,这个值可能会发生变化

字段访问不能保证是线程安全的。特别是,虽然引用不能被撕裂(这是有保证的-引用的读写是原子性的),但由于CPU缓存等原因,不能保证不同的线程将看到最新的值。实际上这很不可能会影响到你的程序,但这就是通常情况下处理线程问题的问题;p

个人而言,我可能会让这个字段变成 volatile 并使用本地变量。例如:

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

以上代码没有线程竞争。添加:

volatile Exception _exception;

确保该值不被缓存在寄存器中(虽然这在技术上是副作用,而非 volatile 的预期/记录效果)


1
坦白地说,我不鼓励您使用volatile字段。Eric Lippert曾表示过:“请不要鼓励它的使用。”(https://dev59.com/HHVD5IYBdhLWcg3wJIEK#17530556) - David Arno
@DavidArno 我不确定添加 lockThread.VolatileRead 或一些 Interlocked 是否会提高可读性,但是... - Marc Gravell
@DavidArno 如果你知道你在做什么,使用 volatile 没有问题,但在 SO 上询问线程问题的人中并不常见。 - Yuval Itzchakov
1
@MarcGravell 毫无疑问,走这条路可能是有问题的,特别是当你使用副作用时,而且使用lock肯定会大大避免他在未来犯错误,但这并不意味着它不是一个有效的答案。 - Yuval Itzchakov
3
我认为使用Volatile.Read()比将变量设置为volatile更好,因为它的目标更清晰(只在需要时使用),并且更明显地表明正在进行volatile读取。(请注意,Volatile.Read()Thread.VolatileRead()不同,后者不应该被使用。) - Matthew Watson
显示剩余5条评论

1

关于这样的问题可能有很多有趣和有用的信息。您可以花几个小时阅读有关此类问题的博客文章和书籍。即使是最了解它的人也存在分歧(正如您在Marc Gravell和Eric Lippert的帖子中所看到的)。

所以对我来说,这个问题没有一个答案。只是一个建议:始终使用锁。一旦有多个线程访问字段时。没有风险,不需要猜测,也不需要讨论编译器和CPU技术。只需使用锁并确保在每台机器上都能使其工作,并且不仅适用于大多数情况下的您的计算机,而且适用于每一种情况。

进一步阅读:


关于“始终使用锁”的一点小建议 - 我发现自己越来越多地使用Interlocked而不是lock,但我倾向于处理一些相当有争议的系统。我的观点是:Interlocked也是“无风险、无猜测、无CPU技术”等等。 - Marc Gravell
是的,可能吧。我没有看到很多情况下使用Interlocked是真正有用的,因为它非常受限制。我猜肯定有一些领域它能够完美地适用。 - Stefan Steinegger

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接