Obsługa wielowątkowości w klientach poczty
Klienci poczty tacy jak ImapClient i Pop3Client Umożliwiają użytkownikom korzystanie z nich w środowisku wielowątkowym. Klienci poczty mogą mieć jedno lub więcej połączeń z serwerem. Do zarządzania zestawem połączeń w klientach używany jest pulę połączeń. Liczba połączeń, które mogą być jednocześnie tworzone i używane, jest ograniczona właściwością CredentialsByHostClient.MaxConnectionsPerServer. Właściwość ta może mieć wartość 1 lub większą. Domyślnie wynosi ona 10. Dla połączenia zaimplementowano kolejkę poleceń wspierającą operacje wielowątkowe. Polecenia implementują najprostsze operacje zdefiniowane w protokole, takie jak Noop, Authenticate itp. Użytkownik może rozpocząć wykonywanie większej liczby poleceń niż dostępnych połączeń, ale zostaną one wykonane dopiero, gdy klient będzie w stanie utworzyć połączenie dla danej operacji.
Metody używania klientów poczty w środowisku wielowątkowym
Klienci poczty mają następujące zachowanie:
1. W przypadku, gdy MaxConnectionsPerServer = 1, klient tworzy 1 połączenie, wykonuje uwierzytelnianie i autoryzację. To połączenie jest utrzymywane w stanie roboczym aż do momentu, gdy klient zostanie zwolniony. Wszystkie operacje z różnych wątków są kierowane do jednej kolejki poleceń umieszczonej w głównym połączeniu.
2. W przypadku, gdy MaxConnectionsPerServer > 1, klient tworzy wymaganą liczbę połączeń, wykonuje uwierzytelnianie i autoryzację dla każdego połączenia. Jedno połączenie jest zarezerwowane jako główne połączenie. To połączenie jest utrzymywane w stanie roboczym aż do momentu, gdy klient zostanie zwolniony. Wszystkie pozostałe połączenia są tworzone i zwalniane w razie potrzeby. Maksymalna liczba takich połączeń jest definiowana przez właściwość MaxConnectionsPerServer, np. jeśli MaxConnectionsPerServer = 2, to jedno jest zarezerwowane jako główne połączenie, a drugie połączenie służy jako dodatkowe dla operacji wykonywanych w innych wątkach. Odpowiednio, jeśli MaxConnectionsPerServer = 3, to pierwsze połączenie jest główne, a dwa pozostałe są używane jako dodatkowe dla operacji w innych wątkach. W przypadku, gdy pojawi się kolejne żądanie połączenia z nowego wątku i wszystkie połączenia są już używane, klient czeka, aż liczba używanych połączeń zostanie zmniejszona. To bardzo ważny moment, który wyjaśnia, dlaczego prawidłowe zwalnianie połączeń jest tak istotne.
Przykłady
Użytkownik może wykonywać operacje w różnych wątkach na kilka sposobów. Można je podzielić na dwa typy:
1. Użytkownik korzysta z asynchronicznych metod (Begin/End) zdefiniowanych w kliencie. W tym przypadku klient poczty uruchamia nowe wątki w razie potrzeby. W kliencie zaimplementowano kolejkę zadań (proszę nie mylić z kolejką poleceń w połączeniu). Zadanie może zostać wykonane, jeśli połączenie jest dostępne. Gdy liczba używanych połączeń spadnie poniżej limitu, klient tworzy nowe połączenie, tworzy wątek dla bieżącego zadania i wykonuje to zadanie. Przykład użycia operacji asynchronicznych:
// 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żytkownik może tworzyć wątki przy użyciu takich obiektów jak Thread, ExecutorService lub innych obiektów przeznaczonych do tego celu. Użytkownik może także używać wątków utworzonych w kodzie zewnętrznym. W tym przypadku klient ma dwa modele zachowania
a. Jeśli użytkownik nie utworzył dodatkowych połączeń dla operacji w wątku, wszystkie operacje tego wątku zostaną wysłane do kolejki poleceń na główne połączenie. Przykład operacji w dodatkowym wątku bez tworzenia nowego połączenia, wszystkie transakcje są realizowane za pośrednictwem głównego połączenia:
/**
* 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. Gdy użytkownik uruchamia metodę tworzącą nowe połączenie dla dodatkowego wątku, wątek ten jest blokowany, dopóki wartość kwoty dla nowych połączeń nie zostanie zmieniona, aby zezwolić na nowe połączenie. Następnie tworzone jest nowe połączenie. To połączenie jest ustawiane jako domyślne połączenie dla wszystkich operacji w tym wątku. Po zakończeniu wszystkich operacji w tym wątku połączenie musi zostać zwolnione. Aby tworzyć nowe połączenia, użyj metody CredentialsByHostClient.CreateConnection. Metoda ta zwraca obiekt implementujący interfejs IDisposable. Aby zwolnić połączenie, należy wywołać metodę Dispose. Tworzenie i zwalnianie połączenia musi być wykonywane wewnątrz wątku, w którym wykonywane są operacje pocztowe. Próba stworzenia nowego połączenia w wątku, w którym został utworzony klient poczty, prowadzi do błędu, ponieważ w tym momencie wątek nie może być użyty do tworzenia nowego połączenia. Tworzenie nowego połączenia nie jest również możliwe, gdy MaxConnectionsPerServer = 1. Przykład kodu tworzenia nowego połączenia w dodatkowym wątku:
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();
}
}
});
Zalecenia
Warto zauważyć, że jeśli użytkownik wysyła wszystkie polecenia do głównego połączenia, może dojść do sytuacji, w której polecenia z różnych wątków zostaną pomieszane. Użytkownik powinien rozumieć, które polecenia są zależne od ich kolejności i podjąć środki synchronizacji takich poleceń. Należy również rozważyć możliwość wykonywania poleceń w różnych sesjach (IMAP/POP3). Najbardziej czasochłonne operacje, takie jak FetchMessage, AppendMessage i Send, prawdopodobnie mają sens być wykonywane w nowym wątku i nowym połączeniu. Natomiast szybkie operacje, takie jak Delete, mają sens być wykonywane za pomocą głównego połączenia. Należy pamiętać, że inicjalizacja nowego połączenia jest wystarczająco czasochłonna.