2016-07-19 12 views
1

Ich habe einen Vertrag, der eine asynchrone Operation definiert, aber ich rufe es aus einem Bereich, der nicht als async eingerichtet ist. Ich möchte mehrere Anrufe parallel machen, und so begann ich mit diesem:async, aber nicht parallel

var tasks = inputs.Select(input => service.GetResult(input)); 
var results = tasks.WhenAll(tasks).Result; 

dachte ich, das würde alle Anrufe parallel beginnen und dann in der zweiten Zeile warten. Bei der Protokollierung des Zieldienstes stellte sich jedoch heraus, dass die Anrufe seriell eingehen.

fand ich this article, die eine ähnliche Methode des Aufrufs zu Mine zeigt und erklärt, dass es nicht unbedingt parallel laufen, so wechselte ich es nur auf einen gerade parallel Aufruf Test:

var results = new ConcurrentBag<Result>(); 
Parallel.ForEach(inputs, input => results.Add(service.GetResult(input).Result)); 

Dies funktioniert wie erwartet - ich kann sehen, dass die Anrufe parallel zum Dienst kommen.

Also, das bringt mich auf zwei Fragen:

1) Was sind die Nachteile von 2 mit Option gehen?
2) Wie würde ich Option 1 richtig funktionieren lassen?

Hier sind ein paar Dienste, um das Problem zu replizieren. Rufen Sie ClientService mit WCFTestClient und eine Liste von vier Ints (1, 2, 3, 4) als ein Beispiel auf. (Port wird wahrscheinlich ändern müssen, wenn Sie es ausführen.)

target:

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

Verwenden Sie niemals .Result, warten Sie immer auf asynchrone Aufrufe. – Bart

+0

@Bart: Um ".Result" nie zu verwenden, müsste ich den gesamten Client async/abwarten, aber das ist der einzige Ort, an dem ich einen Async-Vertrag habe. Ich kann meine eigene lokale Kopie des Vertrags machen (das ist in einer geteilten DLL) und einfach mit dem parallelen Anruf gehen, aber das beantwortet meine Frage nicht und hilft mir nicht zu verstehen, was hier passiert. – zimdanen

+0

asycn ist nicht das Gleiche wie parallel. Asycn blockiert einen Thread nicht, während IO auftritt, während parallel über mehrere Threads ausgeführt wird. Es gibt einige Überlappungen, in denen Sie asynchron warten können, bis ein anderer Thread beendet ist, aber der Hauptzweck von async besteht darin, Threads davon abzuhalten, unnötigerweise blockiert zu werden. – juharr

Antwort

3

Wenn die ConcurrencyMode Ihres Dienstes Single ist (der Standard - außer Kraft gesetzt werden kann in einem ServiceBehavior Attribut), bedeutet dies, dass der Dienst Anrufe sequenziell behandelt. Beide Optionen werden also nacheinander ausgeführt, nur dass der zweite nicht geordnete Ergebnisse liefert. Sie können zu ConcurrencyMode.Multiple wechseln, was gefährlicher ist, da dies bedeutet, dass der Dienst sorgfältig erstellt werden muss, um Thread-sicher zu sein.

  1. Parallel ist für CPU-gebundene Operationen optimiert und werden Ihre Anrufe nach der Anzahl der Kerne in Ihrem System parellelize. Tatsächlich können Sie IO-gebundene Operationen mehr als das parallelisieren, sodass das Ganze langsamer ausgeführt wird. Darüber hinaus verwenden Sie .Result für jeden Thread, verschwenden Wartezeit in jeder Aufgabe Parallel Spawns. Ich würde diesen Ansatz nicht verwenden. Schließlich ist ConcurrentBag ungeordnet, was für Sie wichtig sein kann oder nicht.

  2. In der ersten Option starten Sie jeden WCF-Aufruf vom UI-Thread sequenziell. Dies führt höchstwahrscheinlich dazu, dass die Anrufe von dem Dienst in der gleichen Reihenfolge der Liste behandelt werden.

sollten Sie wahrscheinlich Task.WaitAll() statt Task.WhenAll().Result werden. Ich würde von davon abhalten, das auf einem UI-Thread zu tun. Dies ist die Ursache für viele böse UI hängt. Sie können einfach eine asynchrone Methode von einer synchronen Methode starten (ohne Wait()) - feuern und vergessen. Aktualisieren Sie nach dem Warten auf die Aufgaben die Benutzeroberfläche bei Bedarf in der asynchronen Methode.

Eine letzte Empfehlung - bevor Sie mehrere gleichzeitige Anrufe mit dem gleichen Kanal, sollten Sie Open() es für eine bessere Leistung. Obwohl Kanäle das automatisch tun, gibt es hier einen Vorteil, weil der Kanal gesperrt wird.

EDIT -

Nach dem aktualisierten Code zu sehen, das Problem ist, dass Sie eine Aufgabe sind Start und dann warten, bis der Kanal synchron zu schließen (die Blöcke, bis der Anruf beendet ist). Hier ist eine bessere Umsetzung:

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

ich auch geändert habe es die zwischengespeicherte ChannelFactory zu verwenden und richtig zu schließen und den Abbruch des Kanals.

+1

Einige Punkte der Klarstellung: Der Zieldienst ist 'InstanceContextMode.PerCall' /' ConcurrenceMode.Single'. Der Aufrufer ist auch keine GUI, sondern ein anderer Dienst (mit den gleichen Optionen). Der Service-Client wird unter Verwendung von ChannelFactory erstellt und automatisch mit Open/Close-Aufrufen umbrochen. Zu meiner Frage: Es klingt so, als würden Sie vorschlagen, die 'WhenAll' für' WaitAll' zu wechseln und dann die Ergebnisse der Aufgaben separat zu bekommen? Das scheint keinen Unterschied zu machen. – zimdanen

+0

Es wird nicht, es ist nur besser, die Aufgaben-API zu verwenden. 'WaitAll' kann unter bestimmten Bedingungen Inline-Aufgaben ausführen. Ihr Problem ist die 'ConcurrenceMode.Single'. Beide Optionen haben die Operationen nacheinander ausgeführt, aber die zweite Option hat sie ungeordnet, weil sie von mehreren Threads gestartet wurden. Wenn Sie das ändern möchten, müssen Sie den 'ConcurrenceMode' ändern. –

+0

Entschuldigung für die Annahme, dass es ein UI-Thread war.Ich sehe gerade so viele Fragen :) –