5

Possiedo un'applicazione Web ASP.NET MVC che rende le chiamate di servizio Web in stile REST ad altri server. Ho uno scenario in cui sto facendo due chiamate HttpWebRequest a due servizi separati. Ho bisogno che entrambi si completino per continuare, ma il loro ordine non ha importanza. Potrebbero impiegare 1-2 secondi ciascuno e li sto eseguendo in sequenza ora. Eseguirle in parallelo ridurrebbe il tempo di risposta dell'utente, ma qual è il modo migliore?Consigli per l'esecuzione di .NET HttpWebRequests in parallelo in ASP.NET

Nella ricerca di questo, mi viene in mente diverse opzioni:

  • Eseguire una richiesta sul thread principale, e girare un secondo thread per l'altra richiesta. Dovrebbero essere creati nuovi thread o usare un pool di thread? Se utilizzo una piscina, come posso ridimensionarla? Inoltre, non sono sicuro di come posso unire i fili di nuovo insieme (ad esempio, utilizzare ThreadPool.RegisterWaitForSingleObject)?
  • Prova e utilizza il supporto integrato IAsyncResult per una o entrambe le richieste. Di nuovo, non sono sicuro su quali thread esegue la richiesta asincrona, quindi non si è sicuri di come dimensionare il pool di thread. Come posso unirmi a IAsyncResult nel mio thread principale? Tutti gli esempi trovo le informazioni sul processo nella richiamata, ma posso solo aspettare nella mia thread principale e utilizzare la proprietà IsCompleted?

Ho bisogno di trovare una soluzione che funzionerà ed eseguirà su scala. Questo è il motivo per cui sono preoccupato per il dimensionamento del pool di thread. Mi dispiacerebbe avere il blocco delle richieste perché sono in attesa di thread disponibili.

risposta

1

Una delle risposte a Multithreading WebRequests, a good and stable approach? : CSharp utilizza una ManualResetEvent event = new ManualResetEvent(), un contatore di riferimento eguagliando il numero di richieste in volo e Interlocked.Decrement per controllare il event.Set(). Quindi, il thread principale attende chiamando il numero event.WaitOne().

Tuttavia, WaitHandles - Auto/ManualResetEvent and Mutex menziona che ManualResetEvent "può essere notevolmente più lento rispetto all'utilizzo dei vari metodi di monitoraggio" come Wait, Pulse e PulseAll.

Ho finito per basare il mio codice su questo post del blog Noah Blumenthal: Run generic tasks async (fluent-ly). Ho apportato due modifiche: implementare IDisposable e chiamare .Close() su ManualResetEvent e passare dall'utilizzo di lock() a Interlocked.Increment() e .Decrement().

public class AsyncQueueManager : IDisposable { 
    private readonly ManualResetEvent waitHandle = new ManualResetEvent(true); 
    private int count = 0; 

    public AsyncQueueManager Queue(Action<object> action) { 
     Interlocked.Increment(ref count); 
     waitHandle.Reset(); 
     Action<object> actionWrapper = CreateActionWrapper(action); 
     WaitCallback waitCallback = new WaitCallback(actionWrapper); 
     ThreadPool.QueueUserWorkItem(waitCallback); 
     return this; 
    } 

    private Action<object> CreateActionWrapper(Action<object> action) { 
     Action<object> actionWrapper = (object state) => 
     { 
      try { 
       action(state); 
      } catch (Exception ex) { 
       // log 
      } finally { 
       if (Interlocked.Decrement(ref count) == 0) { 
        waitHandle.Set(); 
       } 
      } 
     }; 
     return actionWrapper; 
    } 

    public void Wait() { 
     waitHandle.WaitOne(); 
    } 
    public void Wait(TimeSpan timeout) { 
     waitHandle.WaitOne(timeout); 
    } 

    public void Dispose() { 
     waitHandle.Close(); 
    } 
} 
-2

A meno che non stiate facendo qualcosa di particolare, la dimensione del ThreadPool è fissa e generalmente abbastanza grande da soddisfare le vostre esigenze. Nel tuo caso particolare potresti incorrere in problemi di vincoli di risorse usando le chiamate AsyncIO se il tuo webservice è sotto carico pesante e ogni chiamante ha avviato 2 thread TP per i propri callback.

Forse il più facile attuazione del presente sarebbe quello di avere due funzioni di callback distinte, una per service1 e uno per service2 all'interno di ciascuno dei CompletionEvents impostare alcune variabili di trigger su "true" e poi nel thread principale attesa per entrambi variabili di trigger da impostare. Puoi farlo con ResetEvents, ma se il tuo server sarà sotto carico potresti voler evitare questo.

pseudo codice del processo potrebbe essere:

Dictionary<string, bool> callCompleted = new Dictionary<string, bool> 

string operation1Key = Guid.NewGuid().ToString(); 
string operation2Key = Guid.NewGuid().ToString(); 

callCompleted[operation1Key] = false; 
callCompleted[operation2Key] = false; 

//make your remote calls and pass the operation key as the data 
//.... 
//.... 

bool waiting = true; 
while (waiting) { 
    bool isFinished = true; 
    foreach(string operationKey in callCompleted.Keys) { 
     isFinished &= callCompleted[operationKey] 
     if (!isFinished) { break; } 
    } 

    waiting = !isFinished; 
} 

E 'un po' rought poiché non so l'esatta natura di come si stanno facendo le chiamate, ma dovrebbe funzionare ragionevolmente bene.

+0

Hmmm, penso che lo spin-wait che si ha nello pseudo-codice (while (true)) sia adatto solo per tempi di attesa molto brevi. Penso che tu voglia usare una sorta di semaforo invece. – Mike

+0

Ha detto che questa era l'app ASP.NET MVC, quindi supponevo che i tempi di attesa sarebbero generalmente piuttosto brevi. Quando dici semaforo non sono sicuro di quale contesto ti riferisci a come un semaforo funzionerebbe esattamente come uno spin-lock (senza il tempo CPU che può essere risolto nel codice rapido sopra) e richiede ancora un ciclo simile a quello sopra per rilasciare il blocco dato che abbiamo a che fare con più thread. – GrayWizardx

1

Mi piace fare questo genere di cose un po 'più manualmente, piuttosto che fare affidamento su richieste web asincrone o sul dimensionamento automatico del pool di thread (25 thread per impostazione predefinita). Naturalmente, questi sono i modi perfetti per risolvere il tuo problema, ma penso che il seguente codice sia un po 'più leggibile (nell'esempio seguente, _links conterrà un elenco dei tuoi collegamenti prima che l'elaborazione avvenga ...):

private static IList<String> _links = new List<String>(); 
private const int NumberOfThreads = 2; 

public void SpawnWebRequests() 
{ 
    IList<Thread> threadList = new List<Thread>(); 

    for (int i = 0; i < NumberOfThreads; i++) 
    { 
     var thread = new Thread(ProcessWebRequests); 
     threadList.Add(thread); 
     thread.Start(); 
    } 

    for (int i = 0; i < NumberOfThreads; i++) 
    { 
     threadList[i].Join(); 
    } 
} 

private static void ProcessWebRequests() 
{ 
    String link; 

    while (true) 
    { 
     lock(_links) 
     { 
      if (_links.Count == 0) 
       break; 

      link = _links.RemoveAt(0); 
     } 

     ProcessWebRequest(link); 
    } 
} 

private static void ProcessWebRequest(String link) 
{ 
    try 
    { 
     var request = (HttpWebRequest)WebRequest.Create(link); 
     request.Method = "HEAD"; // or "GET", since some sites (Amazon) don't allow HEAD 
     request.Timeout = DefaultTimeoutSeconds * 1000; 

     // Get the response (throws an exception if status != 200) 
     using (var response = (HttpWebResponse)request.GetResponse()) 
     { 
      if (response.StatusCode == HttpStatusCode.OK) 
       Log.Debug("Working link: {0}", request.RequestUri); 
     } 
    } 
    catch (WebException ex) 
    { 
     var response = ((HttpWebResponse)ex.Response); 
     var status = response != null 
         ? response.StatusCode 
         : HttpStatusCode.RequestTimeout; 

     Log.WarnException(String.Format("Broken link ({0}): {1}", status, link), ex); 

     // Don't rethrow, as this is an expected exception in many cases 
    } 
    catch (Exception ex) 
    { 
     Log.ErrorException(String.Format("Error processing link {0}", link), ex); 

     // Rethrow, something went wrong 
     throw; 
    } 
} 

Se si desidera gestire la dimensione del pool di thread (se si utilizza ThreadPool.QueueUserWorkItem()), è possibile utilizzare ThreadPool.SetMaxThreads = 2).

Ovviamente, se si desidera utilizzare l'approccio asincronizzato sanzionato da Microsoft, consultare questo esempio: http://msdn.microsoft.com/en-us/library/86wf6409.aspx. Assicurati di ripulire ogni risposta (tramite un blocco "using" o chiudendo l'oggetto response)!

Speranza che aiuta, Noah

+0

All'interno del ciclo 'while (true)' è necessario avere un ritardo. Il ciclo come scritto mangerà fino al 100% della CPU quando nulla è nella lista. Raccomanderei di usare un 'AutoResetEvent' per aspettare e segnalare quando qualcosa è pronto per essere processato. –

0

vi consiglio di creare una classe lavoratrice che fa il HttpWebRequest e avviarlo nel proprio thread per ogni connessione. È sufficiente unire i thread e attendere fino a quando non terminano entrambi o passare un metodo di callback. In entrambi i casi è necessario tenere conto di errori di connessione, timeout e altre eccezioni. Preferisco utilizzare un callback che restituisce il risultato della connessione e il ManagedThreadId del thread, che utilizzo per tenere traccia dei thread. La classe worker dovrebbe catturare tutte le eccezioni in modo che tu possa gestirle nella classe chiamante.

Questo articolo offre alcune informazioni e correzioni per quando si supera il numero massimo di connessioni: http://support.microsoft.com/kb/821268.

Problemi correlati