【问题标题】:Implicit vs. Explicit .NET Threading隐式与显式 .NET 线程
【发布时间】:2016-10-05 13:39:49
【问题描述】:

所以当我在任务管理器中查看我的套接字服务器解决方案时,我注意到应用程序正在创建和销毁线程(9 到 10 到 15 到 10)。其中一些可以被 MySQL 连接器使用,但似乎登录到服务器会创建一个新线程。

我使用异步套接字来接受一个连接,将它传递给 PacketHandler 实例,同时监听同时的连接。我的问题是,我如何知道哪些代码会创建一个新线程?我从来没有明确写过需要创建一个线程,但这似乎是使用异步套接字的结果。我知道你在创建线程时应该保守一点(Cores * 2 = target # of threads),但是当你不知道什么代码会自然地创建一个新线程时,这是一项艰巨的任务。

【问题讨论】:

  • 不要担心不是你的代码。它会按照它的工作方式工作;你无法控制它。如果真的有问题,那就找另一个第三方库。
  • 但这是我的代码..
  • 我的意思是你写的代码。您是否明确使用线程?我不是指您正在使用的代码,例如框架代码或第三方代码。
  • 我没有使用任何框架或第三方代码,也没有明确使用线程。我只是使用 BeginAccept 和 EndAccept 代码,以及他们的朋友 Begin/EndSend、Begin/EndReceive :P
  • 如果您使用的是 C#,那么您使用的是框架。在您不知情的情况下,它将使用它认为合适的线程。谁在乎?只要相信它会以最有效的方式这样做。

标签: c# multithreading sockets asyncsocket


【解决方案1】:

正确的异步代码只使用线程进行回调,这通常只需要线程池线程,除非你做错了什么,否则不需要启动新的线程池线程。

在 MS.NET 中当前的 Sockets 实现肯定是这种情况。它们在异步事件上注册回调,并且在该回调期间使用线程池线程。除非您在回调中执行同步 I/O 操作,否则在线程池上创建更多线程是没有意义的。

不要查看任务管理器 - 查看调试器。您看到的线程很可能与您正在使用的库无关。 .NET 中有相当多的基础设施线程被创建和处理——特别是垃圾收集器。检查这些线程上的堆栈跟踪,您就会知道它们在做什么。

编辑: 在这种情况下,似乎 MySQL 连接器确实不使用异步 I/O。它只是使用多线程假装同步 I/O 是异步的 - 换句话说,异步 API 完全是无用的废话 :) 而且由于他们使用线程池线程进行同步 I/O,你会得到很多问题。要么使用不同的库,要么避免使用异步 API - 它在骗你。

【讨论】:

  • 好的。非常感谢!理解线程仍然是新手,我只是很好奇这些线程是如何在与我类似的应用程序中使用的。
  • @Fuselight 如果您做的每件事都 100% 正确,那么您需要的线程永远不会超过 CPU 可以同时执行的数量。在大多数应用程序中,由于各种原因,通常会有更多的线程——简单、隔离、互操作、控制……同步 I/O 更容易使用,尤其是没有像await 这样的高级语言特性。多线程和异步代码的整个领域相当复杂,而且要正确处理也不是一件容易的事——如果你想做任何实质性的事情,你真的想选择一本关于这个主题的书。
  • @Luaan OP 正在询问 MySQL 连接器,它使用 Task.Run 伪造异步操作。 This alternative project 提供真正的异步操作并且在 .NET Core 上运行,在 Ubuntu 上测试
  • @PanagiotisKanavos 这使得它“不正确异步”,这就是为什么我建议 OP 使用调试器来查看实际发生的情况。我只针对正确的异步 API,包括提到的 Socket API Fuselight。但我会将其包含在答案中,谢谢:)
  • @FuseLight 与其他数据库,使用async 版本就像使用同步数据库一样简单。差异确实很重要,因为异步允许您使用 更小的 VM 实例来处理相同的流量。
【解决方案2】:

不幸的是,MySQL 连接器不提供真正的异步方法。它的Begin/End 方法通过wrapping the synchronous version 在线程中伪造异步执行:

public IAsyncResult BeginExecuteReader(CommandBehavior behavior)

{

  if (caller != null)

    Throw(new MySqlException(Resources.UnableToStartSecondAsyncOp));



  caller = new AsyncDelegate(AsyncExecuteWrapper);

  asyncResult = caller.BeginInvoke(1, behavior, null, null);

  return asyncResult;

}

AsyncExecuteWrapper 在哪里:

internal object AsyncExecuteWrapper(int type, CommandBehavior behavior)

{

  thrownException = null;

  try

  {

    if (type == 1)

      return ExecuteReader(behavior);

    return ExecuteNonQuery();

  }

  catch (Exception ex)

  {

    thrownException = ex;

  }

  return null;

}

因此,他们浪费了等待响应的线程。三年前提交了一个错误but never got a real answer

这就是为什么创建了这个替代的MySQLConnector 项目,它提供真正的异步操作以及.NET Core 支持,例如this method

    internal async Task<int> ExecuteNonQueryAsync(IOBehavior ioBehavior, CancellationToken cancellationToken)
    {
        using (var reader = (MySqlDataReader) await ExecuteReaderAsync(CommandBehavior.Default, ioBehavior, cancellationToken).ConfigureAwait(false))
        {
            do
            {
                while (await reader.ReadAsync(ioBehavior, cancellationToken).ConfigureAwait(false))
                {
                }
            } while (await reader.NextResultAsync(ioBehavior, cancellationToken).ConfigureAwait(false));
            return reader.RecordsAffected;
        }
    }

你会看到ReadAsync也是一个合适的异步方法

差异很大,尤其是在 Web 应用程序中,因为它允许您使用 更小的 VM 实例来处理相同的流量。或者同一个虚拟机可以提供更多的流量。无论如何,价格差异是真实存在的。

这是因为异步网络操作实际上已卸载到驱动程序或主机。 IO 线程池中的线程仅在网络驱动程序向应用程序传递响应时使用。在半虚拟化的情况下,卸载可以一直到主机。

另一方面,伪造的同步操作会阻塞,甚至可能导致忙等待。这是因为同步原语旨在用于...同步访问共享资源。挂起线程成本,因此等待原语首先从自旋锁开始,并且仅在经过一定时间后才挂起线程。

这就是为什么不考虑异步的 Web 应用程序最终会在等待远程响应时消耗大量 CPU

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-09-07
    • 1970-01-01
    • 2011-06-15
    • 2010-09-24
    • 2010-10-21
    • 2019-01-31
    相关资源
    最近更新 更多