Поддержка многопоточности в почтовых клиентах

Почтовые клиенты, такие как ImapClient и Pop3Client Позволяют пользователям использовать их в многопоточной среде. Почтовые клиенты позволяют иметь одно или несколько соединений с сервером. Для управления набором соединений внутри клиентов используется пул соединений. Количество соединений, которое может быть создано и использовано одновременно, ограничивается свойством CredentialsByHostClient.MaxConnectionsPerServer. Это свойство может быть установлено в 1 или более. По умолчанию оно равно 10. Для соединения реализована очередь команд, поддерживающая многопоточные операции. Команды реализуют простейшие операции, определённые в протоколе, такие как Noop, Authenticate и т.д. Пользователь может инициировать выполнение большего количества команд, чем доступно соединений. Однако они будут выполнены только тогда, когда клиент сможет создать соединение для операции.

Методы использования почтовых клиентов в многопоточной среде

Почтовые клиенты имеют следующее поведение:

1. В случае, когда MaxConnectionsPerServer = 1, клиент создаёт 1 соединение, выполняет аутентификацию и авторизацию. Это соединение поддерживается в рабочем состоянии до момента, когда клиент будет уничтожен. Все операции из разных потоков направляются в одну очередь команд, расположенную в основном соединении.

2. В случае, когда MaxConnectionsPerServer > 1, клиент создает необходимое количество соединений, выполняет аутентификацию и авторизацию для каждого соединения. Одно соединение резервируется как основное. Это соединение поддерживается в рабочем состоянии до момента, когда клиент будет уничтожен. Все остальные соединения создаются и уничтожаются по требованию. Максимальное количество таких соединений задаётся свойством MaxConnectionsPerServer, т.е. например, если MaxConnectionsPerServer = 2, то одно резервируется как основное, а второе используется как дополнительное для операций, выполняемых в других потоках. Соответственно, если MaxConnectionsPerServer = 3, первое соединение резервируется как основное, а два остальных используются как дополнительные для операций в других потоках. Если приходит новый запрос на соединение из нового потока, а все соединения уже заняты, клиент ждёт, пока количество используемых соединений не уменьшится. Этот важный момент проясняет, почему корректное освобождение соединений так важно.

Примеры

Пользователь может выполнять операции в разных потоках несколькими способами. Их можно разделить на два типа:

1. Пользователь использует асинхронные (Begin/End) методы, определённые в клиенте. В этом случае почтовый клиент запускает новые потоки по мере необходимости. В клиенте реализована очередь задач (пожалуйста, не путайте её с очередью команд в соединении). Задача может быть выполнена, если соединение доступно. Как только количество используемых соединений становится меньше предельного значения, клиент создаёт новое соединение, создаёт поток для текущей задачи и исполняет эту задачу. Пример использования асинхронных операций:

// Create an imapclient with host, user and password
ImapClient client = new ImapClient();
client.setHost("domain.com");
client.setUsername("username");
client.setPassword("password");
client.selectFolder("InBox");

ImapMessageInfoCollection messages = client.listMessages();
IAsyncResult res1 = client.beginFetchMessage(messages.get_Item(0).getUniqueId());
IAsyncResult res2 = client.beginFetchMessage(messages.get_Item(1).getUniqueId());
MailMessage msg1 = client.endFetchMessage(res1);
MailMessage msg2 = client.endFetchMessage(res2);

2. Пользователь может создавать потоки, используя такие объекты, как Thread, ExecutorService или любые другие предназначенные для этой цели объекты. Также пользователь может использовать потоки, созданные в стороннем коде. При этом клиент имеет две модели поведения

a. Если пользователь не создавал дополнительные соединения для операций в потоке, все операции этого потока будут отправлены в очередь команд основного соединения. Пример операций в дополнительном потоке без создания нового соединения, все транзакции выполняются через основное соединение:

/**
 * Creates an executor service with a fixed pool size, that will time 
 * out after a certain period of inactivity.
 * 
 * @param poolSize The core- and maximum pool size
 * @param keepAliveTime The keep alive time
 * @param timeUnit The time unit
 * @return The executor service
 */
private static ExecutorService createFixedTimeoutExecutorService(
    int poolSize, long keepAliveTime, TimeUnit timeUnit)
{
    ThreadPoolExecutor e = 
        new ThreadPoolExecutor(poolSize, poolSize,
            keepAliveTime, timeUnit, new LinkedBlockingQueue<Runnable>());
    e.allowCoreThreadTimeOut(true);
    return e;
}

final List<MailMessage> list = new ArrayList<MailMessage>();

ExecutorService executor = createFixedTimeoutExecutorService(1, 1000, TimeUnit.MILLISECONDS);

executor.execute(new Runnable() {
    public void run() {
        client.selectFolder("folderName");
        ImapMessageInfoCollection messageInfoCol = client.listMessages();
        for (ImapMessageInfo messageInfo : messageInfoCol) {
            list.add(client.fetchMessage(messageInfo.getUniqueId()));
        }
    }
});

b. Когда пользователь вызывает метод для создания нового соединения для дополнительного потока, этот поток блокируется, пока значение квоты для новых соединений не будет изменено, чтобы разрешить новое соединение. Затем создаётся новое соединение. Это соединение устанавливается как соединение по умолчанию для всех операций в этом потоке. После завершения всех операций в этом потоке соединение должно быть освобождено. Для создания новых соединений используйте метод CredentialsByHostClient.CreateConnection. Этот метод возвращает объект, реализующий интерфейс IDisposable. Чтобы освободить соединение, необходимо вызвать метод Dispose. Создание и освобождение соединения должны выполняться внутри потока, в котором выполняются почтовые операции. Попытка создать новое соединение в потоке, где был создан почтовый клиент, приводит к ошибке, так как этот поток в данный момент нельзя использовать для создания нового соединения. Также создание нового соединения невозможно, когда MaxConnectionsPerServer = 1. Пример кода создания нового соединения в дополнительном потоке:

final List<MailMessage> list = new ArrayList<MailMessage>();

ExecutorService executor = createFixedTimeoutExecutorService(1, 1000, TimeUnit.MILLISECONDS);

executor.execute(new Runnable() {
    public void run() {
        IConnection connection = client.createConnection();
        try {
            client.selectFolder(connection, "FolderName");
            ImapMessageInfoCollection messageInfoCol = client.listMessages(connection);
            for (ImapMessageInfo messageInfo : messageInfoCol)
                list.add(client.fetchMessage(connection, messageInfo.getUniqueId()));
        } finally {
            connection.dispose();
        }
    }
});

Рекомендации

Стоит отметить, что если пользователь отправляет все команды через основное соединение, может возникнуть ситуация, когда команды из разных потоков смешиваются. Пользователь должен понимать, какие команды зависят от их последовательности, и принимать меры по синхронизации таких команд. Также необходимо учитывать возможность выполнения команд в разных сеансах (IMAP/POP3). Наиболее ресурсоёмкие операции, такие как FetchMessage, AppendMessage и Send, вероятно имеет смысл выполнять в отдельном потоке и отдельном соединении. Но быстрые операции, такие как Delete, имеет смысл выполнять через основное соединение. Обратите внимание, что инициализация нового соединения сама по себе достаточно ресурсоёмкая операция.