Suporte a multithreading em clientes de e‑mail

Clientes de e‑mail como ImapClient e Pop3Client permitem que os usuários os utilizem em ambiente multithread. Os clientes de e‑mail permitem ter uma ou mais conexões com um servidor. Para gerenciar um conjunto de conexões dentro dos clientes é usado um pool de conexões. A quantidade de conexões que podem ser criadas e usadas simultaneamente é limitada pela propriedade CredentialsByHostClient.MaxConnectionsPerServer. Essa propriedade pode ser definida como 1 ou um valor maior. Por padrão, essa propriedade tem o valor 10. Uma fila de comandos foi implementada para a conexão, a fim de suportar operações multithread. Os comandos implementam as operações mais simples definidas no protocolo, como Noop, Authenticate e afins. O usuário pode iniciar a execução de um número de comandos maior que a quantidade de conexões disponíveis. Porém, eles só serão executados quando o cliente puder criar uma conexão para a operação.

Métodos de uso de clientes de e‑mail em ambiente multi‑thread

Os clientes de e‑mail têm o seguinte comportamento:

1. No caso de MaxConnectionsPerServer = 1, o cliente cria 1 conexão, realiza autenticação e autorização. Essa conexão permanece em estado de funcionamento até que o cliente seja descartado. Todas as operações de diferentes threads são direcionadas para uma única fila de comandos colocada na conexão principal.

2. No caso de MaxConnectionsPerServer > 1, o cliente cria a quantidade necessária de conexões, realiza autenticação e autorização para cada conexão. Uma conexão é reservada como conexão principal. Essa conexão permanece em estado de funcionamento até que o cliente seja descartado. Todas as outras conexões são criadas e descartadas sob demanda. A quantidade máxima de tais conexões é definida pela propriedade MaxConnectionsPerServer, ou seja, por exemplo, se MaxConnectionsPerServer = 2, uma é reservada como conexão principal e a segunda é usada como adicional para operações executadas em outras threads. Da mesma forma, se MaxConnectionsPerServer = 3, a primeira conexão é reservada como principal e duas outras são usadas como adicionais para operações em outras threads. Caso chegue uma nova solicitação de conexão de uma thread nova e todas as conexões já estejam em uso, o cliente aguarda até que o número de conexões usadas seja reduzido. Este é um ponto muito importante que esclarece por que o descarte correto das conexões é tão fundamental.

Exemplos

O usuário pode executar operações em diferentes threads de várias maneiras. Elas podem ser divididas em dois tipos:

1. O usuário utiliza métodos assíncronos (Begin/End) definidos no cliente. Nesse caso, o cliente de e‑mail lança novas threads quando necessário. No cliente há uma fila de tarefas (por favor, não confunda com a fila de comandos na conexão). Uma tarefa pode ser executada se houver conexão disponível. Quando a quantidade de conexões usadas se torna menor que o valor limite, o cliente cria uma nova conexão, cria uma thread para a tarefa atual e executa-a. Exemplo de uso de operações assíncronas:

2. O usuário pode criar threads usando objetos como Thread, ThreadPool, Task ou quaisquer outros objetos destinados a esse fim. Também pode usar threads criadas em código de terceiros. Durante isso, o cliente possui dois modelos de comportamento

a. Se o usuário não criou 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. 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:

b. Quando o usuário executa o método para criar uma nova conexão para uma thread adicional, essa thread fica bloqueada até que o valor de cota para novas conexões seja alterado, permitindo a nova conexão. Em seguida, a nova conexão é criada. Essa conexão é definida como conexão padrão para todas as operações nessa thread. Após a conclusão de todas as operações nessa thread, a conexão deve ser descartada. Para criar novas conexões, use o método CredentialsByHostClient.CreateConnection. Esse método retorna um objeto que implementa a interface IDisposable. Para liberar a conexão, o método Dispose deve ser invocado. A criação e o descarte da conexão devem ser executados dentro da thread onde as operações de e‑mail são executadas. Tentar criar uma nova conexão na thread onde o cliente de e‑mail foi criado resulta em erro, pois essa thread no momento não pode ser usada para criar nova conexão. Além disso, a criação de nova conexão não é possível quando MaxConnectionsPerServer = 1. Um exemplo de código para criar uma nova conexão em thread adicional:

Recomendações

Vale ressaltar que, se o usuário enviar todos os comandos para a conexão principal, pode ocorrer uma situação em que comandos de diferentes threads se misturem. O usuário deve entender quais comandos dependem de 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 que consomem mais tempo, como FetchMessage, AppendMessage e Send, provavelmente fazem sentido ser executadas em uma nova thread e nova conexão. Mas operações rápidas, como Delete, fazem sentido ser realizadas na conexão principal. Observe que a inicialização de uma nova conexão é uma operação suficientemente demorada.