5

Tengo una aplicación web ASP.NET MVC que realiza llamadas al servicio web de estilo REST a otros servidores. Tengo un escenario en el que hago dos llamadas HttpWebRequest a dos servicios separados. Necesito que ambos completen para continuar, pero su orden no importa. Pueden tomar 1-2 segundos cada uno y los estoy ejecutando en secuencia ahora. Ejecutarlos en paralelo disminuiría el tiempo de respuesta del usuario, pero ¿cuál es la mejor manera?Recomendaciones para ejecutar .NET HttpWebRequests en paralelo en ASP.NET

En la investigación de esto, puedo pensar en varias opciones:

  • ejecutar una solicitud en el hilo principal, y hacer girar un segundo hilo de la otra petición. ¿Debería crear nuevos hilos o usar un grupo de hilos? Si utilizo un grupo, ¿cómo lo dimensiono? Además, no estoy seguro de cómo puedo unir los hilos nuevamente (por ejemplo, use ThreadPool.RegisterWaitForSingleObject)?
  • Pruebe y aproveche el soporte integrado IAsyncResult para una o ambas solicitudes. De nuevo, no estoy seguro de qué subprocesos ejecutar la solicitud asincrónica, por lo que no estoy seguro de cómo dimensionar el grupo de subprocesos. ¿Cómo vuelvo a unir IAsyncResult a mi hilo principal? Todos los ejemplos encuentro información de proceso en la devolución de llamada, pero puedo esperar en mi hilo principal y usar la propiedad IsCompleted.

Necesito encontrar una solución que funcione y funcione a escala. Es por eso que estoy preocupado por el tamaño del grupo de subprocesos. No me gustaría tener bloqueos de solicitudes porque están esperando los hilos disponibles.

Respuesta

1

Una de las respuestas a Multithreading WebRequests, a good and stable approach? : CSharp utiliza un ManualResetEvent event = new ManualResetEvent(), un contador de referencia que indica el número de solicitudes en vuelo y Interlocked.Decrement para controlar el event.Set(). El hilo principal luego espera llamando al event.WaitOne().

Sin embargo, WaitHandles - Auto/ManualResetEvent and Mutex menciona que ManualResetEvent "puede ser significativamente más lento que usar los diversos métodos de Monitor" como Esperar, Pulsar y Pulsar Todo.

Terminé basando mi código en esta entrada de blog de Noah Blumenthal: Run generic tasks async (fluent-ly). Realicé dos cambios: implemente IDisposable y llame al .Close() en el ManualResetEvent y cambie de usar un lock() a Interlocked.Increment() y .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 menos que esté haciendo algo elegante, el tamaño del ThreadPool es fijo y generalmente lo suficientemente grande como para satisfacer sus necesidades. En su caso particular, puede encontrarse con problemas de restricción de recursos usando llamadas AsyncIO si su servicio web está bajo una gran carga y cada llamante ha iniciado 2 subprocesos TP para sus propias devoluciones de llamada.

Tal vez la aplicación más fácil de esto sería tener dos funciones de devolución de llamada distintas, una para service1 y uno para service2 dentro de cada uno de los CompletionEvents establecer alguna variable de disparo a "true" y luego en su espera hilo principal para ambos desencadenar variables a establecer. Puede hacer esto con ResetEvents, pero si su servidor estará bajo carga, es posible que desee evitar esto.

pseudo código del proceso podría ser:

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

Es un poco rought ya que no sé la naturaleza exacta de cómo se están haciendo las llamadas, pero debería funcionar razonablemente bien.

+0

Hmmm, creo que el spin-wait que tiene en el pseudo-código (while (true)) solo es adecuado para tiempos de espera muy cortos. Creo que querrías usar algún tipo de semáforo en su lugar. – Mike

+0

Dijo que se trataba de la aplicación ASP.NET MVC, por lo que supongo que los tiempos de espera serían, en general, bastante cortos. Cuando dices semáforo, no estoy seguro de a qué contexto te refieres, ya que un semáforo funcionaría exactamente como un bloqueo de giro (sin tiempo de CPU que pueda remediarse en el código rápido anterior) y aún requiera un ciclo similar al anterior para liberar el bloqueo ya que estamos tratando con múltiples hilos. – GrayWizardx

1

Me gusta hacer este tipo de cosas un poco más manualmente, en lugar de depender de solicitudes web asíncronas o del tamaño automático del grupo de subprocesos (25 subprocesos de forma predeterminada). Por supuesto, esas son formas muy buenas de resolver su problema, pero creo que el siguiente código es un poco más legible (en el siguiente ejemplo, _links contendría una lista de sus enlaces antes de que ocurra el procesamiento ...):

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

Si solo desea administrar el tamaño del grupo de subprocesos (si está utilizando ThreadPool.QueueUserWorkItem()), puede usar ThreadPool.SetMaxThreads = 2).

Por supuesto, si desea utilizar el enfoque asincrónico sancionado por Microsoft, consulte este ejemplo: http://msdn.microsoft.com/en-us/library/86wf6409.aspx. ¡Solo asegúrese de limpiar cada respuesta (a través de un bloque "usando" o cerrando el objeto de respuesta)!

Espero que ayude, Noah

+0

Dentro del ciclo 'while (true)' necesita tener un retraso. El ciclo como está escrito consumirá 100% de CPU cuando no haya nada en la lista. Yo recomendaría usar un 'AutoResetEvent' para esperar y señalar cuando algo está listo para ser procesado. –

0

recomiendo que se crea una clase trabajadora que hace el HttpWebRequest y empezar en su propio hilo para cada conexión. Simplemente puede unirse a los hilos y esperar hasta que ambos terminen o pasen un método de devolución de llamada. En cualquier caso, debe tener en cuenta las fallas de conexión, los tiempos de espera y otras excepciones. Prefiero usar una devolución de llamada que devuelva el resultado de la conexión y el ManagedThreadId de la secuencia, que utilizo para realizar un seguimiento de los hilos. La clase de trabajador debe detectar todas las excepciones para que pueda manejarlas en la clase de llamada.

Este artículo ofrece algunas ideas y soluciones para cuando se supera la cantidad máxima de conexiones: http://support.microsoft.com/kb/821268.

Cuestiones relacionadas