Obsługa wielowątkowości w klientach poczty

Klienci poczty, tacy jak ImapClient i Pop3Client, umożliwiają ich używanie w środowisku wielowątkowym. Klienci poczty pozwalają na posiadanie jednego lub wielu 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ć tworzone i używane jednocześnie, jest ograniczona właściwością CredentialsByHostClient.MaxConnectionsPerServer. Właściwość tę można ustawić na 1 lub większą wartość. Domyślnie jest ona równa 10. Implementowano kolejkę poleceń dla połączenia, aby obsługiwać operacje wielowątkowe. Polecenia realizują najprostsze operacje zdefiniowane w protokole, takie jak Noop, Authenticate i inne. Użytkownik może rozpocząć wykonanie 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:

2. Użytkownik może tworzyć wątki używając obiektów takich jak Thread, ThreadPool, Task lub innych przeznaczonych do tego celu. Użytkownik może również używać wątków utworzonych w kodzie zewnętrznym. Podczas tego 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:

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:

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.