澳门新浦京手机版浅谈Go语言并发机制

理解应用程序的输入/输出(I/O)模型,意味着其在计划处理负载与残酷的实际使用场景之间的差异。若应用程序比较小,也没有服务于很高的负载,也许它影响甚微。但随着应用程序的负载逐渐上涨,采用错误的I/O模型有可能会让你到处踩坑,伤痕累累。

Go 语言相比Java等一个很大的优势就是可以方便地编写并发程序。Go 语言内置了
goroutine 机制,使用goroutine可以快速地开发并发程序,
更好的利用多核处理器资源。这篇文章学习goroutine 的应用及其调度实现。

Node.js异步I/O学习笔记,node.js学习笔记

“异步”这个名词的大规模流行是在Web
2.0浪潮中,它伴随着Javascript和AJAX席卷了Web。但在绝大多数高级编程语言中,异步并不多见。PHP最能体现这个特点:它不仅屏蔽了异步,甚至连多线程也不提供,PHP都是以同步阻塞的方式来执行。这样的优点利于程序猿顺序编写业务逻辑,但在复杂的网络应用中,阻塞导致它无法更好地并发。

在服务器端,I/O非常昂贵,分布式I/O更加昂贵,只有后端能快速响应资源,前端的体验才能变得更好。Node.js是首个将异步作为主要编程方式和设计理念的平台,伴随着异步I/O的还有事件驱动和单线程,它们构成Node的基调。本文将介绍Node是如何实现异步I/O的。

1. 基本概念

“异步”与“非阻塞”听起来似乎是一回事,从实际效果而言,这两者都达到了并行的目的。但是从计算机内核I/O而言,只有两种方式:阻塞与非阻塞。因此异步/同步和阻塞/非阻塞实际上是两回事。

1.1 阻塞I/O与非阻塞I/O

阻塞I/O的一个特点是调用之后一定要等到系统内核层面完成所有操作后,调用才结束。以读取磁盘上的一个文件为例,系统内核在完成磁盘寻道、读取数据、复制数据到内存中后,这个调用才结束。

阻塞I/O造成CPU等待I/O,浪费等待时间,CPU的处理能力不能得到充分利用。非阻塞I/O的特点就是调用之后会立即返回,返回后CPU的时间片可以用来处理其他事务。由于完整的I/O并没有完成,立即返回的并不是业务层期待的数据,而仅仅是当前调用的状态。为了获取完整的数据,应用程序需要重复调用I/O操作来确认是否完成(即轮询)。轮询技术要以下几种:

1.read:通过重复调用来检查I/O状态,是最原始性能最低的一种方式
2.select:对read的改进,通过对文件描述符上的事件状态来进行判断。缺点是文件描述符最大的数量有限制
3.poll:对select的改进,采用链表的方式避免最大数量限制,但描述符较多时,性能还是十分低下
4.epoll:进入轮询时若没有检查到I/O事件,将会进行休眠,直到事件发生将其唤醒。这是当前Linux下效率最高的I/O事件通知机制

轮询满足了非阻塞I/O确保获取完整数据的需求,但对于应用程序而言,它仍然只能算作一种同步,因为依然需要等待I/O完全返回。等待期间,CPU要么用于遍历文件描述符的状态,要么用于休眠等待事件发生。

1.2 理想与现实中的异步I/O

完美的异步I/O应该是应用程序发起非阻塞调用,无需通过轮询就可以直接处理下一个任务,只需在I/O完成后通过信号或回调将数据传递给应用程序即可。

现实中的异步I/O在不同操作系统下有不同的实现,如*nix平台采用自定义的线程池,Windows平台采用IOCP模型。Node提供了libuv作为抽象封装层来封装平台兼容性判断,并保证上层Node与下层各平台异步I/O的实现各自独立。另外需要强调的是我们经常提到Node是单线程的,这仅仅是指Javascript的执行在单线程中,实际在Node内部完成I/O任务的都另有线程池。

2. Node的异步I/O

2.1 事件循环

Node的执行模型实际上是事件循环。在进程启动时,Node会创建一个无限循环,每一次执行循环体的过程成为一次Tick。每个Tick过程就是查看是否有事件等待处理,如果有则取出事件及其相关的回调函数,若存在关联的回调函数则执行它们,然后进入下一个循环。如果不再有事件处理,就退出进程。

2.2 观察者

每个事件循环中有若干个观察者,通过向这些观察者询问来判断是否有事件要处理。事件循环是一个典型的生产者/消费者模型。在Node中,事件主要来源于网络请求、文件I/O等,这些事件都有对应的网络I/O观察者、文件I/O观察者等,事件循环则从观察者那里取出事件并处理。

2.3 请求对象

澳门新浦京手机版 ,从Javascript发起调用到内核执行完I/O操作的过渡过程中,存在一种中间产物,叫做请求对象。以最简单的Windows下fs.open()方法(根据指定路径和参数去打开一个文件并得到一个文件描述符)为例,从JS调用到内建模块通过libuv进行系统调用,实际上是调用了uv_fs_open()方法。在调用过程中,创建了一个FSReqWrap请求对象,从JS层传入的参数和方法都封装在这个请求对象中,其中我们最为关注的回调函数被设置在这个对象的oncompete_sym属性上。对象包装完毕后,将FSReqWrap对象推入线程池中等待执行。

至此,JS调用立即返回,JS线程可以继续执行后续操作。当前的I/O操作在线程池中等待执行,这就完成了异步调用的第一阶段。

2.4 执行回调

回调通知是异步I/O的第二阶段。线程池中的I/O操作调用完毕后,会将获取的结果储存起来,然后通知IOCP当前对象操作已完成,并将线程归还线程池。在每次Tick的执行中,事件循环的I/O观察者会调用相关的方法检查线程池中是否有执行完的请求,如果存在,会将请求对象加入到I/O观察者的队列中,然后将其当做事件处理。

澳门新浦京手机版 1

3. 非I/O的异步API

Node中还存在一些与I/O无关的异步API,例如定时器setTimeout()、setInterval(),立即异步执行任务的process.nextTick()和setImmdiate()等,这里略微介绍一下。

3.1 定时器API

setTimeout()和setInterval()浏览器端的API是一致的,它们的实现原理与异步I/O类似,只是不需要I/O线程池的参与。调用定时器API创建的定时器会被插入到定时器观察者内部的一棵红黑树中,每次事件循环的Tick都会从红黑树中迭代取出定时器对象,检查是否超过定时时间,若超过就形成一个事件,回调函数立即被执行。定时器的主要问题在于它的定时时间并非特别精确(毫秒级,在容忍范围内)。

3.2 立即异步执行任务API

在Node出现之前,很多人也许为了立即异步执行一个任务,会这样调用:

复制代码 代码如下:

setTimeout(function() {
    // TODO
}, 0);

由于事件循环的特点,定时器的精确度不够,而且采用定时器需要使用红黑树,各种操作时间复杂度为O(log(n))。而process.nextTick()方法只会将回调函数放入队列中,在下一轮Tick时取出执行,复杂度为O(1)更为高效。

此外还有一个setImmediate()方法和上述方法类似,都是将回调函数延迟执行。不过前者的优先级要比后者高,这是因为事件循环对观察者的检查是有先后顺序的。另外,前者的回调函数保存在一个数组中,每轮Tick会将数组中的所有回调函数全部执行完;后者结果保存在链表中,每轮Tick只会执行一个回调函数。

4. 事件驱动与高性能服务器

前面以fs.open()为例阐述了Node如何实现异步I/O。事实上对网络套接字的处理,Node也应用了异步I/O,这也是Node构建Web服务器的基础。经典的服务器模型有:

1.同步式:一次只能处理一个请求,其余请求都处于等待状态
2.每进程/每请求:为每个请求启动一个进程,但系统资源有限,不具备扩展性
3.每线程/每请求:为每个请求启动一个线程。线程比进程要轻量,但每个线程都占用一定内存,当大并发请求到来时,内存很快就会用光

著名的Apache采用的就是每线程/每请求的形式,这也是它难以应对高并发的原因。Node通过事件驱动方式处理请求,可以省掉创建和销毁线程的开销,同时操作系统在调度任务时因为线程较少,上下文切换的代价也很低。即使在大量连接的情况下,Node也能有条不紊地处理请求。

知名服务器Nginx也摒弃了多线程的方式,采用和Node一样的事件驱动方式。如今Nginx大有取代Apache之势。Nginx采用纯C编写,性能较高,但是它仅适合做Web服务器,用于反向代理或负载均衡等。Node可以构建与Nginx相同的功能,也可以处理各种具体业务,自身性能也不错。在实际项目中,我们可以结合它们各自有点,以达到应用的最佳性能。

正如大部分存在多种解决途径的场景一样,重点不在于哪一种途径更好,而是在于理解如何进行权衡。让我们来参观下I/O的景观,看下可以从中窃取点什么。

一、Go语言对并发的支持

Nodejs中有个Fibonacci的异步例子,疑问processnextTick()作用

你运行两个fibonacciAsync()就能看出是异步的了。
 

澳门新浦京手机版 2

使用goroutine编程

程序员怎说服老板采用Nodejs?

导读:近期以来Node.js在业界很火,有关它的的新闻不胜枚举,种种迹象表明业界更多的公司在关注和考虑采用Node.js。俗话说“巧妇难为无米之炊”,程序员该如何成功说服老板听取您的建议?针对这一话题,作者Felix发表了一篇博文,文中分享了一些建设性指南,CSDN研发频道现将此文进行编译,分享给开发者,也欢迎大家发表自己Node.js实战心得。糟糕的使用案例Apps在CPU性能上的高使用率
尽管一直钟情于Node.js,但这里有几个使用案例,结果却并不令人如意。最明显的是Apps在CPU上的使用率以及I/O操作是极其高负荷的。因此,如果你打算写一个视频编码软件,人工智能或者类似CPU使用率比较高的软件,那么请不要使用Node.js,使用C或者C++效果会更好一些。话虽如此,但Node.js允许你轻松的编写C++插件,因此,你可以将它作为一个超级算法的脚本引擎。简单的CRUD/HTML
AppsNode.js最终会成为一款不错的编写Web应用的工具。但是,你不能指望它能像PHP,Ruby,Python那样为你提供更多的好处。也许你的应用程序会因此而获得更多的可扩展性,但并不会因为用Node.js编写的而为你带来更多的访问量。当我们看到Node.js一些不错的框架时,或许你会因此而欣喜不已。事实上,至今还没有比Rails,CakePHP或者Django这些框架更具强大的应用功能。如果你的应用程序只是为了基于一些数据库给HTML做渲染,那么使用Node.js不会给你带来任何利益好处。NoSQL

  • Node.js + 各种时髦词
    假如你的下一个应用程序的系统架构读起来像NoSQL的配料菜谱,请花点时间阅读下面的内容。Redis,CouchDB,MongoDB,Riak,Casandra等这些看起来似乎很诱人,同样令人难以抗拒。如果你正在使用Node.js,那么就不应该附加上一些你完全不了解的技术。当然,也有选择一个文档数据库合理使用的案例。但是如果你想开发一个商业项目,请坚持保守的数据库技术(比如Postgres
    或者 MySQL)或许能满足你的需求。出色的使用案例JSON
    APIs创建一个轻量级的REST / JSON
    API这确实是Node.js的一大亮点。如果需要包装其他的数据源(如数据库)或者Web服务器通过JSON接口让他们暴露出来,那么将非阻塞I/O模块与JavaScript结合在一起是个不错的选择。单一的页面应用如果你打算写一个AJAX单一的页面应用(如Gmail),Node.js非常适合。在极短的响应时间内获得更多的请求数,在客户端和服务器之间共享数据,为现代Web应用程序在客户端上做大量的处理,Node.js都能满足你的需求。Unix工具
    Shelling out to unix
    tools目前Node.js还很年幼,它正试图为自己重新发明各类软件。不过更好的办法是深入到现有的广阔的命令行工具世界里。Node可以把这些成千上万的子进程以stream的方式输出,这也使它成为企业的理想选择。数据流Streaming
    data传统的Web栈将http请求和响应作为元事件处理。然而,他们是流动的,许多非常棒的Node.js应用程序正是利用这一优点创建的。这里有一个非常棒的案例,当进行实时解析上传文件时,还可以在不同的数据层之间创建代理。软件实时应用利用Node.js你可以轻松开发软件实时系统。比如Twitter、聊天工具,体彩或者即时通讯网络接口。但是,值得注意的是,因为JavaScript是一个动态的&#4……余下全文>>
     

“异步”这个名词的大规模流行是在Web
2.0浪潮中,它伴随着Javascript和AJAX席卷了Web。但在绝大多数高…

在这篇文章,我们将会结合Apache分别比较Node,Java,Go,和PHP,讨论这些不同的语言如何对他们的I/O进行建模,各个模型的优点和缺点,并得出一些初步基准的结论。如果关心你下一个Web应用的I/O性能,那你就找对文章了。

使用 go 关键字用来创建 goroutine
。将go声明放到一个需调用的函数之前,在相同地址空间调用运行这个函数,这样该函数执行时便会作为一个独立的并发线程。这种线程在Go语言中称作goroutine。

I/O基础知识:快速回顾

为了理解与I/O密切相关的因素,必须先来回顾在操作系统底层的概念。虽然不会直接处理这些概念的大部分,但通过应用程序的运行时环境你一直在间接地处理他们。而关键在于细节。

goroutine的用法如下:

系统调用

首先,我们有系统调用,它可以描述成这样:

  • 你的程序(在“用户区域”,正如他们所说的)必须让操作系统内核在它自身执行I/O操作。
  • “系统调用”(syscall)意味着你的程序要求内核做某事。不同的操作系统,实现系统调用的细节有所不同,但基本的概念是一样的。这将会有一些特定的指令,把控制权从你的程序转交到内核(类似函数调用但有一些专门用于处理这种场景的特殊sauce)。通常来说,系统调用是阻塞的,意味着你的程序需要等待内核返回到你的代码。
  • 内核在我们所说的物理设备(硬盘、网卡等)上执行底层的I/O操作,并回复给系统调用。在现实世界中,内核可能需要做很多事情才能完成你的请求,包括等待设备准备就绪,更新它的内部状态等,但作为一名应用程序开发人员,你可以不用关心这些。以下是内核的工作情况。

澳门新浦京手机版 3

//go 关键字放在方法调用前新建一个 goroutine 并执行方法体

go GetThingDone(param1, param2);



//新建一个匿名方法并执行

go func(param1, param2) {

}(val1, val2)



//直接新建一个 goroutine 并在 goroutine 中执行代码块

go {

  //do someting...

} 

阻塞调用与非阻塞调用

好了,我刚刚在上面说系统调用是阻塞的,通常来说这是对的。然而,有些调用被分类为“非阻塞”,意味着内核接收了你的请求后,把它放进了队列或者缓冲的某个地方,然后立即返回而并没有等待实际的I/O调用。所以它只是“阻塞”了一段非常短的时间,短到只是把你的请求入列而已。

这里有一些有助于解释清楚的(Linux系统调用)例子:-read()是阻塞调用——你传给它一个文件句柄和一个存放所读到数据的缓冲,然后此调用会在当数据好后返回。注意这种方式有着优雅和简单的优点。-epoll_create()epoll_ctl(),和epoll_wait()这些调用分别是,让你创建一组用于侦听的句柄,从该组添加/删除句柄,和然后直到有活动时才阻塞。这使得你可以通过一个线程有效地控制一系列I/O操作。如果需要这些功能,这非常棒,但也正如你所看到的,使用起来当然也相当复杂。

理解这里分时差异的数量级是很重要的。如果一个CPU内核运行在3GHz,在没有优化的情况下,它每秒执行30亿次循环(或者每纳秒3次循环)。非阻塞系统调用可能需要10纳秒这样数量级的周期才能完成——或者“相对较少的纳秒”。对于正在通过网络接收信息的阻塞调用可能需要更多的时间——例如200毫秒(0.2秒)。例如,假设非阻塞调用消耗了20纳秒,那么阻塞调用消耗了200,000,000纳秒。对于阻塞调用,你的程序多等待了1000万倍的时间。

澳门新浦京手机版 4

内核提供了阻塞I/O(“从网络连接中读取并把数据给我”)和非阻塞I/O(“当这些网络连接有新数据时就告诉我”)这两种方法。而使用何种机制,对应调用过程的阻塞时间明显长度不同。

因为 goroutine 在多核 cpu 环境下是并行的。如果代码块在多个 goroutine
中执行,我们就实现了代码并行。

调度

接下来第三件关键的事情是,当有大量线程或进程开始阻塞时怎么办。

出于我们的目的,线程和进程之间没有太大的区别。实际上,最显而易见的执行相关的区别是,线程共享相同的内存,而每个进程则拥有他们独自的内存空间,使得分离的进程往往占据了大量的内存。但当我们讨论调度时,它最终可归结为一个事件清单(线程和进程类似),其中每个事件需要在有效的CPU内核上获得一片执行时间。如果你有300个线程正在运行并且运行在8核上,那么你得通过每个内核运行一段很短的时间然后切换到下一个线程的方式,把这些时间划分开来以便每个线程都能获得它的分时。这是通过“上下文切换”来实现的,使得CPU可以从正在运行的某个线程/进程切换到下一个。

这些上下文切换有一定的成本——它们消耗了一些时间。在快的时候,可能少于100纳秒,但是根据实现的细节,处理器速度/架构,CPU缓存等,消耗1000纳秒甚至更长的时间也并不罕见。

线程(或者进程)越多,上下文切换就越多。当我们谈论成千上万的线程,并且每一次切换需要数百纳秒时,速度将会变得非常慢。

然而,非阻塞调用本质上是告诉内核“当你有一些新的数据或者这些连接中的任意一个有事件时才调用我”。这些非阻塞调用设计于高效地处理大量的I/O负载,以及减少上下文切换。

到目前为止你还在看这篇文章吗?因为现在来到了有趣的部分:让我们来看下一些流利的语言如何使用这些工具,并就在易用性和性能之间的权衡作出一些结论……以及其他有趣的点评。

请注意,虽然在这篇文章中展示的示例是琐碎的(并且是不完整的,只是显示了相关部分的代码),但数据库访问,外部缓存系统(memcache等全部)和需要I/O的任何东西,都以执行某些背后的I/O操作而结束,这些和展示的示例一样有着同样的影响。同样地,对于I/O被描述为“阻塞”(PHP,Java)这样的情节,HTTP请求与响应的读取与写入本身是阻塞的调用:再一次,更多隐藏在系统中的I/O及其伴随的性能问题需要考虑。

为项目选择编程语言要考虑的因素有很多。当你只考虑性能时,要考虑的因素甚至有更多。但是,如果你关注的是程序主要受限于I/O,如果I/O性能对于你的项目至关重要,那这些都是你需要了解的。“保持简单”的方法:PHP。

回到90年代的时候,很多人穿着匡威鞋,用Perl写着CGI脚本。随后出现了PHP,很多人喜欢使用它,它使得制作动态网页更为容易。

PHP使用的模型相当简单。虽然有一些变化,但基本上PHP服务器看起来像:

HTTP请求来自用户的浏览器,并且访问了你的Apache网站服务器。Apache为每个请求创建一个单独的进程,通过一些优化来重用它们,以便最大程度地减少其需要执行的次数(创建进程相对来说较慢)。Apache调用PHP并告诉它在磁盘上运行相应的.php文件。PHP代码执行并做一些阻塞的I/O调用。若在PHP中调用了file_get_contents(),那在背后它会触发read()系统调用并等待结果返回。

当然,实际的代码只是简单地嵌在你的页面中,并且操作是阻塞的:

<?php

// 阻塞的文件I/O
$file_data = file_get_contents('/path/to/file.dat');

// 阻塞的网络I/O
$curl = curl_init('http://example.com/example-microservice');
$result = curl_exec($curl);

// 更多阻塞的网络I/O
$result = $db->query('SELECT id, data FROM examples ORDER BY id DESC limit 100');

?>

关于它如何与系统集成,就像这样:

澳门新浦京手机版 5

相当简单:一个请求,一个进程。I/O是阻塞的。优点是什么呢?简单,可行。那缺点是什么呢?同时与20,000个客户端连接,你的服务器就挂了。由于内核提供的用于处理大容量I/O(epoll等)的工具没有被使用,所以这种方法不能很好地扩展。更糟糕的是,为每个请求运行一个单独的过程往往会使用大量的系统资源,尤其是内存,这通常是在这样的场景中遇到的第一件事情。

注意:Ruby使用的方法与PHP非常相似,在广泛而普遍的方式下,我们可以将其视为是相同的。

如果需要了解程序的执行情况,怎么拿到并行的结果呢?需要配合使用channel进行。

多线程的方式:Java

所以就在你买了你的第一个域名的时候,Java来了,并且在一个句子之后随便说一句“dot
com”是很酷的。而Java具有语言内置的多线程(特别是在创建时),这一点非常棒。

大多数Java网站服务器通过为每个进来的请求启动一个新的执行线程,然后在该线程中最终调用作为应用程序开发人员的你所编写的函数。

在Java的Servlet中执行I/O操作,往往看起来像是这样:

public void doGet(HttpServletRequest request,  
    HttpServletResponse response) throws ServletException, IOException
{

    // 阻塞的文件I/O
    InputStream fileIs = new FileInputStream("/path/to/file");

    // 阻塞的网络I/O
    URLConnection urlConnection = (new URL("http://example.com/example-microservice")).openConnection();
    InputStream netIs = urlConnection.getInputStream();

    // 更多阻塞的网络I/O
    out.println("...");
}

由于我们上面的doGet方法对应于一个请求并且在自己的线程中运行,而不是每次请求都对应需要有自己专属内存的单独进程,所以我们会有一个单独的线程。这样会有一些不错的优点,例如可以在线程之间共享状态、共享缓存的数据等,因为它们可以相互访问各自的内存,但是它如何与调度进行交互的影响,仍然与前面PHP例子中所做的内容几乎一模一样。每个请求都会产生一个新的线程,而在这个线程中的各种I/O操作会一直阻塞,直到这个请求被完全处理为止。为了最小化创建和销毁它们的成本,线程会被汇集在一起,但是依然,有成千上万个连接就意味着成千上万个线程,这对于调度器是不利的。

一个重要的里程碑是,在Java 1.4 版本(和再次显著升级的1.7
版本)中,获得了执行非阻塞I/O调用的能力。大多数应用程序,网站和其他程序,并没有使用它,但至少它是可获得的。一些Java网站服务器尝试以各种方式利用这一点;
然而,绝大多数已经部署的Java应用程序仍然如上所述那样工作。

澳门新浦京手机版 6

Java让我们更进了一步,当然对于I/O也有一些很好的“开箱即用”的功能,但它仍然没有真正解决问题:当你有一个严重I/O绑定的应用程序正在被数千个阻塞线程狂拽着快要坠落至地面时怎么办。

使用Channel控制并发

作为一等公民的非阻塞I/O:Node

当谈到更好的I/O时,Node.js无疑是新宠。任何曾经对Node有过最简单了解的人都被告知它是“非阻塞”的,并且它能有效地处理I/O。在一般意义上,这是正确的。但魔鬼藏在细节中,当谈及性能时这个巫术的实现方式至关重要。

本质上,Node实现的范式不是基本上说“在这里编写代码来处理请求”,而是转变成“在这里写代码开始处理请求”。每次你都需要做一些涉及I/O的事情,发出请求或者提供一个当完成时Node会调用的回调函数。

在求中进行I/O操作的典型Node代码,如下所示:

http.createServer(function(request, response) {  
    fs.readFile('/path/to/file', 'utf8', function(err, data) {
        response.end(data);
    });
});

可以看到,这里有两个回调函数。第一个会在请求开始时被调用,而第二个会在文件数据可用时被调用。

这样做的基本上给了Node一个在这些回调函数之间有效地处理I/O的机会。一个更加相关的场景是在Node中进行数据库调用,但我不想再列出这个烦人的例子,因为它是完全一样的原则:启动数据库调用,并提供一个回调函数给Node,它使用非阻塞调用单独执行I/O操作,然后在你所要求的数据可用时调用回调函数。这种I/O调用队列,让Node来处理,然后获取回调函数的机制称为“事件循环”。它工作得非常好。

澳门新浦京手机版 7

然而,这个模型中有一道关卡。在幕后,究其原因,更多是如何实现JavaScript
V8
引擎(Chrome的JS引擎,用于Node)1,而不是其他任何事情。你所编写的JS代码全部都运行在一个线程中。思考一下。这意味着当使用有效的非阻塞技术执行I/O时,正在进行CPU绑定操作的JS可以在运行在单线程中,每个代码块阻塞下一个。
一个常见的例子是循环数据库记录,在输出到客户端前以某种方式处理它们。以下是一个例子,演示了它如何工作:

var handler = function(request, response) {

    connection.query('SELECT ...', function (err, rows) {

        if (err) { throw err };

        for (var i = 0; i < rows.length; i++) {
            // 对每一行纪录进行处理
        }

        response.end(...); // 输出结果

    })

};

虽然Node确实可以有效地处理I/O,但上面的例子中的for循环使用的是在你主线程中的CPU周期。这意味着,如果你有10,000个连接,该循环有可能会让你整个应用程序慢如蜗牛,具体取决于每次循环需要多长时间。每个请求必须分享在主线程中的一段时间,一次一个。

这个整体概念的前提是I/O操作是最慢的部分,因此最重要是有效地处理这些操作,即使意味着串行进行其他处理。这在某些情况下是正确的,但不是全都正确。

另一点是,虽然这只是一个意见,但是写一堆嵌套的回调可能会令人相当讨厌,有些人认为它使得代码明显无章可循。在Node代码的深处,看到嵌套四层、嵌套五层、甚至更多层级的嵌套并不罕见。

我们再次回到了权衡。如果你主要的性能问题在于I/O,那么Node模型能很好地工作。然而,它的阿喀琉斯之踵(译者注:来自希腊神话,表示致命的弱点)是如果不小心的话,你可能会在某个函数里处理HTTP请求并放置CPU密集型代码,最后使得每个连接慢得如蜗牛。

Channels用来同步并发执行的函数并提供它们某种传值交流的机制。

真正的非阻塞:Go

在进入Go这一章节之前,我应该披露我是一名Go粉丝。我已经在许多项目中使用Go,是其生产力优势的公开支持者,并且在使用时我在工作中看到了他们。

也就是说,我们来看看它是如何处理I/O的。Go语言的一个关键特性是它包含自己的调度器。并不是每个线程的执行对应于一个单一的OS线程,Go采用的是“goroutines”这一概念。Go运行时可以将一个goroutine分配给一个OS线程并使其执行,或者把它挂起而不与OS线程关联,这取决于goroutine做的是什么。来自Go的HTTP服务器的每个请求都在单独的Goroutine中处理。

此调度器工作的示意图,如下所示:

澳门新浦京手机版 8

这是通过在Go运行时的各个点来实现的,通过将请求写入/读取/连接/等实现I/O调用,让当前的goroutine进入睡眠状态,当可采取进一步行动时用信息把goroutine重新唤醒。

实际上,除了回调机制内置到I/O调用的实现中并自动与调度器交互外,Go运行时做的事情与Node做的事情并没有太多不同。它也不受必须把所有的处理程序代码都运行在同一个线程中这一限制,Go将会根据其调度器的逻辑自动将Goroutine映射到其认为合适的OS线程上。最后代码类似这样:

func ServeHTTP(w http.ResponseWriter, r *http.Request) {

    // 这里底层的网络调用是非阻塞的
    rows, err := db.Query("SELECT ...")

    for _, row := range rows {
        // 处理rows
        // 每个请求在它自己的goroutine中
    }

    w.Write(...) // 输出响应结果,也是非阻塞的

}

正如你在上面见到的,我们的基本代码结构像是更简单的方式,并且在背后实现了非阻塞I/O。

在大多数情况下,这最终是“两个世界中最好的”。非阻塞I/O用于全部重要的事情,但是你的代码看起来像是阻塞,因此往往更容易理解和维护。Go调度器和OS调度器之间的交互处理了剩下的部分。这不是完整的魔法,如果你建立的是一个大型的系统,那么花更多的时间去理解它工作原理的更多细节是值得的;
但与此同时,“开箱即用”的环境可以很好地工作和很好地进行扩展。

Go可能有它的缺点,但一般来说,它处理I/O的方式不在其中。

通过channel传递的元素类型、容器(或缓冲区)和传递的方向由“<-”操作符指定。

谎言,诅咒的谎言和基准

对这些各种模式的上下文切换进行准确的定时是很困难的。也可以说这对你来没有太大作用。所以取而代之,我会给出一些比较这些服务器环境的HTTP服务器性能的基准。请记住,整个端对端的HTTP请求/响应路径的性能与很多因素有关,而这里我放在一起所提供的数据只是一些样本,以便可以进行基本的比较。

对于这些环境中的每一个,我编写了适当的代码以随机字节读取一个64k大小的文件,运行一个SHA-256哈希N次(N在URL的查询字符串中指定,例如.../test.php?n=100),并以十六进制形式打印生成的散列。我选择了这个示例,是因为使用一些一致的I/O和一个受控的方式增加CPU使用率来运行相同的基准测试是一个非常简单的方式。

关于环境使用,更多细节请参考这些基准要点。

首先,来看一些低并发的例子。运行2000次迭代,并发300个请求,并且每次请求只做一次散列(N
= 1),可以得到:

澳门新浦京手机版 9

时间是在全部并发请求中完成请求的平均毫秒数。越低越好。

很难从一个图表就得出结论,但对于我来说,似乎与连接和计算量这些方面有关,我们看到时间更多地与语言本身的一般执行有关,因此更多在于I/O。请注意,被认为是“脚本语言”(输入随意,动态解释)的语言执行速度最慢。

但是如果将N增加到1000,仍然并发300个请求,会发生什么呢 ——
相同的负载,但是hash迭代是之前的100倍(显着增加了CPU负载):

澳门新浦京手机版 10

时间是在全部并发请求中完成请求的平均毫秒数。越低越好。

忽然之间,Node的性能显着下降了,因为每个请求中的CPU密集型操作都相互阻塞了。有趣的是,在这个测试中,PHP的性能要好得多(相对于其他的语言),并且打败了Java。(值得注意的是,在PHP中,SHA-256实现是用C编写的,执行路径在这个循环中花费更多的时间,因为这次我们进行了1000次哈希迭代)。

现在让我们尝试5000个并发连接(并且N = 1)——
或者接近于此。不幸的是,对于这些环境的大多数,失败率并不明显。对于这个图表,我们会关注每秒的请求总数。越高越好

澳门新浦京手机版 11

每秒的请求总数。越高越好。

这张照片看起来截然不同。这是一个猜测,但是看起来像是对于高连接量,每次连接的开销与产生新进程有关,而与PHP
+
Apache相关联的额外内存似乎成为主要的因素并制约了PHP的性能。显然,Go是这里的冠军,其次是Java和Node,最后是PHP。

可以使用内置函数 make分配一个channel:

结论

综上所述,很显然,随着语言的演进,处理大量I/O的大型应用程序的解决方案也随之不断演进。

为了公平起见,暂且抛开本文的描述,PHP和Java确实有可用于Web应用程序的非阻塞I/O的实现。
但是这些方法并不像上述方法那么常见,并且需要考虑使用这种方法来维护服务器的伴随的操作开销。更不用说你的代码必须以与这些环境相适应的方式进行结构化;
“正常”的PHP或Java Web应用程序通常不会在这样的环境中进行重大改动。

作为比较,如果只考虑影响性能和易用性的几个重要因素,可以得到:

语言 线程或进程 非阻塞I/O 易用性
PHP 进程
Java 线程 可用
Node.js 线程
Go 线程(Goroutine)

线程通常要比进程有更高的内存效率,因为它们共享相同的内存空间,而进程则没有。结合与非阻塞I/O相关的因素,当我们向下移动列表到一般的启动时,因为它与改善I/O有关,可以看到至少与上面考虑的因素一样。如果我不得不在上面的比赛中选出一个冠军,那肯定会是Go。

即便这样,在实践中,选择构建应用程序的环境与你的团队对于所述环境的熟悉程度以及可以实现的总体生产力密切相关。因此,每个团队只是一味地扎进去并开始用Node或Go开发Web应用程序和服务可能没有意义。事实上,寻找开发人员或内部团队的熟悉度通常被认为是不使用不同的语言和/或不同的环境的主要原因。也就是说,过去的十五年来,时代已经发生了巨大的变化。

希望以上内容可以帮助你更清楚地了解幕后所发生的事件,并就如何处理应用程序现实世界中的可扩展性为你提供的一些想法。快乐输入,快乐输出!

i := make(chan int)    // by default the capacity is 0

s := make(chan string, 3) // non-zero capacity



r := make(<-chan bool)     // can only read from

w := make(chan<- []os.FileInfo) // can only write to 

配置runtime.GOMAXPROCS

使用下面的代码可以显式的设置是否使用多核来执行并发任务:

runtime.GOMAXPROCS()

GOMAXPROCS的数目根据任务量分配就可以,但是不要大于cpu核数。

配置并行执行比较适合适合于CPU密集型、并行度比较高的情景,如果是IO密集型使用多核的化会增加cpu切换带来的性能损失。

了解了Go语言的并发机制,接下来看一下goroutine 机制的具体实现。

二、区别并行与并发

进程、线程与处理器

在现代操作系统中,线程是处理器调度和分配的基本单位,进程则作为资源拥有的基本单位。每个进程是由私有的虚拟地址空间、代码、数据和其它各种系统资源组成。线程是进程内部的一个执行单元。
每一个进程至少有一个主执行线程,它无需由用户去主动创建,是由系统自动创建的。
用户根据需要在应用程序中创建其它线程,多个线程并发地运行于同一个进程中。

并行与并发

并行与并发(Concurrency and
Parallelism)是两个不同的概念,理解它们对于理解多线程模型非常重要。

在描述程序的并发或者并行时,应该说明从进程或者线程的角度出发。

  1. 并发:一个时间段内有很多的线程或进程在执行,但何时间点上都只有一个在执行,多个线程或进程争抢时间片轮流执行
  2. 并行:一个时间段和时间点上都有多个线程或进程在执行

非并发的程序只有一个垂直的控制逻辑,在任何时刻,程序只会处在这个控制逻辑的某个位置,也就是顺序执行。如果一个程序在某一时刻被多个CPU流水线同时进行处理,那么我们就说这个程序是以并行的形式在运行。

并行需要硬件支持,单核处理器只能是并发,多核处理器才能做到并行执行。

  1. 并发是并行的必要条件,如果一个程序本身就不是并发的,也就是只有一个逻辑执行顺序,那么我们不可能让其被并行处理。
  2. 并发不是并行的充分条件,一个并发的程序,如果只被一个CPU进行处理(通过分时),那么它就不是并行的。

举一个例子,编写一个最简单的顺序结构程序输出”Hello
World”,它就是非并发的,如果在程序中增加多线程,每个线程打印一个”Hello
World”,那么这个程序就是并发的。如果运行时只给这个程序分配单个CPU,这个并发程序还不是并行的,需要部署在多核处理器上,才能实现程序的并行。

三、几种不同的多线程模型

用户线程与内核级线程

线程的实现可以分为两类:用户级线程(User-LevelThread,
ULT)和内核级线程(Kemel-LevelThread,
KLT)。用户线程由用户代码支持,内核线程由操作系统内核支持。

多线程模型

多线程模型即用户级线程和内核级线程的不同连接方式。

(1)多对一模型(M : 1)

将多个用户级线程映射到一个内核级线程,线程管理在用户空间完成。
此模式中,用户级线程对操作系统不可见(即透明)。

澳门新浦京手机版 12

优点:
这种模型的好处是线程上下文切换都发生在用户空间,避免的模态切换(mode
switch),从而对于性能有积极的影响。

缺点:所有的线程基于一个内核调度实体即内核线程,这意味着只有一个处理器可以被利用,在多处理器环境下这是不能够被接受的,本质上,用户线程只解决了并发问题,但是没有解决并行问题。如果线程因为
I/O 操作陷入了内核态,内核态线程阻塞等待 I/O
数据,则所有的线程都将会被阻塞,用户空间也可以使用非阻塞而
I/O,但是不能避免性能及复杂度问题。

(2) 一对一模型(1:1)

将每个用户级线程映射到一个内核级线程。

澳门新浦京手机版 13

每个线程由内核调度器独立的调度,所以如果一个线程阻塞则不影响其他的线程。

优点:在多核处理器的硬件的支持下,内核空间线程模型支持了真正的并行,当一个线程被阻塞后,允许另一个线程继续执行,所以并发能力较强。

缺点:每创建一个用户级线程都需要创建一个内核级线程与其对应,这样创建线程的开销比较大,会影响到应用程序的性能。

(3)多对多模型(M : N)

内核线程和用户线程的数量比为 M : N,内核用户空间综合了前两种的优点。

澳门新浦京手机版 14

这种模型需要内核线程调度器和用户空间线程调度器相互操作,本质上是多个线程被绑定到了多个内核线程上,这使得大部分的线程上下文切换都发生在用户空间,而多个内核线程又可以充分利用处理器资源。

四、goroutine机制的调度实现

goroutine机制实现了M :
N的线程模型,goroutine机制是协程(coroutine)的一种实现,golang内置的调度器,可以让多核CPU中每个CPU执行一个协程。

理解goroutine机制的原理,关键是理解Go语言scheduler的实现。

调度器是如何工作的

Go语言中支撑整个scheduler实现的主要有4个重要结构,分别是M、G、P、Sched,
前三个定义在runtime.h中,Sched定义在proc.c中。

  1. Sched结构就是调度器,它维护有存储M和G的队列以及调度器的一些状态信息等。
  2. M结构是Machine,系统线程,它由操作系统管理的,goroutine就是跑在M之上的;M是一个很大的结构,里面维护小对象内存cache(mcache)、当前执行的goroutine、随机数发生器等等非常多的信息。
  3. P结构是Processor,处理器,它的主要用途就是用来执行goroutine的,它维护了一个goroutine队列,即runqueue。Processor是让我们从N:1调度到M:N调度的重要部分。
  4. G是goroutine实现的核心结构,它包含了栈,指令指针,以及其他对调度goroutine很重要的信息,例如其阻塞的channel。

Processor的数量是在启动时被设置为环境变量GOMAXPROCS的值,或者通过运行时调用函数GOMAXPROCS()进行设置。Processor数量固定意味着任意时刻只有GOMAXPROCS个线程在运行go代码。

参考这篇传播很广的博客:

我们分别用三角形,矩形和圆形表示Machine Processor和Goroutine。

澳门新浦京手机版 15

在单核处理器的场景下,所有goroutine运行在同一个M系统线程中,每一个M系统线程维护一个Processor,任何时刻,一个Processor中只有一个goroutine,其他goroutine在runqueue中等待。一个goroutine运行完自己的时间片后,让出上下文,回到runqueue中。
多核处理器的场景下,为了运行goroutines,每个M系统线程会持有一个Processor。

澳门新浦京手机版 16

在正常情况下,scheduler会按照上面的流程进行调度,但是线程会发生阻塞等情况,看一下goroutine对线程阻塞等的处理。

线程阻塞

当正在运行的goroutine阻塞的时候,例如进行系统调用,会再创建一个系统线程(M1),当前的M线程放弃了它的Processor,P转到新的线程中去运行。

澳门新浦京手机版 17

runqueue执行完成

当其中一个Processor的runqueue为空,没有goroutine可以调度。它会从另外一个上下文偷取一半的goroutine。

澳门新浦京手机版 18

五、对并发实现的进一步思考

Go语言的并发机制还有很多值得探讨的,比如Go语言和Scala并发实现的不同,Golang
CSP 和Actor模型的对比等。

了解并发机制的这些实现,可以帮助我们更好的进行并发程序的开发,实现性能的最优化。

关于三种多线程模型,可以关注一下Java语言的实现。

我们知道Java通过JVM封装了底层操作系统的差异,而不同的操作系统可能使用不同的线程模型,例如Linux和windows可能使用了一对一模型,solaris和unix某些版本可能使用多对多模型。JVM规范里没有规定多线程模型的具体实现,1:1(内核线程)、N:1(用户态线程)、M:N(混合)模型的任何一种都可以。谈到Java语言的多线程模型,需要针对具体JVM实现,比如Oracle/Sun的HotSpot
VM,默认使用1:1线程模型。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持脚本之家。

您可能感兴趣的文章:

  • golang实现并发数控制的方法

发表评论

电子邮件地址不会被公开。 必填项已用*标注