异步但不平行

时间:2016-07-19 17:38:17

标签: c# wcf asynchronous parallel-processing

我有一个定义异步操作的合约,但是我从一个没有设置为异步的区域调用它。我想并行进行多个调用,所以我从这开始:

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);
    }
}

1 个答案:

答案 0 :(得分:3)

如果您的服务的ConcurrencyModeSingle(默认值 - 可以在ServiceBehavior属性中覆盖),则表示服务按顺序处理呼叫 。所以这两个选项都是按顺序执行的,只是第二个选项得到了无序结果。您可以切换到ConcurrencyMode.Multiple,这更危险,因为这意味着必须仔细编写服务以保证线程安全。

  1. Parallel针对CPU绑定操作进行了优化,并将根据系统中的核心数量来确定您的呼叫。实际上,您可以并行化IO绑定操作,因此整个操作执行速度会更慢。此外,您在每个线程上使用.Result,在每个任务Parallel中浪费等待时间。我不会用这种方法。最后,ConcurrentBag是无序的,这可能对你不重要。

  2. 在第一个选项中,您将按顺序从UI线程启动每个WCF调用。这很可能会导致ConcurrencyMode.Single服务按照列表的相同顺序处理呼叫。

  3. 您应该使用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,并正确关闭和中止频道。