【问题标题】:Unexpected error in windows serviceWindows 服务中的意外错误
【发布时间】:2017-01-31 10:28:04
【问题描述】:

我有一个 WCF Windows 服务正在运行。它基本上注册了许多客户端,并在需要时向它们广播消息。 通常它工作正常,但最近我在尝试注册新客户端时遇到错误,已经连接了大约 24 个客户端,但是当我尝试注册第 25 个时,我得到“索引超出了数组的范围。 "被退回。 如果我重新启动服务,所有客户端都会重新连接,并且新客户端能够注册。 调用 NotifyServer 方法向所有注册的客户端广播消息。这通过客户端字典创建发送消息的异步任务。这样做是为了向一个客户端发送消息时发生的任何问题都不会影响向其他客户端发送消息。 该服务设置为使用可靠连接。

    // List of connected Clients
    //
    private static Dictionary<string, IBroadcastorCallBack> clients = new Dictionary<string, IBroadcastorCallBack>();

    // lock indicator object 
    //
    private static object locker = new object();

    public string RegisterClient(string clientName)
    {
        string returnValue = "";

        if (!string.IsNullOrEmpty(clientName))
        {
            try
            {
                var callback = OperationContext.Current.GetCallbackChannel<IBroadcastorCallBack>();
                lock (locker)
                {
                    // Remove the old client if its already there
                    //
                    if (clients.Keys.Contains(clientName))
                    {
                        clients.Remove(clientName);
                    }

                    // Add the new one
                    //
                    clients.Add(clientName, callback);
                }
            }
            catch (Exception ex)
            {
                // Return any error message
                //
                returnValue = ex.Message;
            }
        }

        return returnValue;
    }

    /// <summary>
    /// Notify the service of a message that can be broadcast
    /// </summary>
    /// <param name="eventData">Message details</param>
    /// 
    public async void NotifyServer(EventDataType eventData)
    {

        DateTime Start = DateTime.Now;

        // Get a list copy of the dictionary  for all clients bar the one sending it. This is so we can't update the list inside the loop.
        //
        var clientlist = clients.Where(x => x.Key != eventData.ClientName).ToList();

        // Logging
        //
        if (MetricBroadcast.Properties.Settings.Default.LogXML)
        {
            log.Debug("XML Broadcast from " + eventData.ClientName + " to " + clientlist.Count.ToString() + " clients:\n" + eventData.EventMessage + "\n");
        }

        // Broadcast to all the valid clients
        //
        var BroadcastToClientList = clientlist.Select(client => BroadcastMessage(client.Value, client.Key, eventData)).ToList();

        // Wait until they are all done
        //
        await Task.WhenAll(BroadcastToClientList);

        // If we are logging and the broadcast time is > 1 second, we make a log entry
        //
        DateTime End = DateTime.Now;

        if (MetricBroadcast.Properties.Settings.Default.LogXML)
        {
            TimeSpan res = End - Start;

            if (res.TotalSeconds > 1)
            {
                var timetaken = string.Format("XML Broadcast Time : {0,2:00}:{1,2:00}.{2,3:000}", res.Minutes, res.Seconds, res.Milliseconds);

                log.Debug(timetaken);
            }
        }

    }

    private async Task<bool> BroadcastMessage(IBroadcastorCallBack clientCallback, string ClientKey, EventDataType eventData)
    {
        bool retval = true;

        Exception savedEx = null;

        DateTime BroadStart = DateTime.Now;

        try
        {
            // Send the message to the current client
            //
            clientCallback.BroadcastToClient(eventData);

        }
        catch (Exception e)
        {
            // If we can't access the current clients callback method, 
            // we remove them from the clients list, as they've probably lost their connection.
            //
            clients.Remove(ClientKey);

            savedEx = e;

            retval = false;
        }

        // Log any broadcast that took > .5 seconds
        //
        DateTime BroadEnd = DateTime.Now;

        if (MetricBroadcast.Properties.Settings.Default.LogXML)
        {
            TimeSpan res = BroadEnd - BroadStart;

            if (res.TotalSeconds > .5)
            {
                var timetaken = string.Format("Single XML Broadcast Time to " + ClientKey + " : {0,2:00}:{1,2:00}.{2,3:000}", res.Minutes, res.Seconds, res.Milliseconds);

                log.Debug(timetaken, savedEx);
            }
        }

        return retval;
    }

【问题讨论】:

  • 您在阅读时不会锁定客户端字典(仅在添加新客户端时),因此在多线程环境(如您的环境)中 - 任何事情都可能发生。
  • 啊!这将教我复制和粘贴旧的同步代码作为异步版本的基础......我会拿出来试一试。
  • 好的,我试过了,等了一会儿(好像是服务起来了一段时间才出现),又出现同样的问题。
  • 然后更新你的问题,你到底做了什么。
  • 我从 RegisterClient 代码中删除了 locker 对象和 lock 语句。

标签: c# wcf windows-services


【解决方案1】:

问题很可能是在多线程环境中不正确使用静态clients Dictionary。新客户可以随时注册,包括在您的BroadcastMessageNotifyServer 函数中间。 Dictionary 不是为多线程访问而设计的。以这个为例:

clients.Where(x => x.Key != eventData.ClientName).ToList();

如果在此枚举过程中删除客户端会怎样?谁知道,因为它不是为此而设计的,但很可能会抛出一些异常(比如你的“索引越界”)。这就是为什么您需要为写入和读取锁定字典,而不仅仅是写入:

List<KeyValuePair<string, IBroadcastorCallback>> clientList;
lock (locker) 
    clientList = clients.Where(x => x.Key != eventData.ClientName).ToList();

另一种选择是使用ConcurrentDictionary 类。它专为并发访问而设计,您可以从多个线程安全地对其进行读写。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-11-23
    • 1970-01-01
    • 2015-08-11
    • 1970-01-01
    • 2020-02-20
    • 2011-07-17
    相关资源
    最近更新 更多