TPL与多线程

本文关键字:多线程 TPL | 更新日期: 2023-09-27 18:28:48

我是线程的新手,需要对以下场景进行说明。

我正在开发苹果推送通知服务。我的应用程序要求在网站上添加新交易时向3万用户发送通知。

我可以将3万个用户分成列表,每个列表包含1000个用户,并启动多个线程吗?或者可以使用任务吗?

以下方式有效吗?

if (lstDevice.Count > 0)
{
    for (int i = 0; i < lstDevice.Count; i += 2)
    {
        splitList.Add(lstDevice.Skip(i).Take(2).ToList<DeviceHelper>());
    }
    var tasks = new Task[splitList.Count];
    int count=0;
    foreach (List<DeviceHelper> lst in splitList)
    {
        tasks[count] = Task.Factory.StartNew(() =>
        {
            QueueNotifications(lst, pMessage, pSubject, pNotificationType, push);
        },
            TaskCreationOptions.None);
       count++;
    }

QueueNotification方法只会循环遍历每个列表项,并创建一个类似的有效负载

foreach (DeviceHelper device in splitList)
{
    if (device.PlatformType.ToLower() == "ios")
    {
        push.QueueNotification(new AppleNotification()
                                    .ForDeviceToken(device.DeviceToken)
                                    .WithAlert(pMessage)
                                    .WithBadge(device.Badge)
                                     );
        Console.Write("Waiting for Queue to Finish...");
    }
}
push.StopAllServices(true);

TPL与多线程

从技术上讲,拆分列表,然后启动并行运行list的线程是肯定可行的。你也可以自己实现所有的东西,就像你已经做的那样,但这不是一个好方法。首先,Parallel.ForParallel.ForEach已经在做将列表拆分为并行处理的块的工作。没有必要自己重新实施一切。

现在,您不断询问某个东西是否可以并行运行300或500个通知。但实际上,这不是一个好问题,因为你完全错过了并行运行的要点。

所以,让我来解释一下为什么这个问题不好。一开始,你应该问问自己,为什么你想并行运行一些东西?答案是,你希望通过使用多个CPU核心来更快地运行。

现在,您的简单想法可能是生成300或500个线程更快,因为您有更多的线程,并且它"并行"运行更多的东西。但事实并非如此。

起初,创建线程不是"免费的"。您创建的每个线程都有一些开销,创建一个线程需要一些CPU时间,而且还需要一些内存。最重要的是,如果创建300个线程,并不意味着300个线程并行运行。例如,如果你有一个8核CPU,那么只有8个线程可以并行运行。创建更多线程甚至会影响性能。因为现在您的程序需要在线程之间不断切换,这也会降低CPU性能。

所有这些的结果是。如果你有一些轻量级的小代码,不需要做很多计算,那么创建大量线程会减慢你的应用程序的速度,而不是运行得更快,因为管理线程会比在8个cpu核心上运行更大的开销。

这意味着,如果你有一个30000的列表。通常情况下,将列表拆分为8个块并在8个线程中处理列表会更快,就像创建300个线程一样。

你的目标永远不应该是:它能并行运行xxx件事情吗?问题应该是:我需要多少个线程,每个线程应该处理多少项才能最快地完成我的工作。

这是一个重要的区别,因为仅仅产生更多的线程并不意味着事情会很快结束。

那么,你需要多少个线程,每个线程应该处理多少个项目?嗯,你可以写很多代码来测试它。但数量会随着硬件的不同而变化。一台只有4个核心的电脑比一个有8个核心的系统有另一个最佳选择。如果你正在做的是IO绑定的(例如对磁盘/网络的读/写),那么增加线程也不会提高速度。

因此,您现在可以做的是测试所有内容,尝试获得正确的线程编号,并进行大量基准测试以找到最佳编号。

但实际上,这就是具有Task<T>类的TPL库的全部目的。Task<T>类已经在查看您的计算机有多少cpu核心。当你运行任务时,它会自动尝试创建尽可能多的线程,以最大限度地利用你的系统。

因此,我的建议是,您应该将TPL库与Task<T>类一起使用。在我看来,你永远不应该自己直接创建线程或自己进行分区,因为所有这些都已经在TPL中完成了。

我认为Task-class是一个很好的选择,因为你可以轻松地处理异步进程,而不必直接处理线程。

也许这有帮助:任务与线程差异

但为了给你一个更好的答案,你应该改进你的问题,并给我们更多的细节。

您应该小心创建过多的并行线程,因为这会减慢应用程序的速度。阅读SO的这篇文章:多少线程太多?。最好的办法是让它可配置,然后测试一些值。

我同意Task是一个不错的选择,但创建过多的任务也会给您的系统带来风险,对于故障,您的决定也是提出解决方案的一个因素。对我来说,我更喜欢MSQueue与线程池相结合。

如果你想并行创建推送通知,并通过使用计算机上的所有CPU来最大限度地提高性能,你应该使用Parallel.ForEach:

Parallel.ForEach(
  devices,
  device => {
    if (device.PlatformType.ToUpperInvariant() == "IOS") {
      push.QueueNotification(
        new AppleNotification()
          .ForDeviceToken(device.DeviceToken)
          .WithAlert(message)
          .WithBadge(device.Badge)
      );
    }
  }
);
push.StopAllServices(true);

这假设调用push.QueueNotification是线程安全的。此外,如果此调用锁定共享资源,则可能会因为锁争用而导致性能低于预期。

为了避免这种锁争用,您可以为Parallel.ForEach创建的每个分区创建一个单独的队列。我在这里即兴发挥了一点,因为问题中遗漏了一些细节。我假设变量push是类型Push:的实例

Parallel.ForEach(
  devices,
  () => new Push(),
  (device, _, push) => {
    if (device.PlatformType.ToUpperInvariant() == "IOS") {
      push.QueueNotification(
        new AppleNotification()
          .ForDeviceToken(device.DeviceToken)
          .WithAlert(message)
          .WithBadge(device.Badge)
      );
    }
    return push;
  },
  push.StopAllServices(true);
);

这将为Parallel.ForEach创建的每个分区创建一个单独的Push实例,当分区完成时,它将在实例上调用StopAllServices

这种方法应该不会比将设备拆分为N个列表更糟糕,其中N是CPU的数量,并启动N个线程或N个任务来处理每个列表。如果一个线程或任务"落后",则总执行时间将是该"慢"线程或任务的执行时间。使用Parallel.ForEach时,所有CPU都会被使用,直到所有设备都被处理完为止。