让多个线程共享方法工作负载



>我有一个名为asyncStartList的方法,它发送提供它的电子邮件列表,我正在尝试弄清楚如何在有很多电子邮件的情况下使用多个线程来加快该过程:

public async Task asyncStartList()
{
Stopwatch stopWatch = new Stopwatch();
stopWatch.Start();   
for (int i = 0; i < listLength; i++)
{
currentMailAddress = emailingList[i];
await Task.Run(() => MailingFunction());
currentMailAddress = "";
Console.WriteLine("Your mail to {0} was successfully sent!", emailingList[i]);
}
stopWatch.Stop();
TimeSpan ts = stopWatch.Elapsed;
string elapsedTime = String.Format("{0:00}:{1:00}:{2:00}.{3:00}", 
ts.Hours, ts.Minutes, ts.Seconds, ts.Milliseconds / 10);
Console.WriteLine("Time for completion " + elapsedTime);
Console.ReadLine();
}

MailingFunction()是一个简单的 SmtpClient 和邮件消息。

您的解决方案实际上并不并行运行,因为您需要等待每个发送操作。您可以使用 paralel foreach/for 关键字。否则,您必须在执行所有发送操作后等待。

public async Task asyncStartList()
{
Stopwatch stopWatch = new Stopwatch();
stopWatch.Start();
// option 1
Task[] tasks = emailingList.Select(s => Task.Run(() => { SendEmail(s); }).ToArray();
Task.WaitAll(tasks);
// option 1 end
// option 2
Parallel.ForEach(emailingList, email =>
{
SendEmail(email);
});
// option 2 end
// option 3
Parallel.For(0, emailingList.Length, i =>
{
SendEmail(emailingList[i]);
});
// option 3 end
stopWatch.Stop();
TimeSpan ts = stopWatch.Elapsed;
string elapsedTime = String.Format("{0:00}:{1:00}:{2:00}.{3:00}", ts.Hours, ts.Minutes, ts.Seconds, ts.Milliseconds / 10);
Console.WriteLine("Time for completion " + elapsedTime);
Console.ReadLine();
}
private void SendEmail(string emailAddress)
{
// Do send operation
}

使用System.Threading.Tasks命名空间中的Parallel.ForEach。所以为了for int i = 0;...使用Parallel.ForEach(emailingList, address => {...})

有关示例,请参阅 https://learn.microsoft.com/en-us/dotnet/standard/parallel-programming/how-to-write-a-simple-parallel-foreach-loop

如果解决方案的性能受 CPU 限制,则表示要使用并行线程。如果你的解决方案受到其他东西的约束——例如电子邮件服务器处理请求的能力——你实际应该使用的async,这更简单、更安全。

在这种情况下,有很多方法可以使用异步,但这里有一个简短而简单的模式可以工作:

await Task.WhenAll
(
emailingList.Select( async address => MailingFunctionAsync(address) )
);

是的,这就是它的全部内容。这假设您的电子邮件客户端不仅具有MailingFunction()方法,而且还具有MailingFunctionAsync()方法(例如,使用Outlook的SendAsync()方法或类似方法)。

以下是从此问题中窃取MailingFunctionAsync()示例:

public async Task MailingFunctionAsync(string toEmailAddress)
{
var message = new MailMessage();
message.To.Add(toEmailAddress);
message.Subject = SOME_SUBJECT;
message.Body = SOME_BODY;
using (var smtpClient = new SmtpClient())
{
await smtpClient.SendMailAsync(message);
}
}

这里的常见答案是使用Parallel.ForEach(除了John Wu的答案之外,你应该真正考虑)。虽然一开始Parallel.ForEach似乎是一个简单而好的想法,但它实际上并不是最佳方法。

问题是这样的:

Parallel.ForEach使用线程池。此外,IO 绑定操作将阻止那些等待设备响应并占用资源的线程。

  • 如果您有CPU 绑定代码,则适合并行性;
  • 不过,如果您有IO 绑定代码,异步是合适的。

在这种情况下,发送邮件显然是I/O,因此理想的使用代码将是异步的。

此外,若要正确使用.NET的异步和并行功能,还应了解I/O 线程的概念。

  • 并非程序中的所有内容都会消耗 CPU 时间。当线程尝试从磁盘上的文件读取数据或通过网络发送 TCP/IP 数据包时,它唯一要做的就是将实际工作委托给设备;磁盘或网络适配器;并等待结果。

  • 花费线程时间等待是非常昂贵的。即使线程休眠并且在等待结果时不消耗CPU时间,它也不会真正得到回报,因为它浪费了系统资源。

  • 简单来说,每个线程都包含堆栈变量、本地存储等的内存。此外,您拥有的线程越多,在它们之间切换所需的时间就越多。

虽然,Parallel.ForEach的好处是它易于实现,您也可以设置最大并行度等选项。

那你能做什么...

您最好对并发任务使用async/await模式和/或某种类型的限制,另一个简洁的解决方案是在 TPL 数据流库中ActionBlock<TInput>类。

数据流示例

var block = new ActionBlock<MySomething>(
mySomething => MyMethodAsync(mySomething),
new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = 50 });
foreach (var something in ListOfSomethings)
{
block.Post(something );
}
block.Complete();
await block.Completion;

这种方法给你异步,它也给你MaxDegreeOfParallelism,它不浪费资源,让IO成为IO,而不会咀嚼不必要的资源

免责声明,数据流可能不是您想要的地方,但是我只是想为您提供有关不同的更多信息 提供的方法

最新更新