Поддержка многопоточности и пул соединений в почтовых клиентах
почтовыми клиентами, такими как ImapClient, Pop3Client, and SmtpClient может использоваться в многопоточном окружении. Клиент может поддерживать одно или несколько соединений с сервером. Для управления набором соединений внутри клиента используется пул соединений. Количество соединений, которое может быть создано и использовано одновременно, ограничено CredentialsByHostClient.MaxConnectionsPerServer свойство. Это свойство может быть установлено в 1 или большее значение. По умолчанию оно равно 10.
Для каждой соединения реализована очередь команд, поддерживающая многопоточные операции. Команды реализуют простейшие операции, определённые в протоколе, такие как Noop, Authenticate, и так далее. Пользователь может запустить выполнение большего количества команд, чем доступно соединений, но они будут выполнены только когда клиент сможет создать соединение для операции.
Как почтовые клиенты ведут себя в многопоточном окружении
Почтовые клиенты имеют следующее поведение:
-
Когда
MaxConnectionsPerServer = 1, клиент создаёт одно соединение и выполняет аутентификацию и авторизацию. Это соединение удерживается в рабочем состоянии до освобождения клиента. Все операции из разных потоков направляются в одну очередь команд, размещённую в основном соединении. -
Когда
MaxConnectionsPerServer > 1, клиент создаёт требуемое количество соединений и выполняет аутентификацию и авторизацию для каждого соединения. Одно соединение резервируется как основное. Это соединение удерживается в рабочем состоянии до освобождения клиента. Все остальные соединения создаются и освобождаются по мере необходимости. Максимальное количество таких соединений определяетсяMaxConnectionsPerServerсвойства. Например, еслиMaxConnectionsPerServer = 2, тогда одно соединение резервируется как основное, а второе соединение используется как дополнительное для операций, выполняемых в других потоках. Соответственно, еслиMaxConnectionsPerServer = 3, тогда первое соединение резервируется как основное, а две другие соединения используются как дополнительные для операций, выполняемых в других потоках. Когда запрос соединения приходит из нового потока и все соединения уже заняты, клиент ждёт, пока количество используемых соединений не уменьшится. Это очень важный момент, который проясняет, почему правильное освобождение соединений так важно.
Примеры использования почтовых клиентов в многопоточном окружении
Пользователь может выполнять операции в разных потоках несколькими способами. Их можно разделить на два типа.
Использование асинхронных (Begin/End) методов
Пользователь использует асинхронные (Begin/End) методы, определённые в клиенте. В этом случае клиент почты создает новые потоки по мере необходимости. В клиенте реализована очередь задач (не путайте её с очередью команд в соединении). Задача может быть выполнена, если доступно соединение. Как только количество используемых соединений становится меньше предельного значения, клиент создаёт новое соединение, создаёт поток для текущей задачи и выполняет её. Пример использования асинхронных операций:
// 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);
Использование пользовательских потоков
Пользователь может создавать потоки, используя объекты, такие как Thread, ThreadPool, Task, или любые другие объекты, предназначенные для этой цели. Пользователь также может использовать потоки, созданные в стороннем коде. В этом случае у клиента есть две модели поведения.
a. Если пользователь не позаботился о создании дополнительных соединений для операций в потоке, все операции этого потока будут отправлены в очередь команд основного соединения. Ниже пример операций в дополнительном потоке без создания нового соединения — все транзакции выполняются через основное соединение:
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. Когда пользователь вызывает метод создания нового соединения для дополнительного потока, этот поток блокируется, пока значение квоты для новых соединений не изменится, позволяя создать новое соединение. Затем создаётся новое соединение, которое назначается как стандартное для всех операций в этом потоке. После завершения всех операций в потоке соединение должно быть освобождено. Чтобы создавать новые соединения, используйте CredentialsByHostClient.CreateConnection метод. Этот метод возвращает объект, реализующий IDisposable интерфейс. Чтобы освободить соединение, используется Dispose метод должен быть вызван. Создание и освобождение соединения должно выполняться внутри того потока, где выполняются почтовые операции. Попытка создать новое соединение в потоке, где был создан клиент почты, приводит к ошибке, потому что этот поток в данный момент не может создавать новые соединения. Создать новое соединение также нельзя, когда MaxConnectionsPerServer = 1. Пример кода создания нового соединения в дополнительном потоке:
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));
}
});
Пул соединений
Начиная с Aspose.Email 19.3, пул соединений был переработан. EmailClient был введён класс, который в конце концов заменяет CredentialsByHostClient класс. The EmailClient класс предоставляет ConnectionAsgmtMode свойства, которое определяет режим распределения соединений в многопоточной среде. EmailClient.ConnectionAsgmtMode устанавливается с помощью ConnectionAsgmtType перечисление.
Типы соединений
Существует три типа соединений:
- Основное соединение. Это соединение создаётся и освобождается вместе с клиентом почты. Оно не может быть создано или освобождено вручную.
- Стандартное соединение. Пользователь может создавать стандартные соединения для потоков с помощью CreateConnection метода. Если существует стандартное соединение, все методы клиента почты, выполненные в потоке, будут неявно использовать это соединение. На один поток может существовать только одно стандартное соединение. Оно может быть создано вручную или автоматически, в зависимости от
EmailClient.ConnectionAsgmtModeсвойства. Такие соединения можно создавать вручную с помощьюEmailClient.CreateConnection(createAsDefaultConnection = true)метода. Если стандартное соединение не используется (зависит от режима выделения соединения), вместо него неявно используется основное соединение. - Независимые соединения. Это соединения, которые не привязаны к потокам. Их можно создавать вручную и они должны использоваться явно как параметр метода. Такие соединения можно создавать вручную с помощью
EmailClient.CreateConnection()метода илиEmailClient.CreateConnection(createAsDefaultConnection = false)метод.
Типы распределения соединений
Чтобы настроить EmailClient.ConnectionAsgmtMode свойства, ConnectionAsgmtType перечисления. Предоставляемые типы выделения перечислены ниже.
-
ConnectionAsgmtType.UseMainOrDefault Этот режим используется по умолчанию в почтовых клиентах. Почтовый клиент использует основное соединение для всех операций из нескольких потоков, если стандартное соединение не было создано или если соединение не было передано явно как параметр метода. Основное соединение создаётся одновременно с клиентом почты. Пользователь может создавать стандартные соединения для потоков с помощью
CreateConnectionметод. Если для потока создано стандартное соединение, оно неявно используется для всех методов клиента почты, вызываемых в этом потоке. Если для потока стандартное соединение не создано, для всех методов, вызываемых в этом потоке, используется основное соединение. Пользователь также может создавать соединения, не привязанные к потокам (не стандартные соединения), с помощьюCreateConnectionметода. Чтобы использовать другие соединения (не основные и не стандартные), пользователь должен передать соединение явно как параметр метода. Пользователь также может дополнительно создавать любое количество соединений. На один поток может существовать только одно стандартное соединение. Обратите внимание, что стандартные соединения работают корректно, если пользователь используетThreadобъекты для программирования многозадачности. Если пользователь использует пул соединений илиTaskобъекты для многозадачности, этот режим может привести к некорректному поведению. Чтобы избежать этой проблемы, пользователь должен вручную освобождать стандартное соединение (если оно используется) в конце выполнения кода. -
ConnectionAsgmtType.UseMain Почтовый клиент использует основное соединение для всех операций из нескольких потоков. Основное соединение создаётся одновременно с клиентом почты. Пользователь не может создавать стандартные соединения, но может создавать соединения, не привязанные к потокам, с помощью
CreateConnectionметода. Чтобы использовать другие соединения, пользователь должен передать их явно как параметр метода. -
ConnectionAsgmtType.UseDefault Почтовый клиент использует только стандартные соединения неявно для всех операций из нескольких потоков. Основное соединение в этом режиме не используется. Если для потока не было создано стандартное соединение (при первом вызове метода клиента почты), клиент неявно создаёт стандартное соединение для потока перед выполнением первой операции. Пользователь не может создавать стандартные соединения для потоков с помощью
CreateConnectionметод, потому что они создаются автоматически. Пользователь также может создавать соединения, не привязанные к потокам, с помощьюCreateConnectionметод. Чтобы использовать другие соединения, пользователь должен передать их явно как параметр метода. Пользователь также может дополнительно создавать любое количество соединений. На один поток может быть использовано только одно стандартное соединение. Обратите внимание, что стандартные соединения работают корректно, если пользователь используетThreadобъекты для программирования многозадачности. Если пользователь использует пул соединений илиTaskобъекты для многозадачности, этот режим может привести к некорректному поведению. Чтобы избежать этой проблемы, пользователь должен вручную освобождать стандартное соединение в конце выполнения кода.
Рекомендации
Если пользователь отправляет все команды через основное соединение, может возникнуть ситуация, когда команды из разных потоков混混混混混混混混混混混混混混. Пользователь должен понять, какие команды зависят от их последовательности, и принять меры по синхронизации таких команд. Также необходимо учитывать возможность выполнения команд в разных сеансах (IMAP/POP3). Самыми ресурсоемкими операциями являются FetchMessage, AppendMessage, and Send. Возможно, имеет смысл выполнять эти операции в новом потоке и с новым соединением. Быстрые операции, такие как Delete имеет смысл выполнять с основной соединением. Обратите внимание, что инициализация нового соединения — достаточно затратная операция.