为什么SocketAsyncEventArgs的完成回调经常在新创建的线程中执行,而不是使用有界线程池?

10

我有一个简单的客户端应用程序,通过网络以低吞吐量接收字节缓冲区。下面是代码:

private static readonly HashSet<int> _capturedThreadIds = new HashSet<int>();

private static void RunClient(Socket socket)
{
    var e = new SocketAsyncEventArgs();
    e.SetBuffer(new byte[10000], 0, 10000);
    e.Completed += SocketAsyncEventsArgsCompleted;

    Receive(socket, e);
}

private static void Receive(Socket socket, SocketAsyncEventArgs e)
{
    var isAsynchronous = socket.ReceiveAsync(e);
    if (!isAsynchronous)
        SocketAsyncEventsArgsCompleted(socket, e);
}

private static void SocketAsyncEventsArgsCompleted(object sender, SocketAsyncEventArgs e)
{
    if (e.LastOperation != SocketAsyncOperation.Receive || e.SocketError != SocketError.Success || e.BytesTransferred <= 0)
    {
        Console.WriteLine("Operation: {0}, Error: {1}, BytesTransferred: {2}", e.LastOperation, e.SocketError, e.BytesTransferred);
        return;
    }

    var thread = Thread.CurrentThread;
    if (_capturedThreadIds.Add(thread.ManagedThreadId))
        Console.WriteLine("New thread, ManagedId: " + thread.ManagedThreadId + ", NativeId: " + GetCurrentThreadId());

    //Console.WriteLine(e.BytesTransferred);

    Receive((Socket)sender, e);
}

应用程序的线程行为非常奇特:

  1. SocketAsyncEventsArgsCompleted方法经常在新线程中运行。我本来以为经过一段时间后不会再创建新的线程,因为有线程池(或IOCP线程池),而且吞吐量非常稳定,所以我本来以为线程将被重用。
  2. 线程数量保持较低水平,但我可以在进程资源管理器中看到线程频繁地被创建和销毁。同样地,我不认为线程应该被创建或销毁。

你能解释一下应用程序的行为吗?

编辑:低吞吐量为每秒20个消息(大约200 KB/s)。如果我将吞吐量增加到每秒1000条以上(50 MB/s),应用程序的行为不会改变。


我的猜测是,这取决于接收数据之间的时间间隔以及线程在池中保持活动的时间? - Parimal Raj
同样的猜测在这里 - 我认为线程不会永远保留在池中。 - Maciej
你认为为什么线程的创建/销毁必须明确地与套接字请求有关? - Yuval Itzchakov
该应用程序是一个简单的原型,只有一个运行RunClient的主函数。线程的创建和销毁只能与SocketAsyncEventArgs相关联。 - cao
1
@cao 好的,这确实可能是性能问题。如果您对延迟非常敏感,考虑使用 PInvoke 调用套接字 API。根据您需要多少功能,这更多或更少需要工作量。或者,只需使用同步 IO。它使用更少的 CPU 并具有较低的延迟。但是,当线程数量增加时,它开始失效。在这里,“许多”是一个非常广泛的术语。 - usr
显示剩余7条评论
2个回答

6
低应用程序吞吐量本身无法解释线程的创建和销毁。套接字每秒接收20条消息,这足以使线程保持活动状态(等待线程在闲置10秒后被销毁)。
这个问题与线程池的“线程注入”相关,即线程的创建和销毁策略。线程池线程定期注入并销毁,以衡量新线程对线程池吞吐量的影响。
这被称为“线程探测”。在Channel 9视频CLR 4 - Inside the Thread Pool(跳转至26:30)中有清楚的解释。
似乎线程探测总是使用新创建的线程而不是将线程移入和移出线程池。我想大多数应用程序都更适合这种方式,因为它避免了保持未使用的线程保持活动状态。

2

来自MSDN

从 .NET Framework 4 开始,线程池会创建和销毁工作线程以优化吞吐量,即单位时间内完成的任务数。太少的线程可能不能充分利用可用资源,而太多的线程可能会增加资源争用。

注意

当需求较低时,线程池线程的实际数量可能会低于最小值。

基本上听起来你的低吞吐量导致线程池销毁线程,因为它们没有被使用,只是占用资源。不用担心。微软明确指出:

在大多数情况下,线程池采用自己的算法来分配线程可以获得更好的性能。

如果你真的很在意,你可以随时轮询ThreadPool.GetAvailableThreads()来监视线程池,并查看不同的网络吞吐量如何影响它。


2
谢谢您指向 MSDN 文章。我也阅读了它。这是了解应用程序行为的好开始。但是,您的答案并不能满足我,因为增加吞吐量并不能消除线程问题。 - cao

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接