【问题标题】:Are `System.MulticastDelegate`'s thread-safe?`System.MulticastDelegate` 的线程安全吗?
【发布时间】:2018-07-18 21:13:11
【问题描述】:

我正在寻找可能对此了解更多的人,我的直觉告诉我答案是“不,它不是线程安全的”,但我想确定。

为了说明我的问题,我提供了这个类的一些上下文

public class MyContext
{
    private readonly object _lock = new object();
    public delegate bool MyDelegate(MyContext context);
    private MyDelegate _multicastDelegate;

    public MyContext()
    {
        _multicastDelegate = null;
    }

    public void AddDelegate(MyDelegate del)
    {
        lock(_lock)
        {
            _multicastDelegate += del;
        }
    }

    public void RemoveDelegate(MyDelegate del)
    {
        lock(_lock)
        {
            _multicastDelegate += del;
        }
    }

    public void Go()
    {
        _multicastDelegate.Invoke(this);
    }
}

编辑:我在上面的示例中添加了锁,但这真的不是我的问题的重点。


我试图更好地理解保存调用列表的数组是否是线程安全的。坦率地说,我并不清楚这一切是如何组合在一起的,我们将不胜感激。

根据我找到的文档,唯一没有提供真正见解的引用如下:

MulticastDelegate 有一个委托的链接列表,称为调用列表,由一个或多个元素组成。调用多播委托时,调用列表中的委托按照它们出现的顺序被同步调用。如果在执行列表期间发生错误,则会引发异常。

https://msdn.microsoft.com/en-us/library/system.multicastdelegate.aspx

提前致谢。

【问题讨论】:

  • 您需要在此上下文中定义“线程安全”才能使问题有意义。请注意,使用 +=-= 不会修改现有的委托实例 - 它返回一个 new 实例。您显示的代码不是线程安全的,因为如果两个线程同时调用(比如说)AddDelegate,您很容易最终会发现其中一个被“错过”。这就是自动生成的类字段事件访问器做更多工作的原因。
  • 我在整理代码示例时可能有点太匆忙了。鉴于您的评论,我的问题是关于添加/删除调用列表。
  • @JonSkeet -- 我做了一些编辑,但我认为我的措辞需要帮助......我假设调用列表本质上是一个数组。我想知道该数组是否可以线程安全地添加/删除项目。
  • 委托对象是不可变的。从语法糖很难看出,但委托目标列表永远不会被修改。只有构造函数会创建该列表。因此,当您调用 Go() 而同时另一个线程忙于调用 Add 或 Remove 时,什么都不会发生。除了未调用添加的目标方法或仍调用已删除的目标。这通常是一个线程竞争错误,不值得“线程安全”这个绰号,但它不会让客户端程序员感到惊讶。嗯,不应该。
  • @HansPassant 我认为你在谈到 Jon 在他的评论中所做的同一个问题,那就是“线程安全”是一个相当没有意义的术语,因为它在不同的上下文中意味着不同的东西。

标签: c# multithreading thread-safety multicastdelegate


【解决方案1】:

Delegates不可变的。您永远不会更改代表。任何看似改变委托的方法实际上都是在创建一个新实例。

委托是不可变的;一旦创建,委托的调用列表就不会改变。

因此无需担心在调用委托时调用列表可能会更新。

然而,当委托实际上可能是 null 时,您必须防范并且在您的方法中未能做到这一点。

(new MyContext()).Go();

会导致异常。您过去必须通过将值读取到局部变量中来防止这种情况,测试它是否为空,然后调用它。它现在可以更容易地解决为:

public void Go()
{
    _multicastDelegate?.Invoke(this);
}

【讨论】:

  • 谢谢。我已经有一段时间了,我很难把措辞放在一起,但赋值运算符应该是我的“重要提示”。 --- 再次感谢您的回答让我们更加清楚。
  • 噢……是的。空异常。再次感谢。
【解决方案2】:

MSDN 文档使用的线程安全的定义是指正确同步的代码。它通常不会说明它同步的内容,但它可以是静态成员的类对象,实例成员的实例对象,或者它可以是一些内部对象,例如许多集合类型中的SyncRoot

虽然委托是不可变的,但您仍然必须正确同步。 .NET 和 C# 与 Java 不同,不保证安全发布,因此如果不保证同步,则可以在其他线程中观察部分初始化的对象1

为了让你的代码线程安全,你只需要在从委托字段读取时使用_lock,但你可以在锁外调用Invoke,让委托负责保持自己的线程安全。

public class MyContext
{
    private readonly object _lock = new object();
    public delegate bool MyDelegate(MyContext context);
    private MyDelegate _delegate;

    public MyContext()
    {
    }

    public void AddDelegate(MyDelegate del)
    {
        lock (_lock)
        {
            _delegate += del;
        }
    }

    public void RemoveDelegate(MyDelegate del)
    {
        lock (_lock)
        {
            // You had a bug here, +=
            _delegate -= del;
        }
    }

    public void Go()
    {
        MyDelegate currentDelegate;
        lock (_lock)
        {
            currentDelegate = _delegate;
        }
        currentDelegate?.Invoke(this);
    }
}

  1. Microsoft 的 .NET Framework 实现总是进行 volatile 写入(或者他们这么说),这隐含地为您提供了安全的发布,但我个人并不依赖于此。

【讨论】:

    猜你喜欢
    • 2020-04-15
    • 2011-07-04
    • 2014-04-26
    • 2012-11-30
    • 2010-12-30
    • 2013-03-12
    • 2021-08-03
    • 2010-12-27
    • 2018-06-04
    相关资源
    最近更新 更多