2014-06-06 22 views
6

Ho un'esperienza recente che vorrei condividere che potrebbe essere utile a chiunque debba mantenere un servizio Web ASMX legacy che deve essere aggiornato per richiamare i metodi basati su attività.Chiamare i metodi basati su attività da ASMX

Ho recentemente aggiornato un progetto ASP.NET 2.0 che include un servizio Web ASMX legacy in ASP.NET 4.5. Come parte dell'aggiornamento, ho introdotto un'interfaccia API Web per consentire l'automazione avanzata dell'applicazione. Il servizio ASMX deve coesistere con la nuova API per la retrocompatibilità.

Una delle funzioni dell'applicazione è quella di poter richiedere dati da fonti di dati esterne (storici di impianti industriali, servizi Web su misura, ecc.) Per conto del chiamante. Come parte dell'aggiornamento, ho riscritto parti sostanziali del livello di accesso ai dati per richiedere in modo asincrono i dati utilizzando un modello asincrono basato su attività. Dato che non è possibile utilizzare aync/await in un servizio ASMX, ho modificato i metodi ASMX per effettuare chiamate di blocco ai metodi asincroni, ovvero chiamare il metodo basato su Task e quindi utilizzare Task.WaitAll per bloccare il thread fino al completamento dell'attività.

Quando si chiama un metodo ASMX che stava chiamando un metodo che restituisce Task o Task <T> sotto il cofano, ho trovato che la richiesta è sempre scaduta. Quando ho passato il codice, ho potuto vedere che il codice asincrono era in esecuzione, ma la chiamata a Task.WaitAll non ha mai rilevato che l'attività era stata completata.

Ciò ha causato un forte mal di testa: come potrebbe il servizio ASMX coesistere felicemente con le nuove funzionalità di accesso ai dati asincroni?

risposta

10

Ho recentemente aggiornato un progetto ASP.NET 2.0 che include un servizio Web ASMX legacy a ASP.NET 4.5.

La prima cosa da fare è garantire che [email protected] is set to 4.5 in your web.config.

l'attività padre (ovvero la chiamata di metodo nell'ASMX che ha restituito un'attività) non è mai stata rilevata come completata.

Questa è in realtà una classica situazione di deadlock.I describe it in full on my blog, ma l'essenza di ciò è che await acquisirà (di default) un "contesto" e lo userà per riprendere il metodo async. In questo caso, quel "contesto" è un contesto di richiesta ASP.NET, che consente solo un thread alla volta. Pertanto, quando il codice asmx aumenta lo stack nell'attività (tramite WaitAll), blocca un thread nel contesto di quella richiesta e il metodonon può essere completato.

Spingere l'attesa di blocco su un thread in background funzionerebbe, ma come si nota è un po 'brute-force. Un miglioramento minore sarebbe semplicemente utilizzare var result = Task.Run(() => MethodAsync()).Result;, che accoda il lavoro in background al pool di thread e quindi blocca il thread di richiesta in attesa del completamento. In alternativa, è possibile utilizzare ConfigureAwait(false) per ogni await, che sovrascrive il comportamento di "contesto" predefinito e consente al metodo async di continuare su un thread del pool di thread all'esterno del contesto della richiesta.


Ma un miglioramento molto migliore sarebbe utilizzare le chiamate asincrone "tutto il modo". (Nota a margine: la descrivo più in dettaglio in un MSDN article on async best practices).

ASMX does allow implementazioni asincrone di APM variety. Ti consiglio di rendere il codice di implementazione asmx prima possibile asimmetrico (ad esempio, utilizzando await WhenAll anziché WaitAll). Finirai con un metodo "core" che dovrai quindi aggiungere a wrap in an APM API.

L'involucro sarebbe simile a questa:

// Core async method containing all logic. 
private Task<string> FooAsync(int arg); 

// Original (synchronous) method looked like this: 
// [WebMethod] 
// public string Foo(int arg); 

[WebMethod] 
public IAsyncResult BeginFoo(int arg, AsyncCallback callback, object state) 
{ 
    var tcs = new TaskCompletionSource<string>(state); 
    var task = FooAsync(arg); 
    task.ContinueWith(t => 
    { 
    if (t.IsFaulted) 
     tcs.TrySetException(t.Exception.InnerExceptions); 
    else if (t.IsCanceled) 
     tcs.TrySetCanceled(); 
    else 
     tcs.TrySetResult(t.Result); 

    if (callback != null) 
     callback(tcs.Task); 
    }); 

    return tcs.Task; 
} 

[WebMethod] 
public string EndFoo(IAsyncResult result) 
{ 
    return ((Task<string>)result).GetAwaiter().GetResult(); 
} 

Questo diventa un po 'noioso se si dispone di un sacco di metodi per avvolgere, così ho scritto un po' di ToBegin and ToEnd methods come parte della mia AsyncEx library. L'utilizzo di questi metodi (o la vostra copia di loro se non si desidera che la dipendenza biblioteca), gli involucri semplificare bene:

[WebMethod] 
public IAsyncResult BeginFoo(int arg, AsyncCallback callback, object state) 
{ 
    return AsyncFactory<string>.ToBegin(FooAsync(arg), callback, state); 
} 

[WebMethod] 
public string EndFoo(IAsyncResult result) 
{ 
    return AsyncFactory<string>.ToEnd(result); 
} 
+0

Alcune informazioni davvero utili in là - grazie! Preferirei non apportare modifiche sostanziali all'interfaccia del servizio web (ad esempio, convertirlo per utilizzare l'APM), ma utilizzando Task.Run è sicuramente un modo migliore per evitare il deadlock. Ho anche usato ConfigureAwait (false) ovunque sia possibile, anche se la natura della bestia è che non posso garantire che venga sempre utilizzata, dal momento che alcuni dei driver di origine dati potrebbero, in teoria, essere scritti da Terze parti in futuro. –

+0

Se usi 'Task.Run', perdi * tutti * i vantaggi del codice asincrono. Ovviamente è una tua chiamata, ma tieni presente che, a meno che tu non faccia l'asma asincrono ASM (APM), non ottengono nessuno dei vantaggi del codice asincrono (come la scalabilità). –

+0

Capito. Al momento, ci sono un paio di applicazioni che usano il servizio ASMX; l'intenzione è quella di migrare questi per utilizzare l'API Web, quindi al momento stiamo solo cercando la compatibilità più di ogni altra cosa. Inoltre, l'ASMX è stato sinora sincrono fino ad ora, quindi è un caso di "non guadagnare" benefici asincroni piuttosto che perderli. Le nuove applicazioni che utilizzano l'app Web lo fanno in modo asincrono, tramite l'API Web. –

5

In seguito a ulteriori indagini, ho scoperto che le attività secondarie create dall'attività iniziale potevano essere attese senza problemi, ma l'attività principale (ovvero la chiamata al metodo nell'ASMX che restituiva un'attività <T>) non veniva mai rilevata come completamento.

L'indagine mi ha portato a teorizzare l'esistenza di una sorta di incompatibilità tra lo stack di servizi Web legacy e la libreria parallela attività. La soluzione che mi è venuta in mente è la creazione di un nuovo thread per eseguire le chiamate al metodo basato sulle attività, l'idea è che un thread separato non sarebbe soggetto alle incompatibilità di thread/task che esistevano nel thread che processava la richiesta ASMX. A tal fine, ho creato una semplice classe di supporto che verrà eseguito un Funz <T> in un nuovo thread, bloccare il thread corrente fino a quando il nuovo thread termina e poi restituisce il risultato della chiamata di funzione:

public class ThreadRunner<T> { 
    // The function result 
    private T result; 

    //The function to run. 
    private readonly Func<T> function; 

    // Sync lock. 
    private readonly object _lock = new object(); 


    // Creates a new ThreadRunner<T>. 
    public ThreadRunner(Func<T> function) { 
     if (function == null) { 
      throw new ArgumentException("Function cannot be null.", "function"); 
     } 

     this.function = function; 
    } 


    // Runs the ThreadRunner<T>'s function on a new thread and returns the result. 
    public T Run() { 
     lock (_lock) { 
      var thread = new Thread(() => { 
       result = function(); 
      }); 

      thread.Start(); 
      thread.Join(); 

      return result; 
     } 
    } 
} 

// Example: 
// 
// Task<string> MyTaskBasedMethod() { ... } 
// 
// ... 
// 
// var tr = new ThreadRunner<string>(() => MyTaskBasedMethod().Result); 
// return tr.Run(); 

Esecuzione del Il metodo basato sulle attività in questo modo funziona perfettamente e consente il completamento della chiamata ASMX, ma è ovviamente un po 'brute-force generare un nuovo thread per ogni chiamata asincrona; alternative, miglioramenti o suggerimenti sono benvenuti!

+1

hai trovato in ogni caso per rendere questo più 'bello'? : D –

Problemi correlati