多线程是否适合我的案例?

时间:2010-05-21 19:32:23

标签: .net multithreading

我目前正在设计一个多客户端/服务器应用程序。我使用简单的旧插座,因为WCF或类似的技术不是我需要的。让我解释一下:客户只是简单地调用服务不是经典案例;所有客户端都可以通过向服务器发送数据包来相互“交互”,然后服务器将执行某些操作,并可能向一个或多个客户端重新发送应答消息。虽然可以使用WCF,但是应用程序会因数百条不同的消息而变得非常复杂。

对于每个连接的客户端,我当然使用异步方法来发送和接收字节。我的消息完全正常,一切都很好。除了我正在编写的每行代码之外,我的头因为多线程问题而烧伤。由于可能有大约200个客户端同时连接,我选择采用完全多线程的方式:套接字上的每个接收消息都会立即在接收到的线程池线程上处理,而不是在单个消费者线程上处理。

由于每个客户端都可以与其他客户端交互,并间接与服务器上的共享对象交互,因此我必须保护几乎所有可变的对象。我首先使用ReaderWriterLockSlim来获取每个必须受到保护的资源,但很快就注意到整体写入比服务器应用程序中的读取更多,并切换到众所周知的Monitor以简化代码

到目前为止,这么好。每个资源都受到保护,我有一个帮助类,我必须使用它来获取一个锁及其受保护的资源,所以我不能在没有锁定的情况下使用一个对象。此外,每个客户端都有自己的锁,一旦从其套接字接收到数据包就会输入该锁。这样做是为了防止其他客户端在处理某些消息时更改此客户端的状态,这是经常发生的事情。

现在,我不仅需要保护资源免受并发访问的影响。我必须让每个客户端与服务器保持同步以获得我的一些收藏。我目前正在努力解决的一个棘手问题是:

  • 我有一组客户。每个客户都有自己的唯一ID。
  • 当客户端连接时,它必须接收每个连接的客户端的ID,并且必须通知每个客户端的新用户ID。
  • 当客户端断开连接时,每个其他客户端必须知道它,以使其ID不再对它们有效。
  • 每个客户端在给定时间必须始终拥有与服务器相同的客户端集合,以便我可以假设每个人都了解每个人。这样,如果我向客户#1发送消息“客户#2做了一些事情”,我知道它将永远被正确解释:客户端1永远不会怀疑“但是无论如何谁是客户2?”。

我第一次尝试处理新客户端的连接(让我们称之为X)是这个伪代码(请记住newClient已经锁定在这里):

lock (clients) {
  foreach (var client in clients) {
    lock (client) {
      client.Send("newClient with id X has connected");
    }
  }
  clients.Add(newClient);
  newClient.Send("the list of other clients");
}

现在想象一下,在同一时间,另一个客户端发送了一个数据包,该数据包转换为必须向每个连接的客户端广播的消息,伪代码将是这样的(记住当前客户端 - 让我们称之为Y - 已经锁定在这里):

lock (clients) {
  foreach (var client in clients) {
    lock (client) {
      client.Send("something");
    }
  }
}

此处出现明显的死锁:在一个线程上X被锁定,clients锁定已经进入,开始循环通过客户端,并且在某一时刻必须得到Y的锁定...已经在第二个线程,本身等待客户端集合锁被释放!

这不是服务器应用程序中唯一的这种情况。还有其他集合必须与客户端保持同步,客户端上的某些属性可以被另一个属性更改,等等。我尝试了其他类型的锁,无锁机制和其他一些东西。当我使用过多的锁以确保安全或其他明显的竞争条件时,有明显的死锁。当我终于在两者之间找到了一个很好的中间点时,通常会出现非常微妙的竞争条件/死锁以及其他多线程问题...我的头很快受到伤害因为我写的任何一行代码我都有检查几乎整个应用程序,以确保任何线程都可以正常运行。

所以这是我的最后一个问题:你如何解决这个具体案例,一般情况,更重要的是:我不是在这里走错路吗?我对.NET框架,C#,简单并发或算法一般没什么问题。不过,我迷失在这里。我知道我只能使用一个线程处理传入的请求,一切都会好的。然而,对于更多的客户来说,这根本不会很好地扩展......但我正在考虑越来越多地采用这种简单的方式。你觉得怎么样?

先谢谢你,StackOverflow人员花时间阅读这个大问题。如果我想得到一些帮助,我真的必须解释整个背景。

3 个答案:

答案 0 :(得分:4)

如果您因应用程序的多线程特性而遇到锁定,竞争条件等问题,那么任何人都很难提供即时解决方案。这些问题最多可能是非常间歇性的,并且不能总是容易地再现。这使得即使对于坐在所有代码前面的人也很难。但我会提供一种替代方案,即考虑使用某种消息队列作为发布 - 订阅主干。使用这种架构可以帮助简化许多锅炉板代码。正如我所说,这可能或可能会立即解决您的问题,但希望与您分享不同的方法。

答案 1 :(得分:2)

我在之前的评论中提到了Erlang,并在另一条评论中排队了消息处理。 Erlang的设计初衷是为了支持高度并发,无共享,消息传递风格的系统。

http://en.wikipedia.org/wiki/Erlang_(programming_language)

虽然我从来没有用过它的愤怒,但我读过这本书(编程Erlang),并且非常喜欢它所体现的并发消息传递方法的简单之美。在做了大量复杂的多线程开发之后,我可以理解Erlang寻求解决的挑战,即共享资源和同步的复杂性。

有一个C#项目试图体现Erlang的概念 - Retlang:

http://code.google.com/p/retlang/wiki/GettingStarted

从未使用它,但是消息传递方法绝对是一个好方法,并且可能非常适合您要实现的目标。

答案 2 :(得分:1)

我真的对.NET一无所知,但我可以在C和Linux世界中分享我在异步编程方面的一些经验。

首先,用加仑和加仑盐来加此,但是:使用线程(而不是过程)往往是一个坏主意。 进程仅共享您要共享的信息(通过消息传递),而线程共享所有内容 。因为您不能与每个线程共享代码可访问的每个对象,所以您必须使用锁和诸如此类的东西明确指出未共享的内容。使用流程通常更容易,因为您只需指定共享的内容。我不记得我在哪里读到这个,但有人将多线程编程与你必须在没有内存管理的系统(例如DOS)或操作系统内核中遵循的编程风格进行了比较。这种类型的编程在用户空间中通常是不必要的,因为操作系统和MMU(内存管理单元)会为您处理这些问题。

不使用线程的大型异步程序的一个例子是PostgreSQL。事实上,在其Todo列表中,它列在“我们不想要的功能”(see here)下。当然,现在可能会出现以下情况:线程可以加速任务(因为它们比实例更便宜实例化),但它们不是(也不会很快)用作异步的主要工具在PostgreSQL中编程。

线程和进程的替代方法是简单地使用一个线程和一个进程,但具有事件循环和快速处理程序。但是,这种方法的缺点包括:  *您的代码必须被切碎成不能睡觉的部分。您不必调用简单地下载URL并返回结果的函数,而是必须在结果准备好时提供回调,并且还让主循环响应与下载URL相关的事件(例如,单个数据包到达)。  *您可能无法避免睡觉,或者可能过度困难。

对于一个相对简单的守护进程,我建议使用单进程,单线程方法。但是,如果该守护进程的角色开始变大并且代码变得复杂,则可能需要将其拆分为单独的进程。