2016-07-19 12 views
1

私は非同期操作を定義するコントラクトを持っていますが、私は非同期として設定されていない領域から呼び出しています。私は、並行して複数の呼び出しを作りたいので、私はこれを開始しました:私はそれが並列にすべてのコールを開始した後、第2ラインで待機していました考え出し非同期ですが非同期です

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を呼び出し、例として4つのint(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

.Resultを使用しないでください。常に非同期呼び出しを待っています。 – Bart

+0

@Bart: ".Resultを使わない"ためには、クライアント全体をasync/awaitに変更する必要がありますが、これは非同期契約がある唯一の場所です。私は自分のローカルコピーを(共有DLLにあります)パラレルコールにすることはできますが、私の質問には答えず、ここで何が起こっているのか理解できません。 – zimdanen

+0

asycnは並列と同じものではありません。 AsycnはIOが発生している間にスレッドをブロックしないようにしていますが、並列は複数のスレッドで実行しています。非同期的に別のスレッドが終了するのを待つことができるという点で重複していますが、非同期の主な目的はスレッドが不必要にブロックされないようにすることです。 – juharr

答えて

3

をあなたのサービスのConcurrencyModeSingle(デフォルトの場合 - 上書きすることができますServiceBehavior属性の場合)、サービスが呼び出しを順次処理することを意味します。したがって、両方のオプションが実際に順番に実行されます。それは、2番目のオプションが順序付けられていない結果を得たということだけです。 ConcurrencyMode.Multipleに切り替えることができます。これは、サービスがスレッドセーフであるように慎重に作成する必要があるため、より危険です。

  1. Parallel CPUバウンドの操作に最適化され、システム内のコア数に応じて、あなたの呼び出しをparellelizeます。実際には、IOバインドされた操作をそれよりも並列化することができるので、全体の処理が遅くなります。さらに、すべてのスレッドで.Resultを使用しているため、各タスクで待機時間が無駄になりますParallelが発生します。私はこのアプローチを使用しません。最後に、ConcurrentBagは順序付けられておらず、重要ではないかもしれません。

  2. 最初のオプションでは、UIスレッドから順番に各WCF呼び出しを起動します。これにより、リストの同じ順序でConcurrencyMode.Singleサービスによってコールが処理される可能性が高くなります。

あなたはおそらく代わりにTask.WhenAll().ResultTask.WaitAll()を使用する必要があります。 私はあなたがUIスレッドでそれをやっていないことを断念します。これは、多くの不快なUIハングの根本的な原因です。同期メソッドから非同期メソッドを起動することができます(Wait()を使用せずに) - ただ起動して忘れてください。タスクを待ってから、asyncメソッドで必要に応じてUIを更新するだけです。

1つの最後の推奨 - 同じチャネルを使用して複数の同時呼び出しを行う前に、より良いパフォーマンスのためにOpen()を指定する必要があります。チャンネルはそれを自動的に行いますが、チャンネルをロックすることで利点があります。

EDIT -

あなたの更新されたコードを見た後、問題がタスクを起動してから(コールが終了するまで、どのブロック)を同期的にクローズするチャネルを待っているということです。私も変更した

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を使用すると、適切に閉鎖し、チャネルを中止する:ここではより高度な実装です。

+1

明確化のポイント:ターゲットサービスは 'InstanceContextMode.PerCall' /' ConcurrenceMode.Single'です。呼び出し元はGUIではなく、別のサービス(オプションも同じです)です。サービスクライアントはChannelFactoryを使用して構築され、オープン/クローズコールで自動的にラップされます。私の質問に:あなたは 'WaitAll'のための' WhenAll'を単に外した後、別々にタスクから結果を得ることを提案しているようですね?それは違いがないようです。 – zimdanen

+0

これは、Tasks APIを使うほうが良いでしょう。 'WaitAll'は特定の条件の下でタスクをインライン化できます。あなたの問題は 'ConcurrenceMode.Single'です。どちらのオプションも操作を順番に実行しましたが、2つ目のオプションは複数のスレッドから起動されたため、順序付けられていません。変更したい場合は、 'ConcurrenceMode'を変更する必要があります。 –

+0

そして、それがUIスレッドであると仮定して申し訳ありません。私はちょうどそのような多くの質問が表示されます:) –