2012-05-03 14 views
14

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.

+2

"Przydzielam lokalne zasoby wątku" - nie jest to dobry początek :( –

+2

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. –

+0

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. –

Odpowiedz

1

Jeśli wątek klienta wykonuje połączenia do biblioteki, która wewnętrznie przydziela pewne zasoby, klient powinien "otworzyć" bibliotekę i odzyskać token dla wszystkich dalszych operacji. Token ten może być indeksem int do wektora wewnętrznego biblioteki lub pustego wskaźnika do wewnętrznego obiektu/struktury.Domagaj się, aby klienci zamknęli token przed zakończeniem.

W ten sposób działa 99% wszystkich takich wywołań lib, gdzie stan musi być zachowany przez połączenia klienta, np. uchwyty do gniazd, uchwyty plików.

0

Twoje rozwiązanie .Join wygląda całkiem elegancko. Zablokowane wątki obserwatorów nie są tak straszne.

+1

O tak, są! –

+0

Cóż, wszystkie rzeczy z umiarem.: p Naprawdę nie ma innego sposobu, który znam, aby mieć pewność, że wątek jest kompletny. – IngisKahn

+0

Co się stanie, jeśli wątek zakończy operacje tworzenia libary dla tego kanału, chce go zamknąć, a może później otworzyć ponownie lub inny? Wątki wywołujące nigdy nie kończą się przez cały czas trwania aplikacji, ale mogą otwierać i zamykać zasoby biblioteki tysiące razy. Żadne z tych rozwiązań z polling-client-thread-state nie są przydatne dla biblioteki. –

3

Ponieważ nie kontrolujesz bezpośrednio tworzenia wątku, ciężko jest wiedzieć, kiedy wątek zakończył wykonywanie swojej pracy. Alternatywnym rozwiązaniem może byłoby, aby zmusić klienta, aby powiadomić Cię, gdy skończysz:

public interface IThreadCompletedNotifier 
{ 
    event Action ThreadCompleted; 
} 

public class Library 
{ 
    public void StartSomething(IThreadCompletedNotifier notifier) 
    { 
     Console.WriteLine("Library says: StartSomething called"); 
     notifier.ThreadCompleted +=() => Console.WriteLine("Libaray says: Client thread existed"); 
     var clientThread = Thread.CurrentThread; 
     exitMonitorThread.Start(); 
    } 
} 

W ten sposób, każdy klient, który wzywa was jest zmuszony przejść w jakiś mechanizm powiadamiania że powie gdy jej zrobić robi swoje rzeczy:

public class Client : IThreadCompletedNotifier 
{ 
    private readonly Library library; 

    public event Action ThreadCompleted; 

    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"); 
      if(ThreadCompleted != null) 
      { 
       ThreadCompleted(); 
      } 
     }); 
     thread.Start(); 
    } 
} 
+0

To jest lepsze - klient powinien dostarczyć powiadomienie. Powiadomienie powinno zawierać parametr identyfikujący zwalniany zasób. Parametr ten powinien być rączką/tokenem dla przydzielonego zasobu, tj. nie ma nic wspólnego z identyfikatorem wątku ani podobnymi rzeczami, w przeciwnym razie biblioteka staje się bezużyteczna w przypadku łączonych wątków, "zielonych" wątków/włókien lub wątków, które nigdy się nie kończą, nie przechodzą w pętlę i nie chcą używać różnych zasobów w tym samym lub innym czasie. –

+0

To był mój pierwszy pomysł, Dostarczyć OperationContext, który zaimplementował IDisposable, wtedy wszystkie połączenia lokalne wątku mogłyby zostać wykonane w instrukcji using. Chyba chciałem najpierw odkryć "po prostu działa" alternatywy, ale przychodzę do pomysłu konkretnego API. –

+3

Rozwiązania "to po prostu działa" po prostu nie są . Whidows, Linux, każdy system operacyjny, z jakim kiedykolwiek korzystałem ma "token", "uchwyt" lub inny taki obiekt, jak rozwiązanie do utrzymywania stanu przez połączenia. Używanie nieuporządkowanych danych lokalnych wątków i odpytywanie w celu zakończenia wątków itp. Wyssa życie z twojej biblioteki. –

1

Oprócz robienia żadnej asynchroniczny wymyślnych rzeczy, aby uniknąć całkowicie wątek, chciałbym spróbować połączyć wszystkie oglądania w jednym wątku, który sonduje własności .ThreadState wszystkich wątków, które mają hit twojej biblioteki, powiedzmy, co 100ms (nie jestem pewien, jak szybko musisz posprzątać zasoby ...)

+1

Co się stanie, jeśli wątki klienta nigdy się nie zakończą, ale w pętli otworzą się kolejne biblioteki channes/resources? Rozwiązanie polegające na sondowaniu przez państwo ogranicza użytkowników bibliotek do wątków, które są nieustannie tworzone i kończone. Gwiazdy puli muszą zostać zbanowane, jak również każdy wątek wokół pętli. Po prostu nie zadziała. –

+0

@MartinJames, dlaczego nie można również monitorować puli wątków? Każdy wątek, który kiedykolwiek korzystał z biblioteki, mógł zostać zarejestrowany do odpytywania. Myślę, że to rozwiązanie ma sens. – usr

+0

@Martin Rozumiem, co mówisz, i myślę, że zrozumiałe jest, że wykrywanie zamknięcia nici nie jest jedyną metodą (a nawet preferowaną metodą) czyszczenia. To tylko dodatkowe zabezpieczenie biblioteki przed błędem użytkownika. Masz rację: jeśli użytkownicy nie zamykają własnych uchwytów na połączonych wątkach lub wątku GUI, będą zwlekać na zawsze. –

5

Możesz utworzyć monitor statyczny wątku z finalizatorem. Gdy wątek będzie żył, przytrzyma obiekt monitora. Kiedy umiera thead, przestanie go trzymać. Później, gdy GC pojawi się, sfinalizuje twój monitor. W finalizatorze możesz podnieść wydarzenie, które poinformuje twoją strukturę o (zaobserwowanej) śmierci wątku klienta.

Próbkę kodu można znaleźć w tym GIST: https://gist.github.com/2587063

Oto kopia:

public class ThreadMonitor 
{ 
    public static event Action<int> Finalized = delegate { }; 
    private readonly int m_threadId = Thread.CurrentThread.ManagedThreadId; 

    ~ThreadMonitor() 
    { 
     Finalized(ThreadId); 
    } 

    public int ThreadId 
    { 
     get { return m_threadId; } 
    } 
} 

public static class Test 
{ 
    private readonly static ThreadLocal<ThreadMonitor> s_threadMonitor = 
     new ThreadLocal<ThreadMonitor>(() => new ThreadMonitor()); 

    public static void Main() 
    { 
     ThreadMonitor.Finalized += i => Console.WriteLine("thread {0} closed", i); 
     var thread = new Thread(() => 
     { 
      var threadMonitor = s_threadMonitor.Value; 
      Console.WriteLine("start work on thread {0}", threadMonitor.ThreadId); 
      Thread.Sleep(1000); 
      Console.WriteLine("end work on thread {0}", threadMonitor.ThreadId); 
     }); 
     thread.Start(); 
     thread.Join(); 

     // wait for GC to collect and finalize everything 
     GC.GetTotalMemory(forceFullCollection: true); 

     Console.ReadLine(); 
    } 
} 

Mam nadzieję, że to pomaga. Myślę, że jest bardziej elegancki niż dodatkowy wątek oczekujący.

Powiązane problemy