【问题标题】:Should I use Threads or Tasks - Multiple Client Simulation我应该使用线程还是任务 - 多客户端模拟
【发布时间】:2012-04-19 11:52:24
【问题描述】:

我正在编写一个客户端模拟程序,其中所有模拟客户端都针对服务器运行一些预定义的例程 - 这是一个在 azure 中运行的具有四个实例的 Web 服务器。

所有模拟客户端在连接到服务器后运行相同的例程。

我想在任何时候使用我的程序模拟 300 到 800 个客户端。

我的问题是: 我应该创建 N 个客户端类实例并在 N 个不同的线程中运行它们吗?或

我应该使用任务库来做这些事情吗?

【问题讨论】:

标签: c# multithreading task-parallel-library simulation


【解决方案1】:

你当然不应该创建 800 个线程。

让我们退后一步。您有一个称为“服务器”的设备,它接收来自“客户端”的“请求”并将“响应”发送回这些客户端。假设请求是邮局投递的纸片,响应是包含书籍的盒子,也是邮局投递的。

您希望模拟 800 个客户端来测试服务器。

假设线程是人,处理器是椅子。一个人只能坐在椅子上工作。

创建 800 个线程相当于出去雇佣 800 个人,然后付钱给每个人给服务器发一封信。但是你只有四把椅子,所以这 800 个人必须轮流使用椅子。

这在现实生活中将是一个可笑的解决方案。线程和人一样,非常昂贵。您应该尽量减少创建的线程数。

那么,您是否应该改为通过任务工厂创建 800 个任务并让 TPL 为您并行化它们?

不,你也不应该那样做。 TPL 有一群人(线程)可供借鉴,它试图安排事情,以使工资单上的人不超过可供他们坐的椅子。但你的任务不是“椅子束缚” - - 人们将坐在椅子上,将请求发送到服务器,然后在等待响应返回时离开椅子。 在他们等待的同时,TPL 现在不得不雇佣更多的人来完成额外的任务。

访问 Web 服务器是 I/O 绑定的; 您应该只为受 CPU 限制的任务创建线程池任务。

正确的解决方案是雇佣 两个 人。

一个人——“I/O 完成线程”——除了将请求放入邮箱并检查传入的包之外什么都不做。另一个人——“模拟”人——计算出模拟 800 个客户的正确“时间表”。模拟人制定时间表,然后睡觉。当需要向服务器发送另一个请求时,她会醒来。当她醒来时,她告诉 I/O 完成线程将这封信放入邮箱中,并在收到响应时唤醒她。然后她再次进入睡眠状态,直到该发送另一个请求或响应的时间了进来需要验证。

您应该做的是 (1) 获取 C# 5 的 beta 版本并使用 async/await 创建将请求发送到服务器的任务,然后将控制权交还给消息循环,直到该发送另一个请求或响应进来了。或者,如果您不想使用 C# 5,您应该创建一个任务完成源,并设置具有正确延续的任务。

简而言之:处理许多并行 I/O 任务的正确方法是创建非常少量的线程,每个线程一次只执行非常少量的工作。让 I/O 完成线程处理 I/O 的细节。 你不需要雇佣 800 人来模拟发送 800 封信。雇佣 两个 人,一个看邮箱,一个写信。

【讨论】:

  • 如果他们与服务器进行长时间的交互怎么办?
【解决方案2】:

对于这个application domains 是你最好的选择。

应用程序域是 .NET 应用程序在其中执行的运行时隔离单元。它提供托管内存边界、应用程序配置设置的容器以及为分布式应用程序提供通信接口。

每个 .NET 应用程序通常只承载一个应用程序域,该域由 CLR 在给定进程/程序启动时自动创建。在单个进程/程序中创建其他应用程序域有时很有用(在您的情况下)。使用多个应用程序域可以避免通信复杂性,并使用多个单独的进程来实现,并提供任务隔离。

对于你想要的,你有两个选择。

  1. 在同一域中的不同线程上启动 X 线程。

这意味着你将不得不非常厌倦线程安全,这对于模拟多个登录、模拟客户端等任务来说是非常困难的。

  1. 在同一个进程中启动 X 个线程,每个线程都在自己的应用程序域中。

这将使每个自旋线程保持隔离,并且托管应用程序/程序也易于访问。通过在 X 个单独的应用程序域中进行所有 X 模拟,每个域将被隔离,并且无法通过静态类成员等干扰另一个客户端模拟。

以下内容由 Joseph Albahari 的书 C# 4.0 In a Nutshell 的摘录辅助,我强烈建议您获取:

40 个并发客户端模拟的示例可能对您有用:

class program
{
    static void main()
    {
        // Create 40 domains and 40 threads.
        AppDomain[] domains = new AppDomain[40];
        Thread[] thread = new Thread[40];

        for (int i = 0; i < 40; i++)
        {
            domains[i] = AppDomain.CreateDomain("Client Simulation " + i);
            thread[i] = new Thread(SimulateClientInOtherDomain);
        }

        // Start all threads, passing to each thread its app domain.
        for (int j = 0; j < 40; j++)
            threads[j].Start(domains[j]);

        // Wait for the threads to finish.
        for (int k = 0; k < 40; k++)
            threads[k].Join();

        // Unload the application domains.
        for (int l = 0; l < 40; l++)
            AppDomain.Unload(domains[l]);
    }

    // Thread start with input of with domain to run on/in.
    static void SimulateClientInOtherDomain(object domain)
    {
        ((AppDomain)domain).DoCallBack(Simulate);
    }

    static void Simulate()
    {
       Client simClient1 = new Client("Bill", "Gates", ...);
       simClient1.Simulate();
    }
}

我希望这会有所帮助。

【讨论】:

    【解决方案3】:

    在这种情况下的答案并不那么简单。这实际上取决于您希望如何模拟您的客户:

    1. 如果您想连接 800 个客户端,但不一定要同时连接,最好使用Tasks。它们是轻量级的,可以有效利用底层的ThreadPool

    2. 如果您真的希望客户端完全并行,恐怕没有办法真正避免线程。没有什么神奇的方法可以同时执行 800 个轻量级任务。 Task 抽象是轻量级的,因为它使用线程池。这意味着许多任务被映射到少量的实际线程。但是,当然,这意味着它们并不是真正并行运行,而是尽可能安排运行。 ThreadPool 的最大线程数为 250 (AFAIK),因此如果您使用 Tasks,一次实际执行的“客户端”不会超过 250 个。解决方案是将最大线程数设置为 800,但此时它与使用经典线程数相同。

    【讨论】:

      【解决方案4】:

      我会使用任务库并让任务库为您处理所有线程。您不想启动 800 个线程。一次同时运行这么多线程是个坏主意,这是另一个讨论这个问题的堆栈溢出问题:Maximum number of threads in a .NET app?

      【讨论】:

        猜你喜欢
        • 2019-03-11
        • 2015-12-10
        • 2015-11-09
        • 1970-01-01
        • 2014-03-28
        • 2011-12-06
        • 2019-11-20
        • 2012-09-22
        • 2019-01-30
        相关资源
        最近更新 更多