`

原 异步非阻塞机制与多线程阻塞机制在处理并发耗时等待任务上的效率对比分析

 
阅读更多
原文地址:http://my.oschina.net/mallon/blog/224373


应用服务器的性能分析是复杂的,关注点很多。比如典型场景Web服务器+数据库,底层网络链路和网络硬件性能姑且不论,单看:Web服务器对静态文件的读写与磁盘和文件系统IO性能紧密相关;对数据的处理和数据库性能相关;而高并发访问则关系到操作系统的线程、网络套接字以及异步网络模型的效率。

在数据量大的情况下,数据库的性能成为一个至关重要的因素,随之带来Web服务器等待数据库的时间。在此基础上如果有大量的用户同时访问,那么会对Web服务器带来什么样的影响?以下主要讨论这个问题。

对于并发访问的处理,一般有两种处理机制:异步非阻塞机制、多线程阻塞机制(介绍略)。在测试选择上,前者使用基于Python的Tornado服务器,而后者使用基于Java的Tomcat服务器。注意:本文并非讨论开发语言的优劣,事实上,新版本的Java也支持异步机制,甚至高性能的epoll等。

测试工具:变态级的http_load

测试方法:使用该工具模拟1、10、100、1000个客户端并发访问以下场景,每次测试时间1分钟,得到服务器端每秒的总响应数。注意:由于Tomcat最大线程的限制(下面有提到)以及操作系统对端口数量的限制,1000个并发已经能够得到明显的结论了。

测试场景:

静态文件的读写。一个html文件和一大一小两个图片,大小分别为676k、1.6M和12k,使用http_load工具随机读取。静态文件读写的耗时可以忽略不计的。
模拟一个耗时操作,比如数据库操作。注意:耗时操作并不占用Web服务器本身的资源,它更多地体现的是Web服务器对并发访问处理的“合理”性。
以下是Java Servlet和Tornado服务的源代码:

Servlet

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

@WebServlet(name = "Dispatcher", urlPatterns = "/index")
public class index  extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        PrintWriter out = resp.getWriter();
        resp.setContentType("text/plain;charset=UTF-8");
        try {
            // 超时设置
            Thread.sleep(10000);
            out.println("OK");
        } catch (Exception ex) {
            resp.setStatus(500);
            ex.printStackTrace(out);
        } finally {
            out.flush();
            out.close();
        }
    }
}
Tornado

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#!/usr/bin/env python3
# -*- coding: UTF-8 -*-


import logging
import os
import sys

import tornado.gen
import tornado.ioloop
import tornado.web


def log_function(handler):
    pass


class IndexHandler(tornado.web.RequestHandler):
    @tornado.web.asynchronous
    def get(self, *args, **kwargs):
        ioloop = tornado.ioloop.IOLoop.instance()
        # 超时设置
        ioloop.add_timeout(ioloop.time() + 10, self.done)

    def done(self):
        self.set_header('Content-Type', 'text/plain;charset=UTF-8')
        self.write('OK')
        self.finish()


log_format = '%(levelname)s %(module)s.%(funcName)s[%(lineno)d] %(message)s'
logging.basicConfig(format=log_format, level=logging.INFO)

root_dir = os.path.dirname(os.path.realpath(sys.argv[0]))
os.chdir(root_dir)

handlers = [
    ('/', tornado.web.RedirectHandler, {'url': '/index.html'}),
    ('/index', IndexHandler),
    ('/(.*)', tornado.web.StaticFileHandler, {'path': ''}),
]

settings = {
    'log_function': log_function,
    'debug': False,
}

application = tornado.web.Application(handlers, **settings)
port = 8080
application.listen(port)
tornado.ioloop.IOLoop.instance().start()
测试结果如下:



单看数据总是云里雾里的。下面进行分析。

首先分析静态内容,如果作成图表,那么是这样的:



对于单客户端访问,两者效率相差无几,从这一点上可以看出:开发语言的效率所占的比重是微乎其微的:大凡非计算密集型的应用,IO操作总是短板,相比CPU的速度有数量级的差距。

当客户端数量增多,图表呈现出以下两个特点:

两者的每秒响应数趋稳;
Tomcat的效率大致是Tornado的3-4倍。
如果查看CPU占用:
Tomcat



Tornado



可以看到,Tomcat基本把四个核都占满了,而Tornado只占用了一个核。其中的道理其实是很清楚的:

Tomcat是多线程处理并发访问的,势必会最大限度地占用CPU,而Tornado异步机制是单线程的。
每秒处理能力还是有上限的,这就是每秒响应数趋稳的原因。如果没有缓存直接访问磁盘,那么Tomcat和Tornado的性能应该相差无几,事实上操作系统和Web服务器对文件的读写不可能没有缓存。而这个结果告诉我们在适当的线程数量范围内,文件的缓存读效率是恒定的。Tomcat和Tornado3-4倍的性能差距原因大致就在此。
静态文件的耗时非常少,所以Tomcat的每一个线程均能够很快执行完毕,线程带来的问题并不是那么明显。那么把耗时增大,会发生什么?
把程序中的延时加到1、3、10秒,每秒总响应发生了非常大的变化。此时如果再像上面静态文件分析,已经没有意义了。换一个分析方式:

把并发连接数除以服务器端每秒总响应数,可以得到单个客户端单次访问的平均时间。把这个平均时间再除以人为添加的那个延时,就得到一个比率。这个比率反映出Web服务器作为客户端和数据库(典型场景)之间的“中介”“拉皮条”的效率。这个效率的最佳值是1,如果大于1,那么就表示该“中介”把时间浪费在毫无意义的地方。

这样得到的图就很有意思了:



可以看到,100并发以内,Tomcat和Tornado的比率均在1左右;100到1000并发,Tornado继续保持1,而Tomcat在某个点之后比率就急剧增加了。这是什么原因?

如果使用VisualVM查看Tomcat的线程会发现,它的线程数达到220之后,就不再增加了:



很明显,Tomcat限制了线程数量(应该有参数可以配置的,我没有查资料)。

我没有更进一步做测试,但是可以相信,图表中Tomcat的拐点就在220处。

那么能不能一味地增加线程呢?这已经不需要我多讲了,学过操作系统的同志都知道,CPU的每一颗核上只能执行一条指令序列,线程只是CPU频繁切换造成的“假象”。随着线程的增加,切换时间占的比重将会越来越大。更多的线程除了给系统带来毫无意义的消耗没有其它任何用处。这也是Tomcat不敢把默认线程数设得太大的原因吧。

网上也有其它异步非阻塞与多线程性能的比较,这里就不转载了。

所以呢,以下就是结论了:

对并发耗时等待任务的处理,单线程异步非阻塞方式明显比无限制的阻塞多线程更“合理”,注意这里只谈合理性:用一个线程就能达到同样的效果,为什么要开多线程呢?
对非耗时任务,多线程能不能完全发挥效率也得看场景。即便IO不是短板,理论上线程数也应该低于CPU核数。
分享到:
评论

相关推荐

    java 同步、异步、阻塞和非阻塞分析.docx

    Java 中的同步、异步、阻塞和非阻塞是四个相关但不同的概念,它们都是在多线程编程中解决耗时操作的方法。在这里,我们将详细介绍这些概念之间的区别和联系。 同步(Synchronous) 同步是指在程序中,一个任务执行...

    JAVA语言异步非阻塞设计模式.docx

    异步非阻塞设计模式是现代软件开发中提高性能的关键技术,尤其在处理I/O密集型任务时,它能显著提升系统的并发能力和资源利用率。在这种模型中,系统在发起耗时操作后不会等待结果,而是继续执行其他任务。等到结果...

    C#多线程不阻塞

    本文主要探讨了如何在C#中实现非阻塞的异步方法调用,以避免线程阻塞,从而提高程序效率。 首先,我们需要理解同步与异步调用的区别。同步方法调用是传统的调用方式,当一个线程调用一个函数,如`Foo()`,它会等待...

    Django异步任务线程池实现原理

    当Django应用在处理耗时的任务时,通常会阻塞主线程,导致用户在等待处理结果时无法进行其他操作。为了解决这个问题,Django采用了异步任务执行的方法。主线程在接收到耗时任务请求后,不会直接处理这个任务,而是将...

    C#Winform异步多线程和线程池集成的用法

    本文将深入探讨如何在Winform应用中使用异步多线程和线程池。 一、线程基础 线程是操作系统分配CPU时间的基本单元,每个进程至少包含一个线程。在C#中,可以使用`System.Threading.Thread`类来创建和管理线程。通过...

    Labview2015多线程异步调用工程

    在“Labview2015多线程异步调用工程”中,我们探讨的核心是利用多线程和异步调用来提高程序执行效率和并发能力。这个工程的目的是让主线程能持续快速地进行轮询,同时多个子线程可以并行处理耗时的任务,从而优化...

    实现Windows下异步串口通讯详解(C语言)

    - **多任务处理**:异步通讯允许在同一串口上同时执行读写操作,或者在不同句柄上执行I/O操作,这使得应用程序能够更加高效地管理多个数据流。 - **优化数据传输**:通过合理安排读写操作,可以有效地减少等待时间...

    CVI学习文件-多线程 异步定时器(修改增加学习版)

    在多线程环境下,异步定时器尤其有用,因为它可以在线程等待或执行其他任务时,在后台自动执行任务。CVI中的异步定时器可能使用了底层操作系统提供的API,如Windows的SetTimer函数或POSIX的alarm函数。设置定时器后...

    Java多线程实现异步调用实例

    在本实例中,我们将深入探讨如何使用Java实现多线程以实现异步调用,并理解其背后的机制。 首先,多线程允许一个程序同时执行多个任务。在Java中,我们可以通过继承`Thread`类或实现`Runnable`接口来创建线程。在这...

    Java多线程之异步Future机制的原理和实现共5页.p

    异步Future机制是Java多线程中用于提升效率和优化资源利用的一种设计模式,它允许我们在主线程中不等待子线程的执行结果,而是通过Future对象来获取子线程的计算结果。这种方式极大地提高了程序的响应速度,尤其是在...

    C#中异步和多线程的区别

    标题中提到的C#中的异步和多线程的区别是一个非常重要的话题,尤其是在处理需要进行耗时操作的场景时,开发者必须了解何时应该使用异步编程模式,何时应该使用多线程。以下详细知识点的梳理,将帮助开发者深入理解这...

    thrift阻塞与非阻塞模式下的测试

    - **多线程阻塞**: 为了解决单线程阻塞的问题,服务器可以使用多线程,每个线程处理一个请求,从而在一定程度上提高并发能力。但是,线程创建和销毁开销以及上下文切换仍会影响性能。 2. **非阻塞模式**: - **...

    Spring 异步多线程动态任务处理的使用心得

    总结,Spring提供的异步多线程处理功能为开发者提供了强大而灵活的工具。通过合理配置TaskExecutor、使用`@Async`和`@Scheduled`注解,我们可以轻松实现高效的并发任务和定时任务处理。在实践中,我们应根据具体业务...

    c# 多线程(轮询,等待,回调)操作实例

    在C#编程中,多线程技术是一种关键的并发处理方式,它允许程序同时执行多个任务,提升系统效率。在本实例中,我们将探讨如何利用C#实现多线程,特别是涉及轮询、等待和回调的异步操作,这对于理解和应用多线程编程至...

    AsyncTaskDemo异步消息处理机制

    在Android开发中,异步操作是必不可少的一部分,特别是在更新UI或者执行耗时任务时,我们需要避免阻塞主线程。AsyncTask就是Android提供的一种轻量级的异步处理工具,它使得在子线程中处理数据并在UI线程更新结果变...

    多线程异步调用(并参递参数)

    尤其在处理耗时操作,如网络请求、大数据计算或者I/O密集型任务时,多线程和异步调用能够充分利用现代多核处理器的资源,避免程序阻塞,提升用户体验。本文将深入探讨“多线程异步调用(并参递参数)”这一主题,...

    c#线程同步与异步编程

    在C#编程中,线程同步与异步是并发编程中的关键概念,它们涉及到如何有效地管理多线程环境中的资源和执行顺序。本教程将深入探讨这两个概念,以帮助初学者更好地理解和应用。 同步编程是程序执行的一种方式,其中...

    异步多线程Demo

    异步多线程结合了两者的优点,通过异步调用实现线程间的非阻塞通信,提升系统效率。 4. **线程的创建**:在Java中,可以通过`Thread`类的子类化或实现`Runnable`接口来创建线程;在Python中,可以使用`threading`...

    C#多线程与异步的区别

    通过对C#中多线程与异步操作的深入分析,我们可以看出,虽然两者都能有效提高程序性能,但在具体的应用场景和技术细节上存在显著差异。选择合适的并发模型对于开发高性能、高响应性的应用至关重要。理解这两种技术的...

    Java 异步回调机制实例分析

    Java异步回调机制是一种在程序设计中用于处理异步操作的方法...总结来说,Java异步回调机制是提高程序性能和响应能力的重要工具,尤其在处理耗时操作时。理解并掌握这一机制,对于编写高效、可扩展的Java代码至关重要。

Global site tag (gtag.js) - Google Analytics