代码之家  ›  专栏  ›  技术社区  ›  Kivan

rest-api的异步方法如何减少线程数量?

  •  1
  • Kivan  · 技术社区  · 10 年前

    许多人说现代rest api应该是“异步”的,作为主要论点,他们说在某些平台上,例如在Java中,“阻塞”方式会产生许多线程,而“异步”方式允许限制线程数和开销。

    我不明白的是,它是如何实现的。

    假设我有一个类似vert的框架中的应用程序。x(但实际上这无关紧要,您也可以考虑NodeJS),比如对数据库发出请求的服务的1_000_000并发连接。该框架允许在长任务i|o操作中异步处理每个请求本身,因此数据库数据交换在业务逻辑代码中看起来在语法上是异步的。但是。据我所知,数据库请求不是在真空中发出的,而是在其他线程中处理的,该线程实际上会阻塞,直到数据库请求完成。因此,这意味着尽管事实上请求业务逻辑看起来是异步的和非阻塞的,但从这种逻辑调用的长时间操作实际上在框架的某个地方阻塞了,而且这样的操作做得越多,无论如何应该消耗的线程就越多(对于NodeJS,您可以认为线程是在框架本身的C++代码中创建的)

    因此,正如我所看到的大局——在异步方法中,只有一个线程处理所有请求,这没关系,但有一堆线程,它们在后台执行实际的I/O工作,如果不限制它们的数量,那么线程的数量将与阻塞方法+1的线程数量相同。另一方面,如果以编程方式限制后台线程池的数量,那么与阻塞方法相比,将用户请求队列和请求处理线程数量限制相结合的阻塞方法有什么好处?

    2 回复  |  直到 10 年前
        1
  •  2
  •   slebetman    10 年前

    既然你问的是一个相当低级的问题,我就用一个低级的答案来回答。希望你对C很满意。

    首先,免责声明:我将主要讨论网络代码,因为我所知道的唯一广泛使用的数据库是sqlite。由于您正在询问postgres,我可以假设您对套接字I/O(无论是TCP套接字还是unix本地套接字)如何只使用一个线程感兴趣。

    几乎所有异步系统和库的核心都是一段代码,如下所示:

    while (1)
    {
      read_fd_set = active_fd_set;
    
      // This blocks until we receive a packet or until timeout expires:
      select(FD_SETSIZE, &read_fd_set, NULL, NULL, timeout);
    
      // Process timed events:
      timeout = process_timeout();
    
      // Process I/O:
      for (i = 0; i < FD_SETSIZE; ++i) {
        if (FD_ISSET(i, &read_fd_set)) {
          if (i == sock) {
            /* Connection arriving on listening socket */
            int new;
            size = sizeof(clientname);
            new = accept (sock,(struct sockaddr *) &clientname, &size);
            FD_SET (new, &active_fd_set);
          }
          else {
            /* Data arriving on an already-connected socket. */
            if (read_from_client(i) < 0) {
              close (i);
              FD_CLR (i, &active_fd_set);
            }
          }
        }
      }
    }
    

    (代码示例从 GNU socket programming example )

    if (FD_ISSET...) 声明)。

    非I/O事件在逻辑上只能来自定时事件。这就是超时管理(为清晰起见,未显示详细信息)的用武之地。所有与I/O相关的东西(基本上是所有异步代码)都会从 read_from_client()

    没有并行运行的代码。

    并行化从何而来?

    基本上就是你要连接的服务器。大多数数据库都支持某种形式的并行。有些支持多线程。有些甚至支持节点。js或vert。x风格的并行性,支持异步磁盘I/O(如postgres)。一些 配置 of数据库通过分区和/或分片和/或主/从服务器将数据存储在多个服务器上,从而实现更高级别的并行性。

    硬件。这主要与DMA有关,DMA可以在没有CPU的情况下传输数据。DMA不是一回事。它更像一个概念。不同的系统,如PCI总线、SATA、USB,甚至CPU RAM总线本身都有各种DMA,可以直接将数据传输到RAM(对于RAM,可以将数据传输至更高级别的CPU缓存)或更快的缓冲区。

    等待DMA完成时。CPU没有做任何事情。虽然它什么也不做,但碰巧有一个网络数据包传入或 setTimeout() 处理它们的代码到期可以在CPU上执行。当一个文件被读入RAM时。

    但节点。js文档不断提到I/O线程

    仅用于磁盘I/O。使用单个线程执行异步磁盘I/O并非不可能。Tcl已经这样做了多年,许多其他编程语言和框架也这样做了。这是非常混乱的,因为BSD与Linux的做法不同,Linux与Windows的做法也不同,甚至OSX可能与BSD略有不同,尽管它是从BSD派生出来的,等等。

    为了简单和可靠,节点开发人员选择在单独的线程中处理磁盘I/O。

    注意,即使对于套接字I/O,它也不像我上面给出的代码示例那么简单。自从 select() 有一些限制(例如,您被迫循环访问所有套接字以检查传入数据,即使大多数套接字没有传入数据),人们已经想出了更好的API。显然,不同的操作系统做得不同。这就是为什么创建了许多库来处理跨平台事件处理,如libevent和libuv(node.js使用的一个)。

    异步、面向事件的系统不能 自动地 给你表现的超能力。他们给你的是选择:应用服务器正在飞速发展,所以你把数据库服务器放在哪里,你用什么数据库。

    好的。但我可以用线来做。为什么是异步的?

    基准。

    自1999年以来,许多人已经运行了许多基准测试,在大多数情况下,单线程(或低线程数)、面向事件的系统都优于简单的多线程系统。在单CPU、单核服务器的旧时代尤其如此。现在它仍然部分正确(因为核心仍然有限)。

    这就是为什么Apache被重新写入Apache2以使用异步侦听器的线程池,以及为什么Nginx被从头开始编写以使用异步代码的线程池。

    是的,理想情况下,在现代服务器上,为了使用所有CPU,您仍然需要一些线程。另一种方法是一个进程池,类似于集群模块在node.js中的工作方式。但您可能希望线程/进程的数量保持不变或尽可能保持不变,以避免上下文切换和线程创建的开销。

        2
  •  0
  •   Aleosha    10 年前

    在某些异步框架中,JDBC客户端仍然是同步的,这一点是正确的。 在Vert中查询DB时。x重用相同的应用程序线程。 请参见以下示例:

    @Test
    public void testMultipleThreads() throws InterruptedException {
        Vertx vertx = Vertx.vertx();
    
        System.out.println("Before starting server: " + Thread.activeCount());
    
        // Start server
        vertx.createHttpServer().
                requestHandler(httpServerRequest -> {
                    //      System.out.println("Request");
                    httpServerRequest.response().end();
                }).
                listen(8080, o -> {
                    System.out.println("Server ready");
                });
    
        // Start counting threads
        vertx.setPeriodic(500, (o) -> {
            System.out.println(Thread.activeCount());
        });
    
        // Create requests
        HttpClient client = vertx.createHttpClient();
    
        int loops = 1_000_000;
        CountDownLatch latch = new CountDownLatch(loops);
    
        for (int i = 0; i < loops; i++) {
            client.getNow(8080, "localhost", "/", httpClientResponse -> {
                // System.out.println("Response received");
                latch.countDown();
            });
        }
    
        latch.await();
    }
    

    您会注意到,线程的数量没有变化,即使您提供了您想要的任意数量的连接。也可以添加垂直。x JDBC客户端进行测试。

    推荐文章