Как определить, когда клиентский поток выходит?

Это интересная дилемма писателей библиотек. В моей библиотеке (в моем случае EasyNetQ) Я назначаю локальные ресурсы потока. Поэтому, когда клиент создает новый поток, а затем вызывает определенные методы в моей библиотеке, создаются новые ресурсы. В случае EasyNetQ новый канал для сервера RabbitMQ создается, когда клиент вызывает "Опубликовать в новом потоке". Я хочу иметь возможность обнаруживать, когда клиентский поток завершает работу, чтобы я мог очищать ресурсы (каналы).

Единственный способ сделать это Ive - создать новый поток наблюдателей, который просто блокирует вызов Join в клиентский поток. Здесь простая демонстрация:

Сначала моя библиотека. Он захватывает клиентский поток, а затем создает новый поток, который блокирует "Join:

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();
    }
}

Вот клиент, который использует мою библиотеку. Он создает новый поток, а затем вызывает мою библиотеку метод StartSomething:

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();
    }
}

Когда я запускаю клиент следующим образом:

var client = new Client(new Library());

client.DoWorkInAThread();

// give the client thread time to complete
Thread.Sleep(100);

Я получаю этот вывод:

Library says: StartSomething called
Client thread says: I'm done
Libaray says: Client thread existed

Так оно работает, но это уродливо. Мне действительно не нравится идея всех этих заблокированных потоков наблюдателей, висящих вокруг. Есть ли лучший способ сделать это?

Первая альтернатива.

Предоставить метод, который возвращает работника, который реализует IDisposable, и разъяснить в документации, что вы не должны делиться между рабочими между потоками. Здесь измененная библиотека:

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");
    }
}

Клиент теперь немного сложнее:

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();
    }
}

Основная проблема с этим изменением заключается в том, что это изменение для API. Существующие клиенты должны быть переписаны. Теперь, когда это не так уж плохо, это означало бы пересмотреть их и убедиться, что они правильно очищаются.

Неразрешимая вторая альтернатива. API предоставляет возможность клиенту объявить "область работы". По завершении области библиотека может очистить. Библиотека предоставляет WorkScope, который реализует IDisposable, но в отличие от первой альтернативы выше, метод StartSomething остается в классе Library:

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");
    }
}

Клиент просто помещает вызов StartSomething в 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();
    }
}

Мне нравится это меньше, чем первая альтернатива, потому что это не заставляет пользователя библиотеки думать о сфере видимости.

Ответы

Ответ 1

Вы можете создать статический монитор потока с финализатором. Когда поток жив, он будет удерживать объект монитора. Когда папа умрет, он перестанет его удерживать. Позже, когда GC вскочит, он завершит ваш монитор. В финализаторе вы можете поднять событие, которое сообщит вашей структуре о (наблюдаемой) смерти клиентского потока.

В этом контексте можно найти пример кода: https://gist.github.com/2587063

Вот его копия:

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();
    }
}

Надеюсь, это поможет. Я думаю, что он более изящный, чем ваш дополнительный ожидающий поток.

Ответ 2

Поскольку вы не контролируете прямое создание потока, вам сложно знать, когда поток завершил выполнение своей работы. Альтернативный подход, возможно, состоял бы в том, чтобы заставить вас уведомить вас, когда они будут сделаны:

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();
    }
}

Таким образом, любой клиент, который вызывает вас, вынужден передать какой-то механизм уведомления, который скажет вам, когда он сделает свое дело:

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();
    }
}

Ответ 3

Если клиентский поток выполняет вызовы в вашу библиотеку, которые внутренне выделяют некоторые ресурсы, клиент должен "открыть" вашу библиотеку и вернуть токен для всех последующих операций. Этот токен может быть индексом int в вектор, внутренний для библиотеки, или указатель void на внутренний объект/структуру. Настаивайте на том, чтобы клиенты закрывали токен перед завершением.

Таким образом 99% всех таких вызовов lib работают там, где должно быть сохранено состояние между вызовами клиентов, например. дескрипторы сокетов, дескрипторы файлов.

Ответ 4

Помимо использования любых асинхронных причудливых вещей, чтобы избежать потока в целом, я попытался бы объединить все наблюдения в один поток, который опросит свойство .ThreadState всех потоков, попавших в вашу библиотеку, скажем, каждые 100 мс (I ' m не уверен, как быстро вам нужно очистить ресурсы...)

Ответ 5

Ваше решение .Join выглядит довольно элегантно для меня. Заблокированные потоки наблюдателей - не такая страшная вещь.