Podpora vícevláknového zpracování v e‑mailových klientech

E‑mailoví klienti jako ImapClient a Pop3Client Umožněte uživatelům používat je v prostředí vícevláknového zpracování. E‑mailoví klienti umožňují mít jedno nebo více spojení se serverem. Pro správu sady spojení uvnitř klientů se používá zásobník spojení (connection pool). Počet spojení, která mohou být vytvořena a používána současně, je omezen vlastností CredentialsByHostClient.MaxConnectionsPerServer. Tato vlastnost může být nastavena na 1 nebo vyšší hodnotu. Ve výchozím nastavení je tato hodnota 10. Pro podporu vícevláknových operací je pro spojení implementována fronta příkazů. Příkazy implementují nejjednodušší operace definované v protokolu, např. Noop, Authenticate a podobně. Uživatel může zahájit provádění většího počtu příkazů, než je dostupných spojení, ale budou vykonány až ve chvíli, kdy klient bude schopný vytvořit spojení pro danou operaci.

Metody používání e‑mailových klientů v prostředí vícevláknového zpracování

E‑mailoví klienti mají následující chování:

1. V případě, že MaxConnectionsPerServer = 1, klient vytvoří 1 spojení, provede autentizaci a autorizaci. Toto spojení zůstává v pracovním stavu až do okamžiku, kdy je klient zlikvidován. Všechny operace z různých vláken jsou směrovány do jedné fronty příkazů umístěné v hlavním spojení.

2. V případě, že MaxConnectionsPerServer > 1, klient vytvoří požadovaný počet spojení, provede autentizaci a autorizaci pro každé spojení. Jedno spojení je rezervováno jako hlavní spojení. Toto spojení zůstává v pracovním stavu až do okamžiku, kdy je klient zlikvidován. Všechna ostatní spojení jsou vytvářena a rušena dle potřeby. Maximální počet takových spojení je definován vlastností MaxConnectionsPerServer, tj. například pokud je MaxConnectionsPerServer = 2, jedno je rezervováno jako hlavní spojení a druhé spojení slouží jako doplňkové pro operace prováděné v jiných vláknech. Podobně pokud je MaxConnectionsPerServer = 3, první spojení je hlavní a dvě další jsou použita jako doplňková pro operace ve vláknech. V případě, že přijde další požadavek na spojení z nového vlákna a všechna spojení jsou již využita, klient čeká, dokud se počet použitých spojení nesníží. Tento okamžik je velmi důležitý, protože vysvětluje, proč je správné uvolňování spojení tak podstatné.

Příklady

Uživatel může provádět operace v různých vláknech několika způsoby. Daří se je rozdělit do dvou typů:

1. Uživatel používá asynchronní metody (Begin/End) definované v klientovi. V tomto případě e‑mailový klient spouští nová vlákna podle potřeby. V klientovi je implementována fronta úkolů (prosím, nepleťte si ji s frontou příkazů ve spojení). Úkol může být proveden, pokud je k dispozici spojení. Jakmile počet použitých spojení klesne pod limitní hodnotu, klient vytvoří nové spojení, vytvoří vlákno pro aktuální úkol a úkol vykoná. Příklad použití asynchronních operací:

// 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. Uživatel může vytvářet vlákna pomocí objektů jako Thread, ExecutorService nebo jakýchkoli jiných objektů určených k tomuto účelu. Také může používat vlákna vytvořená v kódu třetích stran. V tomto případě má klient dva modely chování

a. Pokud uživatel nevytvořil další spojení pro operace ve vláknu, všechny operace tohoto vlákna budou odeslány do fronty příkazů hlavního spojení. Příklad operací v dalším vláknu bez vytvoření nového spojení, všechny transakce probíhají přes hlavní spojení:

/**
 * 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. Když uživatel spustí metodu pro vytvoření nového spojení pro další vlákno, je toto vlákno blokováno, dokud se neupraví kvóta pro nová spojení, aby bylo nové spojení povoleno. Pak je nové spojení vytvořeno. Toto spojení je nastaveno jako výchozí spojení pro všechny operace v tomto vlákně. Po dokončení všech operací v tomto vlákně musí být spojení uvolněno. Pro vytvoření nových spojení použijte metodu CredentialsByHostClient.CreateConnection. Tato metoda vrací objekt, který implementuje rozhraní IDisposable. Pro uvolnění spojení je třeba zavolat metodu Dispose. Vytváření a uvolňování spojení musí probíhat uvnitř vlákna, ve kterém se provádějí e‑mailové operace. Pokus o vytvoření nového spojení ve vlákně, ve kterém byl vytvořen e‑mailový klient, vede k chybě, protože toto vlákno v tuto chvíli nemůže být použito pro vytvoření nového spojení. Vytvoření nového spojení také není možné, když je MaxConnectionsPerServer = 1. Příklad kódu pro vytvoření nového spojení v dalším vlákně:

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

Doporučení

Stojí za zmínku, že pokud uživatel odesílá všechny příkazy přes hlavní spojení, může dojít k zamíchání příkazů z různých vláken. Uživatel by měl rozumět, které příkazy jsou závislé na svém pořadí, a přijmout opatření k synchronizaci takových příkazů. Je také nutné zvážit možnost vykonávání příkazů v různých relacích (IMAP/POP3). Nejsložitější operace, jako FetchMessage, AppendMessage a Send, mají pravděpodobně smysl provádět v novém vlákně a s novým spojením. Naopak rychlé operace jako Delete je vhodné provádět přes hlavní spojení. Upozorňujeme, že inicializace nového spojení je sama o sobě časově náročná operace.