Поддержка многопоточности в почтовых клиентах
Почтовые клиенты, такие как ImapClient и Pop3Client, позволяют использовать их в многопоточной среде. Клиенты могут иметь одно или несколько соединений с сервером. Для управления набором соединений внутри клиента используется пул соединений. Количество соединений, которое может быть создано и использовано одновременно, ограничивается свойством CredentialsByHostClient.MaxConnectionsPerServer. Это свойство может быть установлено в 1 или большее значение. По умолчанию оно равно 10. Для соединения реализована очередь команд, поддерживающая многопоточные операции. Команды реализуют простейшие операции, определённые в протоколе, такие как Noop, Authenticate и т.д. Пользователь может запустить выполнение большего количества команд, чем доступно соединений, но они будут выполнены только тогда, когда клиент сможет создать соединение для операции.
Методы использования почтовых клиентов в многопоточной среде
Почтовые клиенты имеют следующее поведение:
1. В случае, когда MaxConnectionsPerServer = 1, клиент создаёт 1 соединение, выполняет аутентификацию и авторизацию. Это соединение поддерживается в рабочем состоянии до момента, когда клиент будет уничтожен. Все операции из разных потоков направляются в одну очередь команд, расположенную в основном соединении.
2. В случае, когда MaxConnectionsPerServer > 1, клиент создает необходимое количество соединений, выполняет аутентификацию и авторизацию для каждого соединения. Одно соединение резервируется как основное. Это соединение поддерживается в рабочем состоянии до момента, когда клиент будет уничтожен. Все остальные соединения создаются и уничтожаются по требованию. Максимальное количество таких соединений задаётся свойством MaxConnectionsPerServer, т.е. например, если MaxConnectionsPerServer = 2, то одно резервируется как основное, а второе используется как дополнительное для операций, выполняемых в других потоках. Соответственно, если MaxConnectionsPerServer = 3, первое соединение резервируется как основное, а два остальных используются как дополнительные для операций в других потоках. Если приходит новый запрос на соединение из нового потока, а все соединения уже заняты, клиент ждёт, пока количество используемых соединений не уменьшится. Этот важный момент проясняет, почему корректное освобождение соединений так важно.
Примеры
Пользователь может выполнять операции в разных потоках несколькими способами. Их можно разделить на два типа:
1. Пользователь использует асинхронные (Begin/End) методы, определённые в клиенте. В этом случае почтовый клиент запускает новые потоки по мере необходимости. В клиенте реализована очередь задач (пожалуйста, не путайте её с очередью команд в соединении). Задача может быть выполнена, если соединение доступно. Как только количество используемых соединений становится меньше предельного значения, клиент создаёт новое соединение, создаёт поток для текущей задачи и исполняет эту задачу. Пример использования асинхронных операций:
2. Пользователь может создавать потоки, используя такие объекты, как Thread, ThreadPool, Task или любые другие объекты, предназначенные для этой цели. Также пользователь может использовать потоки, созданные в стороннем коде. При этом клиент имеет две модели поведения
a. Если пользователь не создавал дополнительные соединения для операций в потоке, все операции этого потока будут отправлены в очередь команд основного соединения. Пример операций в дополнительном потоке без создания нового соединения, все транзакции выполняются через основное соединение:
b. Когда пользователь вызывает метод для создания нового соединения для дополнительного потока, этот поток блокируется, пока значение квоты для новых соединений не будет изменено, чтобы разрешить новое соединение. Затем создаётся новое соединение. Это соединение устанавливается как соединение по умолчанию для всех операций в этом потоке. После завершения всех операций в этом потоке соединение должно быть освобождено. Для создания новых соединений используйте метод CredentialsByHostClient.CreateConnection. Этот метод возвращает объект, реализующий интерфейс IDisposable. Чтобы освободить соединение, необходимо вызвать метод Dispose. Создание и освобождение соединения должны выполняться внутри потока, в котором выполняются почтовые операции. Попытка создать новое соединение в потоке, где был создан почтовый клиент, приводит к ошибке, так как этот поток в данный момент нельзя использовать для создания нового соединения. Также создание нового соединения невозможно, когда MaxConnectionsPerServer = 1. Пример кода создания нового соединения в дополнительном потоке:
Рекомендации
Стоит отметить, что если пользователь отправляет все команды через основное соединение, может возникнуть ситуация, когда команды из разных потоков смешиваются. Пользователь должен понимать, какие команды зависят от их последовательности, и принимать меры по синхронизации таких команд. Также необходимо учитывать возможность выполнения команд в разных сеансах (IMAP/POP3). Наиболее ресурсоёмкие операции, такие как FetchMessage, AppendMessage и Send, вероятно имеет смысл выполнять в отдельном потоке и отдельном соединении. Но быстрые операции, такие как Delete, имеет смысл выполнять через основное соединение. Обратите внимание, что инициализация нового соединения сама по себе достаточно ресурсоёмкая операция.