Поддержка многопоточности в почтовых клиентах
Почтовые клиенты, такие как 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, имеет смысл выполнять через основное соединение. Обратите внимание, что инициализация нового соединения является достаточно ресурсоемкой операцией.