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

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

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

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

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

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

Примеры

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

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

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

/**
 * Создает сервис исполнителя с фиксированным размером пула, который будет 
 * завершён после определённого периода бездействия.
 * 
 * @param poolSize Основной и максимальный размер пула
 * @param keepAliveTime Время поддержания активности
 * @param timeUnit Единица времени
 * @return Сервис исполнителя
 */
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, имеют смысл выполнять с основным соединением. Пожалуйста, обратите внимание, что инициализация нового соединения — это достаточно времязатратная операция.