Supporto multi-threading nei client di posta
Client di posta come ImapClient e Pop3Client consentono agli utenti di utilizzarli in un ambiente multi-thread. I client di posta permettono di avere una o più connessioni con un server. Per gestire un insieme di connessioni all’interno dei client viene utilizzato un pool di connessioni. La quantità di connessioni che possono essere create e utilizzate contemporaneamente è limitata dalla proprietà CredentialsByHostClient.MaxConnectionsPerServer. Questa proprietà può essere impostata a 1 o a un valore più alto. Per impostazione predefinita la proprietà è pari a 10. È stata implementata una coda dei comandi per la connessione al fine di supportare operazioni multi-thread. I comandi implementano le operazioni più semplici definite dal protocollo, come Noop, Authenticate e così via. L’utente può avviare l’esecuzione di un numero di comandi maggiore rispetto al numero di connessioni disponibili. Tuttavia, verranno eseguiti solo quando il client sarà in grado di creare una connessione per l’operazione.
Metodi per utilizzare i client di posta in ambiente multi-thread
I client di posta hanno il seguente comportamento:
1. Nel caso in cui MaxConnectionsPerServer = 1, il client crea 1 connessione, esegue l’autenticazione e l’autorizzazione. Questa connessione rimane attiva fino al momento in cui il client verrà eliminato. Tutte le operazioni da thread diversi sono indirizzate a una singola coda di comandi posta nella connessione principale.
2. Nel caso in cui MaxConnectionsPerServer > 1, il client crea la quantità necessaria di connessioni, esegue l’autenticazione e l’autorizzazione per ogni connessione. Una connessione è riservata come connessione principale. Questa connessione rimane attiva fino al momento in cui il client viene eliminato. Tutte le altre connessioni vengono create e eliminate su richiesta. La quantità massima di tali connessioni è definita dalla proprietà MaxConnectionsPerServer, ad esempio se MaxConnectionsPerServer = 2, una è riservata come connessione principale e la seconda viene usata come aggiuntiva per le operazioni eseguite in altri thread. Analogamente, se MaxConnectionsPerServer = 3, la prima connessione è riservata come principale e le altre due sono usate come aggiuntive per le operazioni in altri thread. Nel caso in cui arrivi una nuova richiesta di connessione da un nuovo thread e tutte le connessioni sono già in uso, il client attende finché il numero di connessioni utilizzate non diminuisce. Questo è un momento molto importante che chiarisce perché il corretto rilascio delle connessioni è così fondamentale.
Esempi
L’utente può eseguire operazioni in diversi thread in vari modi. Possono essere divisi in due tipologie:
1. L’utente utilizza i metodi asincroni (Begin/End) definiti nel client. In questo caso il client di posta avvia nuovi thread quando necessario. Nel client è implementata una coda di attività (per favore, non confonderla con la coda dei comandi nella connessione). Un’attività può essere eseguita se la connessione è disponibile. Quando la quantità di connessioni utilizzate diventa inferiore al valore limite, il client crea una nuova connessione, crea un thread per l’attività corrente e la esegue. Esempio di utilizzo di operazioni asincrone:
// 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’utente può creare thread utilizzando oggetti come Thread, ExecutorService o qualsiasi altro oggetto previsto a tal fine. Inoltre l’utente può utilizzare thread creati in codice di terze parti. In questo caso il client ha due modelli di comportamento
a. Se l’utente non ha creato connessioni aggiuntive per le operazioni nel thread, tutte le operazioni per questo thread verranno inviate alla coda dei comandi della connessione principale. Un esempio di operazioni in un thread aggiuntivo senza creare una nuova connessione, tutte le transazioni avvengono tramite la connessione 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. Quando l’utente esegue il metodo per creare una nuova connessione per un thread aggiuntivo, questo thread è bloccato finché il valore di quota per le nuove connessioni non viene modificato per consentire una nuova connessione. Successivamente viene creata la nuova connessione. Questa connessione è impostata come connessione predefinita per tutte le operazioni in questo thread. Dopo che tutte le operazioni in questo thread sono completate, la connessione deve essere eliminata. Per creare nuove connessioni utilizzare il metodo CredentialsByHostClient.CreateConnection. Questo metodo restituisce un oggetto che implementa l’interfaccia IDisposable. Per rilasciare la connessione è necessario invocare il metodo Dispose. La creazione e l’eliminazione della connessione devono essere eseguite all’interno del thread in cui vengono eseguite le operazioni di posta. Tentare di creare una nuova connessione nel thread in cui è stato creato il client di posta porta a un errore, poiché questo thread non può essere utilizzato per la creazione di una nuova connessione. Inoltre, la creazione di una nuova connessione non è possibile quando MaxConnectionsPerServer = 1. Un esempio di codice per creare una nuova connessione in un thread aggiuntivo:
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();
}
}
});
Raccomandazioni
È importante notare che se l’utente invia tutti i comandi alla connessione principale, può verificarsi una situazione in cui i comandi provenienti da thread diversi si mescolano. L’utente deve capire quali comandi dipendono dalla loro sequenza e adottare misure per sincronizzare tali comandi. È inoltre necessario considerare la possibilità di eseguire i comandi in sessioni diverse (IMAP/POP3). Le operazioni più dispendiose in termini di tempo, come FetchMessage, AppendMessage e Send, probabilmente hanno senso essere eseguite con un nuovo thread e una nuova connessione. Tuttavia, operazioni rapide come Delete hanno senso essere eseguite con la connessione principale. Si noti che l’inizializzazione di una nuova connessione è un’operazione sufficientemente dispendiosa in termini di tempo.