C# 如何检测客户端线程何时退出?

C# 如何检测客户端线程何时退出?,c#,.net,multithreading,C#,.net,Multithreading,这是一个有趣的图书馆作家的困境。在我的库中(在我的例子EasyNetQ中),我分配线程本地资源。因此,当客户端创建一个新线程,然后调用我的库中的某些方法时,就会创建新的资源。在EasyNetQ的情况下,当客户端在新线程上调用“Publish”时,将创建到RabbitMQ服务器的新通道。我希望能够检测客户端线程何时退出,以便清理资源(通道) 我提出的唯一方法是创建一个新的“观察者”线程,它只会阻塞对客户端线程的连接调用。下面是一个简单的演示: 首先是我的“图书馆”。它抓取客户机线程,然后创建一个新

这是一个有趣的图书馆作家的困境。在我的库中(在我的例子EasyNetQ中),我分配线程本地资源。因此,当客户端创建一个新线程,然后调用我的库中的某些方法时,就会创建新的资源。在EasyNetQ的情况下,当客户端在新线程上调用“Publish”时,将创建到RabbitMQ服务器的新通道。我希望能够检测客户端线程何时退出,以便清理资源(通道)

我提出的唯一方法是创建一个新的“观察者”线程,它只会阻塞对客户端线程的连接调用。下面是一个简单的演示:

首先是我的“图书馆”。它抓取客户机线程,然后创建一个新线程,阻止“连接”:

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为客户端声明“工作范围”提供了一种方法。一旦作用域完成,库就可以清理了。库提供了一个实现IDisposable的工作范围,但与上面的第一个备选方案不同,StartSomething方法保留在库类中:

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调用放在工作范围内

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

与第一种选择相比,我更喜欢这一点,因为它不会强迫库用户考虑范围。

如果客户端线程调用您的库,并在内部分配一些资源,则客户端应“打开”您的库,并为所有后续操作获取令牌。该标记可以是指向库内部向量的int索引,也可以是指向内部对象/结构的void指针。坚持客户端必须在终止前关闭令牌


这就是99%这样的lib调用的工作方式,其中状态必须在客户端调用中保留,例如套接字句柄、文件句柄。

您的
.Join
解决方案在我看来非常优雅。阻塞的观察线程并不是一件可怕的事情。

因为您没有直接控制线程的创建,所以很难知道线程何时完成了它的工作。另一种方法可能是强迫客户在完成后通知您:

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

除了做一些异步的花哨的事情来避免一个线程之外,我会尝试将所有的监视合并到一个线程中,该线程轮询访问库的所有线程的.ThreadState属性,比如说,每100ms一次(我不确定您需要多快地清理资源…)

您可以创建具有终结器的线程静态监视器。当线程处于活动状态时,它将保存监视器对象。当thead死后,它将停止持有它。稍后,当GC启动时,它将最终确定您的监视器。在终结器中,您可以引发一个事件,该事件将通知您的框架(已观察到)客户端线程的死亡

示例代码可在以下要点中找到:

这是一份副本:

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();
    }
}
公共类线程监视器
{
公共静态事件操作已完成=委托{};
私有只读int m_threadId=Thread.CurrentThread.ManagedThreadId;
~ThreadMonitor()
{
最终确定(线程ID);
}
公共int线程ID
{
获取{return m_threadId;}
}
}
公共静态类测试
{
专用只读静态ThreadLocal s_threadMonitor=
new ThreadLocal(()=>new ThreadMonitor());
公共静态void Main()
{
ThreadMonitor.Finalized+=i=>Console.WriteLine(“线程{0}已关闭”,i);
变量线程=新线程(()=>
{
var threadMonitor=s_threadMonitor.Value;
WriteLine(“在线程{0}上开始工作”,threadMonitor.ThreadId);
睡眠(1000);
WriteLine(“在线程{0}上结束工作”,threadMonitor.ThreadId);
});
thread.Start();
thread.Join();
//等待GC收集并最终确定所有内容
GC.GetTotalMemory(强制收集:true);
Console.ReadLine();
}
}

我希望有帮助。我认为它比额外的等待线程更优雅。

“我正在分配线程本地资源”-这不是一个好的开始:(不幸的是,这是低级AMQP库的一个限制,不允许您在线程之间共享通道。我猜另一种API设计是库提供客户端需要创建的非线程安全的“发布者”。我不确定是否正确关联了用例和示例代码。客户端代码是否为EasyNetQ需要启动一个新线程才能使用库?您可以输入一个更实际的客户端代码吗?如果不能,请输入以下行中的内容:var ctx=library.StartExecutionContext();…ctx.Complete();然后在库ctx内。Complete将清除,可能通过使用ManualResetEvent向线程发送完成信号当前EasyNetQ提供一个IBus实例,该实例将持续客户端应用程序的生命周期。要发布消息,请调用bus