我想比较两种理论情景。为了这个问题,我简化了案例。但基本上它是典型的生产者消费者情景。 (我专注于消费者)。
我有一个很大的Queue<string> dataQueue
我必须传输到多个客户端。
让我们从更简单的案例开始:
class SequentialBlockingCase
{
public static Queue<string> DataQueue = new Queue<string>();
private static List<string> _destinations = new List<string>();
/// <summary>
/// Is the main function that is run in its own thread
/// </summary>
private static void Run()
{
while (true)
{
if (DataQueue.Count > 0)
{
string data = DataQueue.Dequeue();
foreach (var destination in _destinations)
{
SendDataToDestination(destination, data);
}
}
else
{
Thread.Sleep(1);
}
}
}
private static void SendDataToDestination(string destination, string data)
{
//TODO: Send data using http post, instead simulate the send
Thread.Sleep(200);
}
}
}
现在这个设置工作得很好。它位于那里并轮询Queue
,当有数据要发送时,它会将其发送到所有目的地。
的问题:
所以这是我的第二次尝试:
class ParalleBlockingCase
{
public static Queue<string> DataQueue = new Queue<string>();
private static List<string> _destinations = new List<string>();
/// <summary>
/// Is the main function that is run in its own thread
/// </summary>
private static void Run()
{
while (true)
{
if (DataQueue.Count > 0)
{
string data = DataQueue.Dequeue();
Parallel.ForEach(_destinations, destination =>
{
SendDataToDestination(destination, data);
});
}
else
{
Thread.Sleep(1);
}
}
}
private static void SendDataToDestination(string destination, string data)
{
//TODO: Send data using http post
Thread.Sleep(200);
}
}
如果1个目的地缓慢或不可用,此修订至少不会影响其他目的地。
但是这个方法仍然是阻塞的,我不确定Parallel.ForEach
是否使用了线程池。我的理解是它将创建X个线程/任务并一次执行4个(4个核心cpu)。但在任务5开始之前,它必须完全完成芬兰语任务1。
因此我的第三个选择:
class ParalleAsyncCase
{
public static Queue<string> DataQueue = new Queue<string>();
private static List<string> _destinations = new List<string> { };
/// <summary>
/// Is the main function that is run in its own thread
/// </summary>
private static void Run()
{
while (true)
{
if (DataQueue.Count > 0)
{
string data = DataQueue.Dequeue();
List<Task> tasks = new List<Task>();
foreach (var destination in _destinations)
{
var task = SendDataToDestination(destination, data);
task.Start();
tasks.Add(task);
}
//Wait for all tasks to complete
Task.WaitAll(tasks.ToArray());
}
else
{
Thread.Sleep(1);
}
}
}
private static async Task SendDataToDestination(string destination, string data)
{
//TODO: Send data using http post
await Task.Delay(200);
}
}
现在从我理解这个选项开始,仍会在Task.WaitAll(tasks.ToArray());
的主线程上阻塞,这很好,因为我不希望它以创建任务的速度跑得比执行更快。
但是并行执行的任务应该使用ThreadPool
,并且所有X个任务应该立即开始执行,而不是阻塞或按顺序执行。 (线程池将在它们变为活动状态或awaiting
)
现在我的问题。
选项3是否比选项2具有任何性能优势。
特别是在性能更高的服务器端方案中。在我正在处理的特定软件中。上面我的简单用例会有多个实例。也就是几个消费者。
我对两种解决方案的理论差异和专业与缺点感兴趣,如果有的话,我甚至可能是更好的第四种选择。
答案 0 :(得分:9)
Parallel.ForEach
将使用线程池。异步代码将不,因为it doesn't need any threads at all(链接到我的博客)。
正如Mrinal指出的那样,如果你有CPU绑定代码,那么并行性是合适的;如果您有I / O绑定代码,则异步是合适的。在这种情况下,HTTP POST显然是I / O,因此理想的消费代码将是异步的。
如果有的话,甚至可能是更好的第四选择。
我建议让您的消费者完全异步。为此,您需要使用与异步兼容的生产者/消费者队列。那是一个相当高级的(BufferBlock<T>
) in the TPL Dataflow library,而且是一个相当简单的(AsyncProducerConsumerQueue<T>
) in my AsyncEx library。
使用其中任何一个,您都可以创建一个完全异步的消费者:
List<Task> tasks = new List<Task>();
foreach (var destination in _destinations)
{
var task = SendDataToDestination(destination, data);
tasks.Add(task);
}
await Task.WhenAll(tasks);
或更简化:
var tasks = _destinations
.Select(destination => SendDataToDestination(destination, data));
await Task.WhenAll(tasks);
答案 1 :(得分:2)
你的主要问题 - Parallel.ForEach vs Async Forloop
computing operations
,在内存处理中始终Parallel API
,因为线程池调用的线程用于执行某些工作,这是调用的目的。IO bound operations
,始终为Async-Await
,因为没有调用任何线程,并且它使用硬件功能IO completion ports
在后台进行处理。由于Async-Await是首选选项,因此请允许我在您的实现中指出一些内容:
Synchronous
因为您没有等待主要操作Send data using http post
,正确的代码将是await Http Post Async
而不是await Task.Delay
Async
之类的标准Http post Async
实施,则无需明确启动Task
,只有在您使用自定义Async
方法的情况下才会这样做Task.WaitAll
仅适用于没有Synchronization上下文或UI线程的Console应用程序,否则会导致死锁,需要使用Task.WhenAll
现在关于Parallel approach
Parallel API
确实可以在Thread pool
上运行,并且大多数情况下它能够重用线程,从而进行优化,但如果任务长时间运行,它可能最终会创建多个线程,以限制您可以使用构造函数选项new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount }
,从而将最大数量限制为系统中逻辑核心数另一个要点为什么Parallel API
对于IO
绑定调用一个坏主意,因为每个线程都是UI
的代价高昂的资源,包括创建Thread environment block + User memory + Kernel Memory
和IO操作它闲置无所事事,这无论如何都不好