【问题标题】:Is this approach to a shared Dictionary thread-safe?这种共享字典的方法是线程安全的吗?
【发布时间】:2015-12-06 23:02:27
【问题描述】:

我正在通过 ASP.NET 构建一个基本的基于 Web 的聊天,它轮询托管在 Windows 服务中的 WCF 服务。我对在多线程应用程序中管理共享资源还很陌生,所以我对我的方法中的线程安全有几个问题。

服务核心是一个名为 ServerManager 的 Singleton 类,它维护一个 ChatServer 对象字典,该字典仅被初始化和填充一次,并且永远不会再次添加或删除。来自 WCF 服务的调用将引用客户端“连接”到的实例的密钥 (serverID)。在任何给定时间,可能会有多个调用进入服务,多个线程同时调用 ServerManager 的方法。

对 Dictionary _servers 的访问与锁定块同步,同时获得对请求的 ChatServer 对象的引用,然后在锁定块之外使用本地引用。我不希望所有的 ChatServer 都被一个 ChatServer 的读/写阻塞。

在读取/写入 _messages 对象期间,我还在 ChatServer 实例中包含了同步。

  1. 在获取对所请求对象的引用时同步对 _servers 的访问是否是线程安全的,然后在锁定块之外使用该引用 (currentServer)?换句话说,只要访问这些对象中的共享数据是同步的,多个线程是否可以安全地访问 Dictionary 中的不同元素?

  2. 像在 ChatServer.GetNewMessages() 中那样将字符串列表返回给调用者是否是线程安全的?

  3. 如果 newMessages 是一些新 ChatMessage 对象的列表,我需要做些什么来确保 GetNewMessages 在返回 ChatMessage 列表时不会导致线程安全问题?

这是一个类的sn-p(注意:这个代码已经被简化以说明问题)

class ServerManager
{
    private static readonly ServerManager _instance = new ServerManager();
    private Dictionary<Int32, ChatServer> _servers = new Dictionary<Int32, ChatServer>();
    private Object _lock = new Object();

    private ServerManager()
    {
        _servers.Add(1, new ChatServer());
        _servers.Add(2, new ChatServer());
        _servers.Add(3, new ChatServer());
    }

    public static ServerManager Instance
    {
        get { return _instance; }
    }

    public void AddMessage(Int32 serverID, String message)
    {
        ChatServer currentServer;

        lock (_lock)
        {
            currentServer = _servers[serverID];
        }

        currentServer.AddMessage(message);
    }

    public List<String> GetNewMessages(Int32 serverID)
    {
        ChatServer currentServer;

        lock (_lock)
        {
            currentServer = _servers[serverID];
        }

        return currentServer.GetNewMessages();
    }
}

class ChatServer
{
    private List<String> _messages = new List<string>();
    private Object _lock = new Object();

    public ChatServer() { }

    public void AddMessage(String message)
    {
        lock (_lock)
        {
            _messages.Add(message);
        }
    }

    public List<String> GetNewMessages()
    {
        List<String> newMessages = new List<String>();

        lock (_lock)
        {
            newMessages.AddRange(_messages);
        }

        return newMessages;
    }
}

【问题讨论】:

  • 不确定是否需要锁定字典以查找条目,如果您只是在初始化期间更改它,并且从不添加/删除条目。
  • 看看ConcurrentDictionary,它将大大简化您的代码,并且您不需要所有这些锁。
  • 你在哪里设置_lock = new object(); ?
  • 我打算在声明中初始化 _lock,但是在进行代码编辑以将其发布到此处时,我在某处丢失了它。
  • 可能要考虑ImmutableDictionary&lt;&gt;

标签: c# multithreading wcf thread-safety


【解决方案1】:

在获取 _servers 时同步访问是否是线程安全的? 引用所请求的对象,然后利用该引用 (currentServer) 在锁块之外?换句话说,可以 多个线程安全地访问不同的元素 字典只要访问这些对象内的共享数据 同步?

是的,它是线程安全的,假设元素本身是线程安全的。在这种情况下,您正在处理 ChatServer 对象,它们本身必须保证线程安全

像在 ChatServer.GetNewMessages() 中那样将字符串列表返回给调用者是否是线程安全的?

是的 - 您正在创建一个 local 变量,然后返回该变量。线程之间共享的所有资源都在锁定范围内(在您的示例中仅共享 _messages)。

如果 newMessages 是一些新 ChatMessage 对象的列表,那么 我需要做些什么来确保 GetNewMessages 不会导致 返回 ChatMessage 列表时的线程安全问题?

不 - 您在 GetNewMessages 中的代码不必更改。 然而ChatMessage 必须自己保证线程安全。事实上,如果 ChatMessage 本身不是线程安全的(当然,没有克隆对象),那么编写 GetNewMessages 以提供线程安全是不可能的。

建议

  1. 您的代码目前已损坏:_servers_lock 未初始化 - 请在您的帖子中修复它,以便更轻松地复制和粘贴

  2. 您的ServerManager 中不需要任何 锁 - 假设_servers 从不 添加或删除。

  3. ChatServer 类中可能需要也可能不需要锁。正如现在所写的那样,它不存在线程安全问题(也就是说,您的locks 是正确的)。但是,根据类的使用方式,您可能需要查看 ReaderWriterLockConcurrentBag&lt;T&gt;

你的ServerManager 类可以写成没有锁,并且是线程安全的:

class ServerManager
{
    private static readonly ServerManager _instance = new ServerManager();
    private readonly Dictionary<Int32, ChatServer> _servers;

    private ServerManager()
    {
        _servers = new Dictionary<int, ChatServer>();
        _servers.Add(1, new ChatServer());
        _servers.Add(2, new ChatServer());
        _servers.Add(3, new ChatServer());
    }

    public static ServerManager Instance
    {
        get { return _instance; }
    }

    public void AddMessage(Int32 serverID, String message)
    {
        _servers[serverID].AddMessage(message);
    }

    public List<String> GetNewMessages(Int32 serverID)
    {
        return _servers[serverID].GetNewMessages();
    }
}

【讨论】:

    【解决方案2】:

    这个相当长的答案的简短摘要:您的代码是线程安全的,但可以改进。

    ChatServer 对象的字典,它只被初始化和填充一次,并且永远不会再次添加或删除。

    如果Dictionary 是只读的,则无需同步对它的访问。但是,如果它是只读的,请在您的代码中使用 ReadOnlyDictionary 类使其显式化。

    在获取对请求对象的引用时同步访问 _servers 是否是线程安全的,然后在锁定块之外使用该引用 (currentServer)?换句话说,只要访问这些对象中的共享数据是同步的,多个线程是否可以安全地访问 Dictionary 中的不同元素?

    它是线程安全的,但最好使用专门的数据结构来为您做到这一点。例如ConcurrentDictionary。线程安全的数据结构通常效率更高,因为它们中的大多数使用Interlocked 进行线程同步。

    您的字典是只读的,因此甚至不需要同步。

    像在 ChatServer.GetNewMessages() 中那样将字符串列表返回给调用者是否是线程安全的?

    每次调用都是一个全新的列表,它是线程安全的。但是为什么要返回List&lt;string&gt;?客户端是否可以修改返回的集合(AddRemoveClear)?选择客户端需要的最小接口,如IEnumerable&lt;string&gt;IReadOnlyList&lt;string&gt; 并使用List&lt;T&gt;.AsReadOnly() 将其返回。您仍然需要列表的副本,AsReadOnly() 仅在列表周围创建一个包装器,而不是只读副本。

    更好的选择是将消息存储在线程安全的集合中,例如ConcurrentQueue&lt;T&gt;。您可以直接在客户端中使用队列,或者每次使用 ConcurrentQueue&lt;T&gt;.ToArray() 调用 GetNewMessages 时返回一个快照。

    在这里,您可以看到让您的方法返回接口的好处。第一个选项实际上返回一个ReadOnlyCollection&lt;string&gt;,第二个是string[],但是由于这两种类型都实现了IEnumerable&lt;string&gt;IReadOnlyList&lt;string&gt;,因此客户端不受您选择的影响。这是一个您可以随时更改的实现细节。

    如果 newMessages 是一些新 ChatMessage 对象的列表,我需要做些什么来确保 GetNewMessages 在返回 ChatMessage 列表时不会导致线程安全问题?

    使ChatMessage 不可变。不可变类型始终是线程安全的。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-08-02
      • 1970-01-01
      • 1970-01-01
      • 2017-04-29
      • 1970-01-01
      相关资源
      最近更新 更多