Soporte de multihilo y pool de conexiones en clientes de correo

Clientes de correo como ImapClient, Pop3Client, y SmtpClient puede usarse en un entorno multihilo. Un cliente puede mantener una o más conexiones con un servidor. Para gestionar el conjunto de conexiones dentro de un cliente, se usa un pool de conexiones. El número de conexiones que pueden crearse y usarse al mismo tiempo está limitado por el CredentialsByHostClient.MaxConnectionsPerServer propiedad. Esta propiedad puede establecerse en 1 o en un valor mayor. Por defecto, es igual a 10.

Se implementa una cola de comandos para cada conexión para soportar operaciones multihilo. Los comandos implementan las operaciones más simples definidas en el protocolo, como Noop, Authenticate, y así sucesivamente. Un usuario puede iniciar la ejecución de más comandos de los que hay conexiones disponibles, pero solo se ejecutarán cuando el cliente pueda crear una conexión para la operación.

Cómo se Comportan los Clientes de Correo en un Entorno Multihilo

Los clientes de correo tienen el siguiente comportamiento:

  1. Cuando MaxConnectionsPerServer = 1, el cliente crea una conexión y realiza autenticación y autorización. Esta conexión se mantiene en estado activo hasta que se elimina el cliente. Todas las operaciones de diferentes hilos se dirigen a una única cola de comandos ubicada en la conexión principal.

  2. Cuando MaxConnectionsPerServer > 1, el cliente crea el número requerido de conexiones y realiza autenticación y autorización para cada conexión. Una conexión se reserva como la principal. Esta conexión se mantiene en estado activo hasta que se elimina el cliente. Todas las demás conexiones se crean y eliminan bajo demanda. El número máximo de dichas conexiones lo define el MaxConnectionsPerServer propiedad. Por ejemplo, si MaxConnectionsPerServer = 2, entonces una conexión se reserva como la principal, y una segunda conexión se usa como adicional para operaciones ejecutadas en otros hilos. En consecuencia, si MaxConnectionsPerServer = 3, entonces la primera conexión se reserva como la conexión principal, y otras dos conexiones se usan como adicionales para operaciones ejecutadas en otros hilos. Cuando una solicitud de conexión proviene de un nuevo hilo y todas las conexiones ya están en uso, el cliente espera hasta que disminuya el número de conexiones usadas. Este es un momento muy importante que aclara por qué la eliminación correcta de conexiones es tan importante.

Ejemplos de Uso de Clientes de Correo en un Entorno Multihilo

Un usuario puede ejecutar operaciones en diferentes hilos de varias maneras. Pueden dividirse en dos tipos.

Usando Métodos Asincrónicos (Begin/End)

Un usuario usa los métodos asincrónicos (Begin/End) definidos en el cliente. En este caso, el cliente de correo lanza nuevos hilos cuando es necesario. Se implementa una cola de tareas en el cliente (no confundir con la cola de comandos en la conexión). Una tarea puede ejecutarse si hay una conexión disponible. Una vez que el número de conexiones usadas es menor que el valor límite, el cliente crea una nueva conexión, crea un hilo para la tarea actual y ejecuta dicha tarea. Un ejemplo de uso de operaciones asincrónicas:

// Create an imapclient with host, user and password
ImapClient client = new ImapClient();
client.Host = "domain.com";
client.Username = "username";
client.Password = "password";
client.SelectFolder("InBox");

ImapMessageInfoCollection messages = client.ListMessages();
IAsyncResult res1 = client.BeginFetchMessage(messages[0].UniqueId);
IAsyncResult res2 = client.BeginFetchMessage(messages[1].UniqueId);
MailMessage msg1 = client.EndFetchMessage(res1);
MailMessage msg2 = client.EndFetchMessage(res2);

Usando Hilos Creados por el Usuario

Un usuario puede crear hilos usando objetos como Thread, ThreadPool, Task, o cualquier otro objeto destinado a este fin. Un usuario también puede usar hilos creados en código de terceros. En este caso, el cliente tiene dos modelos de comportamiento.

a. Si el usuario no se ha ocupado de crear conexiones adicionales para operaciones en el hilo, todas las operaciones de ese hilo se enviarán a la cola de comandos de la conexión principal. A continuación se muestra un ejemplo de operaciones en un hilo adicional sin crear una nueva conexión — todas las transacciones se realizan a través de la conexión principal:

List<MailMessage> List = new List<MailMessage>();
ThreadPool.QueueUserWorkItem(delegate(object o)
{
    client.SelectFolder("folderName");
    ImapMessageInfoCollection messageInfoCol = client.ListMessages();
    foreach (ImapMessageInfo messageInfo in messageInfoCol)
    {
        List.Add(client.FetchMessage(messageInfo.UniqueId));
    }
});

b. Cuando el usuario ejecuta un método para crear una nueva conexión para un hilo adicional, ese hilo queda bloqueado hasta que el valor de cuota para nuevas conexiones cambie y permita una nueva conexión. Entonces se crea una nueva conexión. Esta conexión se establece como la conexión predeterminada para todas las operaciones en este hilo. Tras completarse todas las operaciones en este hilo, la conexión debe disponerse. Para crear nuevas conexiones, use el CredentialsByHostClient.CreateConnection método. Este método devuelve un objeto que implementa el IDisposable interfaz. Para liberar la conexión, el Dispose el método debe invocarse. Crear y disponer de una conexión debe ejecutarse dentro del hilo donde se ejecutan las operaciones de correo. Intentar crear una nueva conexión en el hilo donde se creó el cliente de correo genera un error, porque ese hilo no puede crear una nueva conexión en ese momento. También no es posible crear una nueva conexión cuando MaxConnectionsPerServer = 1. Un ejemplo de código para crear una nueva conexión en un hilo adicional:

List<MailMessage> List1 = new List<MailMessage>();
ThreadPool.QueueUserWorkItem(delegate(object o)
{
    using (IDisposable connection = client.CreateConnection())
    {
        client.SelectFolder("FolderName");
        ImapMessageInfoCollection messageInfoCol = client.ListMessages();
        foreach (ImapMessageInfo messageInfo in messageInfoCol)
            List1.Add(client.FetchMessage(messageInfo.UniqueId));
    }
});

Pool de Conexiones

A partir de Aspose.Email 19.3, el pool de conexiones se reestructuró. El EmailClient se introdujo una clase, que eventualmente reemplaza al CredentialsByHostClient clase. El EmailClient clase proporciona un ConnectionAsgmtMode propiedad que define el modo de asignación de conexión en un entorno multihilo. EmailClient.ConnectionAsgmtMode se establece usando el ConnectionAsgmtType enumeración.

Tipos de Conexión

Hay tres tipos de conexión:

  • La conexión principal. Es la conexión creada y eliminada junto con el cliente de correo. No puede crearse ni eliminarse manualmente.
  • Conexión predeterminada. Un usuario puede crear conexiones predeterminadas para hilos con el CreateConnection método. Si existe una conexión predeterminada, todos los métodos del cliente de correo ejecutados en un hilo usarán implícitamente esa conexión. Solo puede existir una conexión predeterminada por hilo. Puede crearse manualmente o automáticamente, según el EmailClient.ConnectionAsgmtMode propiedad. Estas conexiones pueden crearse manualmente con el EmailClient.CreateConnection(createAsDefaultConnection = true) método. Si no se usa una conexión predeterminada (depende del modo de asignación de conexión), se usa implícitamente la conexión principal.
  • Conexiones independientes. Son conexiones que no están vinculadas a hilos. Pueden crearse manualmente y deben usarse explícitamente como parámetro del método. Estas conexiones pueden crearse manualmente con el EmailClient.CreateConnection() método o el EmailClient.CreateConnection(createAsDefaultConnection = false) método.

Tipos de asignación de conexión

Para configurar el EmailClient.ConnectionAsgmtMode propiedad, el ConnectionAsgmtType se utiliza la enumeración. Los tipos de asignación que proporciona se enumeran a continuación.

  • ConnectionAsgmtType.UseMainOrDefault Este modo se usa por defecto en los clientes de correo. El cliente de correo usa la conexión principal para todas las operaciones desde varios hilos si no se ha creado una conexión predeterminada, o si no se ha pasado una conexión como parámetro del método explícitamente. La conexión principal se crea al mismo tiempo que el cliente de correo. El usuario puede crear conexiones predeterminadas para hilos con el CreateConnection método. Si se crea una conexión predeterminada para un hilo, se usa implícitamente para todos los métodos del cliente de correo invocados en ese hilo. Si no se crea una conexión predeterminada para un hilo, se usa la conexión principal para todos los métodos invocados en ese hilo. El usuario también puede crear conexiones no vinculadas a hilos (no predeterminadas) con el CreateConnection método. Para usar otras conexiones (no principales y no predeterminadas), el usuario debe pasar la conexión explícitamente como parámetro del método. El usuario también puede crear cualquier número de conexiones. Solo puede existir una conexión predeterminada por hilo. Tenga en cuenta que las conexiones predeterminadas funcionan correctamente si el usuario usa Thread objetos para programación multitarea. Si el usuario usa un pool de conexiones o Task objetos para multitarea, este modo puede conducir a un comportamiento incorrecto. Para evitar este problema, el usuario debe disponer manualmente de la conexión predeterminada (si se usa) al final de la ejecución del código.

  • ConnectionAsgmtType.UseMain El cliente de correo usa la conexión principal para todas las operaciones desde varios hilos. La conexión principal se crea al mismo tiempo que el cliente de correo. El usuario no puede crear conexiones predeterminadas, pero puede crear conexiones no vinculadas a hilos con el CreateConnection método. Para usar otras conexiones, el usuario debe pasarlas explícitamente como parámetro del método.

  • ConnectionAsgmtType.UseDefault El cliente de correo usa solo conexiones predeterminadas implícitamente para todas las operaciones desde varios hilos. La conexión principal no se usa en este modo. Si no se ha creado una conexión predeterminada para un hilo (en la primera invocación de un método del cliente de correo), el cliente de correo crea una conexión predeterminada implícitamente para el hilo antes de ejecutar la primera operación. El usuario no puede crear conexiones predeterminadas para hilos con el CreateConnection método porque se crean automáticamente. El usuario también puede crear conexiones no vinculadas a hilos con el CreateConnection método. Para usar otras conexiones, el usuario debe pasarlas explícitamente como parámetro del método. El usuario también puede crear cualquier número de conexiones. Solo se puede usar una conexión predeterminada por hilo. Tenga en cuenta que las conexiones predeterminadas funcionan correctamente si el usuario usa Thread objetos para programación multitarea. Si el usuario usa un pool de conexiones o Task objetos para multitarea, este modo puede conducir a un comportamiento incorrecto. Para evitar este problema, el usuario debe disponer manualmente de la conexión predeterminada al final de la ejecución del código.

Recomendaciones

Si el usuario envía todos los comandos a la conexión principal, puede surgir una situación en la que los comandos de diferentes hilos se mezclen. El usuario debe entender qué comandos dependen de su secuencia y tomar medidas para sincronizar dichos comandos. También es necesario considerar la posibilidad de ejecutar comandos en diferentes sesiones (IMAP/POP3). Las operaciones que más tiempo consumen son FetchMessage, AppendMessage, y Send. Probablemente tenga sentido realizar estas operaciones con un nuevo hilo y una nueva conexión. Operaciones rápidas como Delete tiene sentido ejecutarlo con la conexión principal. Tenga en cuenta que la inicialización de una nueva conexión es una operación bastante costosa en tiempo.