这种方法对共享的字典是线程安全的吗?

时间:2015-12-06 23:02:28

标签: c# multithreading wcf thread-safety

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

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

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

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

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

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

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

    < / LI>

    这是一个类的片段(注意:此代码已经过简化以说明问题)

    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;
        }
    }
    

2 个答案:

答案 0 :(得分:1)

  

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

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

  

在ChatServer.GetNewMessages()中将String列表返回给调用者是否是线程安全的?

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

  

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

不 - 您GetNewMessages中的代码无需更改。 但是ChatMessage必须对线程安全做出自己的保证。实际上,如果GetNewMessages本身不是线程安全的,那么编码ChatMessage就不可能提供线程安全性(当然,不能克隆对象)。

<强> 建议

  1. 您的代码目前已被破解:_servers_lock未初始化 - 请在您的帖子中进行修改,以便更轻松地复制和删除粘贴

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

  3. ChatServer课程中可能需要或可能不需要锁定。正如现在所写,它没有线程安全问题(也就是说,lock是正确的)。但是,根据课程的使用方式,您可能需要查看ReaderWriterLockConcurrentBag<T>

  4. 您的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();
        }
    }
    

答案 1 :(得分:1)

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

  

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

如果它是只读的,您不需要同步Dictionary的访问权限。但是,如果它是只读的,请使用ReadOnlyDictionary类在代码中将其显式化。

  

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

它是线程安全的,但最好使用专门为您执行此操作的数据结构。例如ConcurrentDictionary。线程安全的数据结构通常效率更高,因为大多数数据结构使用Interlocked进行线程同步。

你的字典是只读的,所以它甚至不需要同步。

  

在ChatServer.GetNewMessages()中将String列表返回给调用者是否是线程安全的?

它是每个电话的全新列表,它是线程安全的。但是你为什么要回归List<string>?客户是否应该能够修改返回的集合(AddRemoveClear)?选择客户端所需的最小界面,例如IEnumerable<string>IReadOnlyList<string>,然后使用List<T>.AsReadOnly()将其返回。您仍然需要列表的副本,AsReadOnly()只在列表周围创建一个包装器,而不是只读副本。

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

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

  

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

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