【问题标题】:Concurrency problems. Locks not released并发问题。未释放的锁
【发布时间】:2014-01-11 00:36:36
【问题描述】:

您好,我在处理我的项目时有些头疼。
简短总结:

  • 客户端/服务器应用程序(我在服务器端)
  • 多线程
  • 使用 System.Timers.Timer 每秒保持活动状态
  • 单独线程上的主网络循环(从客户端读取/写入数据包)
  • 单独线程上的服务器(此时无关紧要)

我有一个处理所有客户端的 ClientHandler 类。 (Networker 是主循环) ClientList 是这样实现的:

public List<Client> ClientList { get; private set; }

每次我尝试访问 ClientList(读/写)时,我都会使用...

lock(ClientList){}
lock(ClientHandler.ClientList){}

... 取决于我在 ClientHandler 内部还是外部。

到目前为止,我没有使用任何锁,因此存在一些并发问题。
但是现在当我使用/误用锁时,我遇到了一些保活问题。
如果客户端连接:

public bool AddClient(Client client)
{
    lock (ClientList)
    {
        if (client == null || ClientList.Contains(client))
            return false;

        ClientList.Add(client);
        return true;
    }
}

而且我的计时器每一秒都会加入一个keepalive:

private void KeepAliveTimer_Elapsed(object sender, ElapsedEventArgs e)
{
    KeepAliveTimer.Stop();
    lock (ClientList)
    {
        if (ClientList.Count > 0)
        {
            foreach (Client client in ClientList)
            {
                lock (client)
                {
                    client.PacketQueue.Enqueue(new KeepAlivePacket());
                }
            }
        }
    }
    KeepAliveTimer.Start();
}

还有我当前的主循环:

private void Networker()
{
    while (IsRunning)
    {
        lock (ClientHandler.ClientList)
        {
            if (ClientHandler.ClientList.Count > 0)
            {
                foreach (Client client in ClientHandler.ClientList)
                {
                    // Check if client has data to read.
                    // Read for up to 10 msecs.
                    if (client.DataAvailable)
                    {
                        DateTime expiry = DateTime.Now.AddMilliseconds(10);
                        while (DateTime.Now <= expiry)
                        {
                            int id = client.ReadByte();

                            if (id == -1 || !PacketHandler.HandlePacket((byte)id, client, this))
                            {
                                ClientHandler.DisconnectClient(client);
                                continue;
                            }
                        }
                    }


                    // Check if client has data to write.
                    // Write for up to 10 msecs.
                    if (client.PacketQueue.Count > 0)
                    {
                        DateTime expiry = DateTime.Now.AddMilliseconds(10);
                        while (DateTime.Now <= expiry && client.PacketQueue.Count > 0)
                        {
                            IPacket packet = client.PacketQueue.Dequeue();
                            if (!packet.Write(client))
                            {
                                ClientHandler.DisconnectClient(client);
                                continue;
                            }
                        }
                    }

                }
            }
        }

        Thread.Sleep(1);
    }
}

在所有这些锁定之前,我的测试客户端每秒都会收到一个 KeepAlivePacket。
现在我只得到它一次,因为在第一个 KeepAlivePacket KeepAliveTimer_Elapsed 之后无法再访问锁,因为它被其他线程永久锁定(用一些调试输出对其进行了测试)。

提供的代码中是否有某些内容可能是疯子,或者我做错了什么?

如果有人能让我摆脱这种痛苦,那就太好了。

编辑(感谢 Joachim Isaksson):
我不知道这是否是唯一的错误,但我忘记的一件事是在我读取第一个数据包后检查主循环是否有可用数据。
这是第一个问题,因为我只用我的 TestClient 发送了一个数据包,而服务器卡在了 client.ReadByte,因为事先没有检查。

if (client.DataAvailable)
{
    DateTime expiry = DateTime.Now.AddMilliseconds(10);
    while (DateTime.Now <= expiry && client.DataAvailable)
    {
        try
        {
            int id = client.ReadByte();
            // do stuff...
        }...
    }
}

【问题讨论】:

  • 与论坛网站不同,我们不使用“谢谢”、“感谢任何帮助”或Stack Overflow 上的签名。请参阅“Should 'Hi', 'thanks,' taglines, and salutations be removed from posts?.
  • 好的,谢谢,记住这一点:)
  • 不确定您呼叫的是哪个ReadByte,但我怀疑它会等待数据可用。如果没有,它将锁定直到有,并且永远不会释放锁定。
  • 我会检查一下。它是 NetworkStream.ReadByte 的包装器。我这样做是为了在后台进行异常处理。我只在主循环中检查返回值。

标签: c# multithreading locking


【解决方案1】:

为什么不使用 System.collections.concurrent 中的集合而不是自己锁定?

【讨论】:

  • 那将是一个替代方案,但我想知道我做错了什么来学习所有这些东西。我不喜欢总是使用高级课程来避免自己学习如何做的理论。
【解决方案2】:

封装您的资源并为每个共享资源使用单独的锁定对象。锁定集合实例(共享资源)或其所有者实例被认为是不好的做法(!)。

然后,在拥有私有集合实例的对象上添加所有必要的方法来操作您的集合。

readonly List<MyItemClass> _myItems = new List<MyItemClass>();
readonly object lockObject = new object();

public IEnumerable<MyItemClass> MyItems
{
    get
    {
         lock(lockObject)
         {
              return _myItems.ToArray();
         }
    }
}

public void Add(MyItemClass item)
{
    lock(lockObject)
    {
        _myItems.Add(item);
    }
}

public bool Remove(MyItemClass item)
{
    lock(lockObject)
    {
        return _myItems.Remove(item);
    }
}

这为我省去了很多挫折。

题外话,但有些相关:如果您的集合是 ObservableCollection,您可以调用静态方法

BindingOperations.EnableCollectionSynchronization(_myItems, lockObject)

在您的所有者实例构造函数中。 这将确保即使是 WPF 的绑定机制也可以枚举集合,即使它在另一个线程上更新。它在枚举列表时使用与 Add 和 Remove 方法相同的锁定对象(重绘 itemlist 控件等)。静态方法是 .NET 4.5 的新方法,无需将可观察的集合更改从 ViewModel 分发到 UI 线程。

【讨论】:

    猜你喜欢
    • 2012-03-25
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-02-05
    • 2019-10-18
    • 1970-01-01
    • 1970-01-01
    • 2018-09-11
    相关资源
    最近更新 更多