Suporte a Multithreading e Pool de Conexões em Clientes de E‑mail
Clientes de e‑mail como ImapClient, Pop3Client, e SmtpClient pode ser usado em um ambiente multithread. Um cliente pode manter uma ou mais conexões com um servidor. Para gerenciar o conjunto de conexões dentro de um cliente, usa‑se um pool de conexões. O número de conexões que podem ser criadas e usadas ao mesmo tempo é limitado pelo CredentialsByHostClient.MaxConnectionsPerServer propriedade. Esta propriedade pode ser definida como 1 ou um valor maior. Por padrão, é igual a 10.
Uma fila de comandos é implementada para cada conexão para suportar operações multithread. Comandos implementam as operações mais simples definidas no protocolo, como Noop, Authenticate, e assim por diante. Um usuário pode iniciar a execução de mais comandos do que há conexões disponíveis, mas eles só serão executados quando o cliente puder criar uma conexão para a operação.
Como os Clientes de E‑mail Se Comportam em Ambiente Multithread
Os clientes de e‑mail têm o seguinte comportamento:
-
Quando
MaxConnectionsPerServer = 1, o cliente cria uma conexão e realiza autenticação e autorização. Essa conexão permanece em estado ativo até que o cliente seja descartado. Todas as operações de diferentes threads são direcionadas a uma fila de comandos localizada na conexão principal. -
Quando
MaxConnectionsPerServer > 1, o cliente cria o número necessário de conexões e realiza autenticação e autorização para cada conexão. Uma conexão é reservada como a conexão principal. Essa conexão permanece em estado ativo até que o cliente seja descartado. Todas as demais conexões são criadas e descartadas sob demanda. O número máximo de tais conexões é definido peloMaxConnectionsPerServerpropriedade. Por exemplo, seMaxConnectionsPerServer = 2, então uma conexão é reservada como a conexão principal, e uma segunda conexão é usada como adicional para operações executadas em outras threads. Consequentemente, seMaxConnectionsPerServer = 3, então a primeira conexão é reservada como a conexão principal, e duas outras conexões são usadas como adicionais para operações executadas em outras threads. Quando uma solicitação de conexão vem de uma nova thread e todas as conexões já estão em uso, o cliente aguarda até que o número de conexões usadas diminua. Este é um momento muito importante que esclarece por que descartar corretamente as conexões é tão importante.
Exemplos de Uso de Clientes de E‑mail em Ambiente Multithread
Um usuário pode executar operações em diferentes threads de várias maneiras. Elas podem ser divididas em dois tipos.
Usando Métodos Assíncronos (Begin/End)
Um usuário usa os métodos assíncronos (Begin/End) definidos no cliente. Nesse caso, o cliente de e‑mail lança novas threads quando necessário. Uma fila de tarefas é implementada no cliente (não confundir com a fila de comandos na conexão). Uma tarefa pode ser executada se houver uma conexão disponível. Quando o número de conexões usadas ficar abaixo do valor limite, o cliente cria uma nova conexão, cria uma thread para a tarefa atual e executa essa tarefa. Um exemplo de uso de operações assíncronas:
// 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 Threads Criadas pelo Usuário
Um usuário pode criar threads usando objetos como Thread, ThreadPool, Task, ou quaisquer outros objetos destinados a esse fim. Um usuário pode também usar threads criadas em código de terceiros. Nesse caso, o cliente tem dois modelos de comportamento.
a. Se o usuário não cuidou de criar conexões adicionais para operações na thread, todas as operações dessa thread serão enviadas para a fila de comandos da conexão principal. A seguir, um exemplo de operações em uma thread adicional sem criar uma nova conexão — todas as transações são feitas via conexão 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. Quando o usuário executa um método para criar uma nova conexão para uma thread adicional, essa thread é bloqueada até que o valor de cota para novas conexões mude e permita uma nova conexão. Então, uma nova conexão é criada. Essa conexão é definida como a conexão padrão para todas as operações nessa thread. Após todas as operações nessa thread serem concluídas, a conexão deve ser descartada. Para criar novas conexões, use o CredentialsByHostClient.CreateConnection método. Este método retorna um objeto que implementa o IDisposable interface. Para liberar a conexão, o Dispose método deve ser invocado. Criar e descartar uma conexão deve ser executado dentro da thread onde as operações de e‑mail são realizadas. Uma tentativa de criar uma nova conexão na thread onde o cliente de e‑mail foi criado leva a um erro, pois essa thread não pode ser usada para criar uma nova conexão naquele momento. Criar uma nova conexão também não é possível quando MaxConnectionsPerServer = 1. Um exemplo de código criando uma nova conexão em uma thread 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 Conexões
A partir do Aspose.Email 19.3, o pool de conexões foi refatorado. O EmailClient classe foi introduzida, que eventualmente substitui o CredentialsByHostClient classe. O EmailClient classe fornece um ConnectionAsgmtMode propriedade que define o modo de alocação de conexão em um ambiente multithread. EmailClient.ConnectionAsgmtMode é definido usando o ConnectionAsgmtType enumeração.
Tipos de Conexão
Existem três tipos de conexão:
- A conexão principal. Esta é a conexão criada e descartada juntamente com o cliente de e‑mail. Não pode ser criada ou descartada manualmente.
- Conexão padrão. Um usuário pode criar conexões padrão para threads com o CreateConnection método. Se existir uma conexão padrão, todos os métodos do cliente de e‑mail executados em uma thread usarão implicitamente essa conexão. Apenas uma conexão padrão pode existir por thread. Ela pode ser criada manualmente ou automaticamente, dependendo do
EmailClient.ConnectionAsgmtModepropriedade. Essas conexões podem ser criadas manualmente com oEmailClient.CreateConnection(createAsDefaultConnection = true)método. Se uma conexão padrão não for usada (depende do modo de alocação de conexão), a conexão principal será usada implicitamente em seu lugar. - Conexões independentes. São conexões que não estão vinculadas a threads. Elas podem ser criadas manualmente e devem ser usadas explicitamente como parâmetro do método. Essas conexões podem ser criadas manualmente com o
EmailClient.CreateConnection()método ou oEmailClient.CreateConnection(createAsDefaultConnection = false)método.
Tipos de Alocação de Conexão
Para configurar o EmailClient.ConnectionAsgmtMode propriedade, o ConnectionAsgmtType enumeração é usada. Os tipos de alocação que ela fornece são listados abaixo.
-
ConnectionAsgmtType.UseMainOrDefault Este modo é usado por padrão em clientes de e‑mail. O cliente de e‑mail usa a conexão principal para todas as operações de múltiplas threads se uma conexão padrão não tiver sido criada, ou se uma conexão não tiver sido passada como parâmetro do método explicitamente. A conexão principal é criada ao mesmo tempo que o cliente de e‑mail. O usuário pode criar conexões padrão para threads com o
CreateConnectionmétodo. Se uma conexão padrão para uma thread for criada, ela é usada implicitamente por todos os métodos do cliente de e‑mail invocados naquela thread. Se uma conexão padrão para uma thread não for criada, a conexão principal é usada por todos os métodos invocados nessa thread. O usuário também pode criar conexões não vinculadas a threads (não padrão) com oCreateConnectionmétodo. Para usar outras conexões (não principais e não padrão), o usuário deve passar a conexão explicitamente como parâmetro do método. O usuário pode ainda criar quantas conexões desejar. Apenas uma conexão padrão pode existir por thread. Observe que conexões padrão funcionam corretamente se o usuário usarThreadobjetos para programação de multitarefa. Se o usuário usar um pool de conexões ouTaskobjetos para multitarefa, esse modo pode levar a um comportamento incorreto. Para evitar esse problema, o usuário deve descartar manualmente a conexão padrão (se for usada) ao final da execução do código. -
ConnectionAsgmtType.UseMain O cliente de e‑mail usa a conexão principal para todas as operações de múltiplas threads. A conexão principal é criada ao mesmo tempo que o cliente de e‑mail. O usuário não pode criar conexões padrão, mas pode criar conexões não vinculadas a threads com o
CreateConnectionmétodo. Para usar outras conexões, o usuário deve passá‑las explicitamente como parâmetro do método. -
ConnectionAsgmtType.UseDefault O cliente de e‑mail usa apenas conexões padrão implicitamente para todas as operações de múltiplas threads. A conexão principal não é usada neste modo. Se uma conexão padrão não tiver sido criada para uma thread (na primeira invocação de um método do cliente de e‑mail), o cliente de e‑mail cria uma conexão padrão implicitamente para a thread antes da primeira operação ser executada. O usuário não pode criar conexões padrão para threads com o
CreateConnectionmétodo porque são criadas automaticamente. O usuário também pode criar conexões não vinculadas a threads com oCreateConnectionmétodo. Para usar outras conexões, o usuário deve passá‑las explicitamente como parâmetro do método. O usuário pode ainda criar quantas conexões desejar. Apenas uma conexão padrão pode ser usada por thread. Observe que conexões padrão funcionam corretamente se o usuário usarThreadobjetos para programação de multitarefa. Se o usuário usar um pool de conexões ouTaskobjetos para multitarefa, esse modo pode levar a um comportamento incorreto. Para evitar esse problema, o usuário deve descartar manualmente a conexão padrão ao final da execução do código.
Recomendações
Se o usuário enviar todos os comandos para a conexão principal, pode surgir uma situação em que comandos de diferentes threads se misturem. O usuário deve entender quais comandos dependem da sua sequência e tomar medidas para sincronizar tais comandos. Também é necessário considerar a possibilidade de executar comandos em sessões diferentes (IMAP/POP3). As operações mais demoradas são FetchMessage, AppendMessage, e Send. Provavelmente faz sentido executar essas operações com uma nova thread e uma nova conexão. Operações rápidas, como Delete faz sentido executar com a conexão principal. Observe que a inicialização de uma nova conexão é uma operação bastante demorada.