Oto interesujący dylemat bibliotekarza. W mojej bibliotece (w moim przypadku EasyNetQ) przydzielam zasoby lokalne wątku. Kiedy klient tworzy nowy wątek, a następnie wywołuje określone metody w mojej bibliotece, tworzone są nowe zasoby. W przypadku EasyNetQ nowy kanał do serwera RabbitMQ jest tworzony, gdy klient wywołuje "Publikuj" w nowym wątku. Chcę być w stanie wykryć, kiedy wątek klienta zostanie zakończony, aby można było wyczyścić zasoby (kanały).Jak wykryć, gdy wątek klienta zostanie zamknięty?
Jedynym sposobem, w jaki to zrobiłem, jest utworzenie nowego wątku "watcher", który po prostu blokuje połączenie Join do wątku klienta. Oto prosta demonstracja:
Najpierw moja "biblioteka". Przechwytuje wątek klienta, a następnie tworzy nowy wątek blokujący "Dołącz":
public class Library
{
public void StartSomething()
{
Console.WriteLine("Library says: StartSomething called");
var clientThread = Thread.CurrentThread;
var exitMonitorThread = new Thread(() =>
{
clientThread.Join();
Console.WriteLine("Libaray says: Client thread existed");
});
exitMonitorThread.Start();
}
}
Oto klient korzystający z mojej biblioteki. To tworzy nowy wątek, a następnie wywołuje mojej biblioteki StartSomething metoda:
public class Client
{
private readonly Library library;
public Client(Library library)
{
this.library = library;
}
public void DoWorkInAThread()
{
var thread = new Thread(() =>
{
library.StartSomething();
Thread.Sleep(10);
Console.WriteLine("Client thread says: I'm done");
});
thread.Start();
}
}
Kiedy uruchomić klienta tak:
var client = new Client(new Library());
client.DoWorkInAThread();
// give the client thread time to complete
Thread.Sleep(100);
uzyskać ten wynik:
Library says: StartSomething called
Client thread says: I'm done
Libaray says: Client thread existed
Tak to działa , ale jest brzydka. Naprawdę nie podoba mi się, że wszystkie te zablokowane wątki obserwatorów kręcą się wokół. Czy jest lepszy sposób na zrobienie tego?
Pierwsza alternatywa.
Należy podać metodę zwracającą pracownika implementującego IDisposable i wyjaśniającą w dokumentacji, że nie należy współużytkować pracowników między wątkami. Oto zmodyfikowana biblioteka:
public class Library
{
public LibraryWorker GetLibraryWorker()
{
return new LibraryWorker();
}
}
public class LibraryWorker : IDisposable
{
public void StartSomething()
{
Console.WriteLine("Library says: StartSomething called");
}
public void Dispose()
{
Console.WriteLine("Library says: I can clean up");
}
}
Klient jest teraz trochę bardziej skomplikowana:
public class Client
{
private readonly Library library;
public Client(Library library)
{
this.library = library;
}
public void DoWorkInAThread()
{
var thread = new Thread(() =>
{
using(var worker = library.GetLibraryWorker())
{
worker.StartSomething();
Console.WriteLine("Client thread says: I'm done");
}
});
thread.Start();
}
}
Głównym problemem z tej zmiany jest to, że jest to zmiana na łamanie API. Obecni klienci będą musieli zostać ponownie napisani. To nie jest takie złe, oznacza to, że trzeba będzie je powtórzyć i upewnić się, że są poprawne.
Nie przełamująca druga alternatywa. Interfejs API umożliwia klientowi zadeklarowanie "zakresu pracy". Po zakończeniu działania biblioteka może wyczyścić. Biblioteka zapewnia WorkScope który implementuje IDisposable, ale w przeciwieństwie do pierwszej alternatywy powyżej, metoda StartSomething pozostaje w klasie Biblioteka:
public class Library
{
public WorkScope GetWorkScope()
{
return new WorkScope();
}
public void StartSomething()
{
Console.WriteLine("Library says: StartSomething called");
}
}
public class WorkScope : IDisposable
{
public void Dispose()
{
Console.WriteLine("Library says: I can clean up");
}
}
Klient po prostu stawia wezwanie StartSomething w WorkScope ...
public class Client
{
private readonly Library library;
public Client(Library library)
{
this.library = library;
}
public void DoWorkInAThread()
{
var thread = new Thread(() =>
{
using(library.GetWorkScope())
{
library.StartSomething();
Console.WriteLine("Client thread says: I'm done");
}
});
thread.Start();
}
}
Podoba mi się to mniej niż pierwsza alternatywa, ponieważ nie zmusza użytkownika biblioteki do myślenia o zasięgu.
"Przydzielam lokalne zasoby wątku" - nie jest to dobry początek :( –
Niestety jest to ograniczenie biblioteki AMQP niskiego poziomu, nie można udostępniać kanałów między wątkami. Chyba alternatywny projekt interfejsu API byłoby dla biblioteki, aby zapewnić "nieobowiązkowy" dla wątków "wydawca", który musi utworzyć klient. –
Nie jestem pewien, czy poprawnie koreluję przypadek użycia i przykładowy kod. Czy kod klienta EasyNetQ byłby wymagany do rozpoczęcia nowego Wątek w celu korzystania z biblioteki? Można wstawić bardziej rzeczywisty kod klienta? Jeśli nie, to coś w linii: var ctx = library.StartExecutionContext(); ... ctx.Complete(); następnie wewnątrz biblioteki ctx.Complete oczyści, może używając ManualResetEvent do sygnalizuje, że wątek został zakończony. –