我有一个定义异步操作的合约,但是我从一个没有设置为异步的区域调用它。我想并行进行多个调用,所以我从这开始:
var tasks = inputs.Select(input => service.GetResult(input));
var results = tasks.WhenAll(tasks).Result;
我认为这将并行启动所有调用,然后等待第二行。但是,在查看目标服务的日志记录时,我发现这些调用是串行进行的。
我发现this article显示了一种类似的调用我的方法,并解释说它不一定要并行运行,所以我只是将其切换为直接并行调用来测试:
var results = new ConcurrentBag<Result>();
Parallel.ForEach(inputs, input => results.Add(service.GetResult(input).Result));
这可以按预期工作 - 我可以看到呼叫是并行发送到服务的。
所以,这让我有两个问题:
1)使用选项2有什么缺点?
2)如何让选项1正常工作?
以下是一些复制问题的服务。以WCFTestClient调用ClientService和四个整数列表(1,2,3,4)作为示例。 (运行时,端口可能需要更改。)
TargetService:
using System.Diagnostics;
using System.ServiceModel;
using System.Threading.Tasks;
namespace AsyncNotParallel
{
[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall, ConcurrencyMode = ConcurrencyMode.Single)]
public class TargetService : ITargetService
{
public async Task<int> GetResult(int input)
{
Trace.WriteLine($"In: {input}");
await Task.Delay(1000); // Do stuff.
Trace.WriteLine($"Out: {input}");
return input;
}
}
[ServiceContract]
public interface ITargetService
{
[OperationContract]
Task<int> GetResult(int input);
}
}
ClientService:
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.ServiceModel;
using System.Threading.Tasks;
namespace AsyncNotParallel
{
[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall, ConcurrencyMode = ConcurrencyMode.Single)]
public class ClientService : IClientService
{
public int GetResults(List<int> inputs)
{
// Option 1:
var tasks = inputs.Select(input => Execute((ITargetService service) => service.GetResult(input)));
var results1 = Task.WhenAll(tasks).Result.Sum();
// Option 2:
var bag = new ConcurrentBag<int>();
Parallel.ForEach(inputs, input => bag.Add(Execute((ITargetService service) => service.GetResult(input)).Result));
var results2 = bag.Sum();
return results1 + results2;
}
private TResult Execute<TService, TResult>(Func<TService, TResult> operation)
{
var address = new EndpointAddress("http://localhost:34801/TargetService.svc");
var binding = new BasicHttpBinding();
var factory = new ChannelFactory<TService>(binding, address);
var channel = factory.CreateChannel();
var result = operation(channel);
((IClientChannel)channel).Close();
return result;
}
}
[ServiceContract]
public interface IClientService
{
[OperationContract]
int GetResults(List<int> inputs);
}
}
答案 0 :(得分:3)
如果您的服务的ConcurrencyMode
为Single
(默认值 - 可以在ServiceBehavior
属性中覆盖),则表示服务按顺序处理呼叫 。所以这两个选项都是按顺序执行的,只是第二个选项得到了无序结果。您可以切换到ConcurrencyMode.Multiple
,这更危险,因为这意味着必须仔细编写服务以保证线程安全。
Parallel
针对CPU绑定操作进行了优化,并将根据系统中的核心数量来确定您的呼叫。实际上,您可以并行化IO绑定操作,因此整个操作执行速度会更慢。此外,您在每个线程上使用.Result
,在每个任务Parallel
中浪费等待时间。我不会用这种方法。最后,ConcurrentBag
是无序的,这可能对你不重要。
在第一个选项中,您将按顺序从UI线程启动每个WCF调用。这很可能会导致ConcurrencyMode.Single
服务按照列表的相同顺序处理呼叫。
您应该使用Task.WaitAll()
代替Task.WhenAll().Result
。我会非常不鼓励你在UI线程上这样做。这是许多令人讨厌的UI挂起的根本原因。您可以简单地从同步方法启动异步方法(不需要Wait()
) - 只需点开即可忘记。等待任务后,只需根据需要在异步方法中更新UI。
最后一条建议 - 在使用同一频道进行多个并发呼叫之前,您应该Open()
以获得更好的性能。尽管频道会自动执行此操作,但由于某些频道的锁定,这里有一个好处。
编辑 -
在看到更新的代码之后,问题在于您正在启动任务,然后等待通道同步关闭(这将阻塞直到通话结束)。这是一个更好的实现:
private async Task<TResult> Execute<TService, TResult>(Func<TService, Task<TResult>> operation)
{
var address = new EndpointAddress("http://localhost:34801/A");
var binding = new BasicHttpBinding();
var channel = ChannelFactory<TService>.CreateChannel(binding, address);
var clientChannel = (IClientChannel)channel;
try
{
var result = await operation(channel).ConfigureAwait(false);
return result;
}
finally
{
if (clientChannel.State != CommunicationState.Faulted)
{
await Task.Factory.FromAsync(clientChannel.BeginClose, clientChannel.EndClose, null).ConfigureAwait(false);
}
else if (clientChannel.State != CommunicationState.Closed)
{
clientChannel.Abort();
}
}
}
我还修改了它以使用缓存的ChannelFactory
,并正确关闭和中止频道。