Support du multithreading dans les clients de messagerie

Clients de messagerie tels que ImapClient et Pop3Client Permettre aux utilisateurs de les utiliser dans un environnement multithread. Les clients de messagerie peuvent établir une ou plusieurs connexions avec un serveur. La gestion d'un ensemble de connexions au sein des clients utilise un pool de connexions. Le nombre de connexions pouvant être créées et utilisées simultanément est limité par la propriété CredentialsByHostClient.MaxConnectionsPerServer. Cette propriété peut être définie à 1 ou à une valeur supérieure. Par défaut, elle vaut 10. Une file d'attente de commandes a été implémentée pour la connexion afin de prendre en charge les opérations multithread. Les commandes implémentent les opérations les plus simples définies dans le protocole, telles que Noop, Authenticate, etc. L'utilisateur peut lancer l'exécution d'un nombre de commandes supérieur au nombre de connexions disponibles, mais elles ne seront exécutées que lorsque le client pourra créer une connexion pour l'opération.

Méthodes d'utilisation des clients de messagerie en environnement multithread

Les clients de messagerie ont le comportement suivant :

1. Dans le cas où MaxConnectionsPerServer = 1, le client crée une seule connexion, effectue l'authentification et l'autorisation. Cette connexion reste active jusqu'au moment où le client est libéré. Toutes les opérations provenant de différents threads sont dirigées vers une seule file d'attente de commandes placée dans la connexion principale.

2. Dans le cas où MaxConnectionsPerServer > 1, le client crée le nombre nécessaire de connexions, effectue l'authentification et l'autorisation pour chaque connexion. Une connexion est réservée comme connexion principale. Cette connexion reste active jusqu'au moment où le client est libéré. Toutes les autres connexions sont créées et libérées à la demande. Le nombre maximal de telles connexions est défini par la propriété MaxConnectionsPerServer, c'est‑à‑dire, par exemple, si MaxConnectionsPerServer = 2, alors une connexion est réservée comme principale et la seconde est utilisée comme supplémentaire pour les opérations exécutées dans d'autres threads. De même, si MaxConnectionsPerServer = 3, la première connexion est principale et les deux autres sont utilisées comme supplémentaires pour les opérations exécutées dans d'autres threads. Si une nouvelle demande de connexion provient d'un nouveau thread et que toutes les connexions sont déjà utilisées, le client attend que le nombre de connexions utilisées diminue. C'est un point très important qui explique pourquoi la libération correcte des connexions est si cruciale.

Exemples

L'utilisateur peut exécuter des opérations dans différents threads de plusieurs manières. Elles peuvent être divisées en deux types :

1. L'utilisateur utilise les méthodes asynchrones (Begin/End) définies dans le client. Dans ce cas, le client de messagerie lance de nouveaux threads lorsque nécessaire. Le client implémente une file d'attente de tâches (veuillez ne pas confondre avec la file d'attente de commandes de la connexion). Une tâche peut être exécutée si une connexion est disponible. Dès que le nombre de connexions utilisées devient inférieur à la valeur limite, le client crée une nouvelle connexion, crée un thread pour la tâche en cours et exécute cette tâche. Exemple d'utilisation d'opérations asynchrones :

// 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. L'utilisateur peut créer des threads en utilisant des objets tels que Thread, ExecutorService ou tout autre objet destiné à cet effet. L'utilisateur peut également utiliser des threads créés dans du code tiers. Dans ce cas, le client possède deux modèles de comportement

a. Si l'utilisateur n'a pas créé de connexions supplémentaires pour les opérations dans le thread, toutes les opérations de ce thread seront envoyées à la file d'attente de commandes de la connexion principale. Un exemple d'opérations dans un thread supplémentaire sans créer de nouvelle connexion, toutes les transactions sont effectuées via la connexion principale :

/**
 * 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. Lorsque l'utilisateur exécute la méthode pour créer une nouvelle connexion pour un thread supplémentaire, ce thread est bloqué jusqu'à ce que la valeur du quota pour les nouvelles connexions soit modifiée afin de permettre la nouvelle connexion. Ensuite, la nouvelle connexion est créée. Cette connexion est définie comme connexion par défaut pour toutes les opérations de ce thread. Après que toutes les opérations de ce thread soient terminées, la connexion doit être libérée. Pour créer de nouvelles connexions, utilisez la méthode CredentialsByHostClient.CreateConnection. Cette méthode renvoie un objet qui implémente l'interface IDisposable. Pour libérer la connexion, la méthode Dispose doit être appelée. La création et la libération de la connexion doivent être exécutées à l'intérieur du thread où les opérations de messagerie sont réalisées. Tenter de créer une nouvelle connexion dans le thread où le client de messagerie a été créé entraîne une erreur, car ce thread ne peut pas être utilisé à ce moment pour créer une nouvelle connexion. De plus, la création d'une nouvelle connexion n'est pas possible lorsque MaxConnectionsPerServer = 1. Un exemple de code de création d'une nouvelle connexion dans un thread supplémentaire :

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();
        }
    }
});

Recommandations

Il convient de noter que si l'utilisateur envoie toutes les commandes sur la connexion principale, une situation peut survenir où les commandes provenant de différents threads sont mélangées. L'utilisateur doit comprendre quelles commandes dépendent de leur séquence et prendre des mesures pour synchroniser ces commandes. Il faut également envisager la possibilité d'exécuter des commandes dans des sessions différentes (IMAP/POP3). Les opérations les plus chronophages, telles que FetchMessage, AppendMessage et Send, sont probablement mieux exécutées dans un nouveau thread avec une nouvelle connexion. En revanche, les opérations rapides comme Delete peuvent être effectuées sur la connexion principale. Veuillez noter que l'initialisation d'une nouvelle connexion est une opération suffisamment longue.