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);
从技术上讲,拆分列表,然后启动并行运行list的线程是肯定可行的。你也可以自己实现所有的东西,就像你已经做的那样,但这不是一个好方法。首先,Parallel.For
或Parallel.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都会被使用,直到所有设备都被处理完为止。