关于协程的学习 & 线程栈默认10M

先看的这篇文章:http://blog.csdn.net/qq910894904/article/details/41699541

以nginx为代表的事件驱动的异步server正在横扫天下,那么事件驱动模型会是server端模型的终点吗?

我们可以深入了解下,事件驱动编程的模型。

事件驱动编程的架构是预先设计一个事件循环,这个事件循环程序不断地检查目前要处理的信息,根据要处理的信息运行一个触发函数。其中这个外部信息可能来自一个目录夹中的文件,可能来自键盘或鼠标的动作,或者是一个时间事件。这个触发函数,可以是系统默认的也可以是用户注册的回调函数。

事件驱动程序设计着重于弹性以及异步化上面。许多GUI框架(如windows的MFC, Android的GUI框架),Zookeeper的Watcher等都使用了事件驱动机制。未来还会有其他的基于事件驱动的作品出现。

基于事件驱动的编程是单线程思维,其特点是异步+回调。

协程也是单线程,但是它能让原来要使用异步+回调方式写的非人类代码,可以用看似同步的方式写出来。它是实现推拉互动的所谓非抢占式协作的关键。

总结

协程的好处:

  • 跨平台
  • 跨体系架构
  • 无需线程上下文切换的开销
  • 无需原子操作锁定及同步的开销
  • 方便切换控制流,简化编程模型
  • 高并发+高扩展性+低成本:一个CPU支持上万的协程都不是问题。所以很适合用于高并发处理。

缺点:

  • 无法利用多核资源:协程的本质是个单线程,它不能同时将 单个CPU 的多个核用上,协程需要和进程配合才能运行在多CPU上.当然我们日常所编写的绝大部分应用都没有这个必要,除非是cpu密集型应用。
  • 进行阻塞(Blocking)操作(如IO时)会阻塞掉整个程序:这一点和事件驱动一样,可以使用异步IO操作来解决

https://www.zhihu.com/question/32218874

协程方便是因为“用代码来表示状态,而不是维护一坨数据结构来表示状态”,则客观得多。

作者:Tim Shen
链接:https://www.zhihu.com/question/32218874/answer/56255707
来源:知乎
著作权归作者所有,转载请联系作者获得授权。

讲点历史吧。

早期的Unix时代崇尚同步编程,当时设计的时候也没多想,管他什么性能不性能,先做出来再说。所以最开始人是开多进程(process)再用管子(pipe)连接起来的;多直观啊,我把东西推进管子里,你走你那头接一下。

不知道哪天人注意到开个千儿八百个进程机器就卡死了,这怎么行,所以我们只搞一个进程,里面有一坨打开的网络连接和文件,用select这个系统调用对io事件同时进行监听,谁先来我就处理谁。然而发现性能也不好,没什么卵用。

人们在这两条路上都想办法提升性能。进程这边人们又搞了多线程,结果还是不够。多线程还是吃内核资源,抗不住。

Linux忍不住了,搞出个epoll(不过我不知道和bsd的kqueue哪个在前)系统调用,就是红黑树改良版的select。这下子人开心了,真他妈快啊,开千儿八百个连接小意思,同时进行事件监听,判断连接的的id和事件的类型,再打(dispatch)到不同的回调函数里。

不知是不是市场原因,正巧这时候互联网开始火了,性能越好赚钱越多嘛,所以肯定来这套高效的接口,管他好不好用呢,反正俺们吃苦耐劳,外加抖M,才不怕手动维护状态。你多线程开太多最后不也是要手动维护状态。

不熟悉操作系统的开发人员们只知道有什么用什么,也没去多想怎么优化多线程。现在多线程优化出来了,叫协程,调度器有的有(比如go的用户态调度器)有的没有(比如yield),取决于怎么用。其内存开销仍然比异步回调要大(一个协程一个栈,而异步回调的话一个event loop一共就一个栈),但是现在内存也是便宜了,不算是瓶颈。人最关注的是“千万不能堵着(block)”,要千方百计让CPU转起来,这样concurrency才能上去。而到处乱开协程(因为比线程便宜啊)就能达到这个效果,开十万个协程内存也没爆炸,跑上24个就能把CPU打满。所以异步回调的这个优势已经没了。

我注意到从事应用开发和网络服务开发的人员对于底层可能没那么熟悉,有时候不免走了弯路。kqueue/epoll这么个简单的东西,被开发者们搞出花来了,异步编程事件回调一套一套的,写书的也有,写库的也有(最近那个reactive也是)。自然术业有专攻,但是倘若稍微花些精力过一遍底层概念,学习/发明上层东西的时候会快很多,也少些痛苦。

至于趋势什么的,也就赶个时髦吧,这些枝叶伎俩学学也要不了几个小时。如果是担心要花很多精力学习某个“技术”,又怕学了没用,故有“是不是趋势啊”这一问,那多半是根基不牢,打打基础就行了。

我是写C++的,我发现我唯一用callback/closure/lambda的地方是我有个函数,但是我想把这个函数扣个洞,让用户来填这个洞。用得还真不算多。

注:之前想学Java的时候,花了很多时间来了解趋势。现在想想,的确是瞻前顾后呀,学一下就知道趋势了,哈哈。

------另一个人的回答---------

有时候总觉得 gevent(python的一个库) 这类协程库是被 python 的 GIL 逼出来的,如果原生线程支持足够好,协程的必要性可能并不一定很大。

协程最早来自高性能计算领域的成功案例,协作式调度相比抢占式调度而言,可以在牺牲公平性时换取吞吐。

在互联网行业面临 C10K 问题时,线程方案不足以扛住大量的并发,这时的解决方案是 epoll() 式的事件循环,nginx 在这波潮流中顺利换掉 apache 上位。同一时间的开发社区为 nginx 的成绩感到震撼,出现了很多利用事件循环的应用框架,如 tornado / nodejs,也确实能够跑出更高的分数。而且 python/ruby 社区受 GIL 之累,几乎没有并发支持,这时事件循环是一种并发的解放。

然而事件循环的异步控制流对开发者并不友好。业务代码中随处可见的 mysql / memcache 调用,迅速地膨胀成一坨 callback hell。这时社区发现了协程,在用户态实现上下文切换的工具,把 epoll() 事件循环隐藏起来,而且成本不高:用每个协程一个用户态的栈,代替手工的状态管理。似乎同时得到了事件循环和线程同步控制流的好处,既得到了 epoll() 的高性能,又易于开发。甚至通过 monkey patch,旧的同步代码可以几乎无缝地得到异步的高性能,真是太完美了。

注:callback hell可以看:http://www.infoq.com/cn/articles/nodejs-callback-hell/

Node.js需要按顺序执行异步逻辑时一般采用后续传递风格,也就是将后续逻辑封装在回调函数中作为起始函数的参数,逐层嵌套。这种风格虽然可以提高CPU利用率,降低等待时间,但当后续逻辑步骤较多时会影响代码的可读性,结果代码的修改维护变得很困难。根据这种代码的样子,一般称其为"callback hell"或"pyramid of doom",本文称之为回调大坑,嵌套越多,大坑越深。

注:monkey patch可以看:http://www.tuicool.com/articles/2aIZZb

monkey patch指的是在运行时动态替换,一般是在startup的时候. (感觉大多数是Python程序的一种技巧)

用过gevent就会知道,会在最开头的地方gevent.monkey.patch_all();把标准库中的thread/socket等给替换掉.这样我们在后面使用socket的时候可以跟平常一样使用,无需修改任何代码,但是它变成非阻塞的了.

之前做的一个游戏服务器,很多地方用的import json,后来发现ujson比自带json快了N倍,于是问题来了,难道几十个文件要一个个把import json改成import ujson as json吗?

其实只需要在进程startup的地方monkey patch就行了.是影响整个进程空间的.

同一进程空间中一个module只会被运行一次.

下面是代码.

main.py

import json
import ujson
def monkey_patch_json():
    json.__name__ = ‘ujson‘
    json.dumps = ujson.dumps
    json.loads = ujson.loads

monkey_patch_json()
print ‘main.py‘,json.__name__
import sub
sub.py
import json
print ‘sub.py‘,json.__name__
运行main.py,可以看到都是输出‘ujson‘,说明后面import的json是被patch了的.

最后,注意不能单纯的json = ujson来替换.

然而,跑了一圈回来,协程相比原生线程又有多少差别呢。

1. 用户态栈,更轻量地创建“轻量线程”;
2. 协作式的用户态调度器,更少线程上下文切换;
3. 重新实现 mutex 等同步原语;

协程的创建成本更小,但是创建成本可以被线程池完全绕开,而且线程池更 fine grained,这时相比线程池的优势更多在于开发模型的省力,而不在性能。此外,"轻量线程" 这个名字有一定误导的成分,协程作为用户态线程,需要的上下文信息与系统线程几乎无异。如果说阻碍系统线程 scale 的要素是内存(一个系统线程的栈几乎有 10mb 虚拟内存,线程的数量受虚拟地址空间限制),那么用户态线程的栈如果使用得不节制,也需要同量的内存。

注:线程栈的默认大小可以通过 ulimit -s 来查看。

$ ulimit -s
10240

也就是10M;修改的方法如下:

linux查看修改线程默认栈空间大小 ulimit -s

1、通过命令 ulimit -s 查看linux的默认栈空间大小,默认情况下 为10240 即10M

2、通过命令 ulimit -s 设置大小值 临时改变栈空间大小:ulimit -s 102400, 即修改为100M

3、可以在/etc/rc.local 内 加入 ulimit -s 102400 则可以开机就设置栈空间大小

4、在/etc/security/limits.conf 中也可以改变栈空间大小:

#<domain> <type> <item> <value>

* soft stack 102400

重新登录,执行ulimit -s 即可看到改为102400 即100M

协作式调度相比抢占式调度的优势在于上下文切换开销更少(但是差异是否显著?)、更容易把缓存跑热,但是也放弃了原生线程的优先级概念,如果存在一个较长时间的计算任务,将影响到 IO 任务的响应延时。

而内核调度器总是优先 IO 任务,使之尽快得到响应。此外,单线程的协程方案并不能从根本上避免阻塞,比如文件操作、内存缺页,这都属于影响到延时的因素。

事件循环方案被认识的一个优势是可以避免上锁,而锁是万恶之源。协程方案基于事件循环方案,似乎继承了不用上锁的优势。然而并不是。上下文切换的边界之外,并不能保证临界区。该上锁的地方仍需要上锁。

差异存在,但该维护的信息并没有更少。如果运行时对系统线程的支持比较好,业务系统使用协程的综合效益并不一定相比线程池更好。我们业内通常意义上的"高并发",往往只是要达到几k qps,然而 qps 是衡量吞吐而非并发的指标(并发1k意味着同时响应1k个连接,而 qps 衡量一秒响应多少请求,这可以是排队处理,并不一定"并发"),靠线程池并非做不到。但对 python 这类 GIL 运行时而言,这却拥有显著提升性能的优势了,只是这时瓶颈在 GIL,而不在线程。

至于并发量导向的业务,一般也是状态上下文较少的业务,比如推送,这时 callback hell 基本可控,使用协程相比事件循环依然更容易编程,但效益并不显著。

最后尝试总结一下个人的想法:

协程不是趋势,它是一个在历史中被挖掘出来的、对现有问题的一个有用的补充。

适用的场景:

  • 高性能计算,牺牲公平性换取吞吐;
  • 面向 IO Bound 任务,减少 IO 等待上的闲置,这其实和高性能计算领域内的优势是一致的;
  • Generator 式的流式计算;
  • 消除 Callback Hell,使用同步模型降低开发成本的同时保留更灵活控制流的好处,比如同时发三个请求;这时节约地使用栈,可以充分地发挥 "轻量" 的优势;

但并不是万灵丹:

  • 如果栈使用得不节制,消耗的内存量和系统线程无异,甚至内存管理还不如系统线程(系统线程可以动态地调整虚拟内存,用户线程的 Segmented Stack 方案存在严重的抖动问题,Continous Stack 方案管理不当也会抖动,为了避免抖动则成了空间换时间,而内核在这方面做了多少 heuristic 呢);
  • IO Bound 任务可以通过调线程池大小在一定程度上缓解,目标是把 CPU 跑满即可,这点线程池的表现可能不完美,但在业务逻辑这个领域是及格的;
  • 此外,一般的 python/ruby 任务并不是严格的 IO Bound,比如 ORM 的对象创建、模版渲染、GC 甚至解释器本身,都是 CPU 大户;单个请求扣去 redis 请求和数据库请求的时间,其它时间是否仍不少呢?
  • CPU 上长时间的计算,导致用户线程的调度变差,不能更快地响应,单个请求的平均时间反而可能更长(诚然并发可能更高);然而这在 python 这类 GIL 语言来看并不算劣势,甚至比 GIL 的调度更好,至少 gevent 可以知道各 IO 任务的优先级,而 GIL 的调度是事实上的 FIFO;

References

- Xiao-Feng Li: Thread mapping: 1:1 vs M:N
- http://thread.gmane.org/gmane.comp.lang.rust.devel/6479

时间: 2024-08-26 13:52:57

关于协程的学习 & 线程栈默认10M的相关文章

【记录一个问题】android下的ucontext协程,因为使用栈上的对象,导致cv::Mat被莫名析构

工作的流程是这样:某个协程在栈上创建task对象,在task对象内有需要返回的cv::Mat. 然后把task放到另一个线程上去执行,然后切换到别的协程,等到工作线程执行完task后,再唤醒协程. 这时候协程内去访问cv::Mat使用是empty. 同样的代码,task对象不是在栈上创建,而是new出来,一切正常. 暂未找到原因,怀疑与栈的拷贝有关.其实也打印了使用各个阶段的指针的地址,发现地址未发生改变. 原文地址:https://www.cnblogs.com/ahfuzhang/p/116

python学习-线程、进程、协程

线程 python 的 threading 模块提供了线程的相关操作,线程是应用程序中工作的最小单元. import time import threading def process(arg): time.sleep(1) print(arg) if __name__ == '__main__': for i in range(10): t = threading.Thread(target=process, args=(i,)) t.start() 通过 threading 模块实现多线程,

Python之路(十八):进程,线程,协程

python基础之进程.线程.协程 引子 进程 线程(优先阅读) 协程 进程 概念:就是一个程序在一个数据集上的一次动态执行过程(本质上来讲,就是运行中的程序(代指运行过程),程序不运行就不是进程)    抽象概念 组成:  1.程序:我们编写的程序用来描述进程要完成哪些功能以及如何完成  2.数据集:数据集则是程序在执行过程中所需要使用的资源  3.进程控制块:进程控制块用来记录进程的外部特征,描述进程的执行变化过程,系统可以利用它来控制和管理进程,它是系统感知进程存在的唯一标志. 阐释:进程

python线程、进程、协程

进程与线程之间的定义 计算机是由硬件和软件组成的.硬件中的CPU是计算机的核心,它承担计算机的所有任务. 操作系统是运行在硬件之上的软件,是计算机的管理者,它负责资源的管理和分配.任务的调度. 程序是运行在系统上的具有某种功能的软件,比如说浏览器,音乐播放器等. 每次执行程序的时候,都会完成一定的功能,比如说浏览器帮我们打开网页,为了保证其独立性,就需要一个专门的管理和控制执行程序的数据结构——进程控制块. 进程就是一个程序在一个数据集上的一次动态执行过程. 进程一般由程序.数据集.进程控制块三

python 3.x 学习笔记17(协程以及I/O模式)

1.协程(微线程)协程是一种用户态的轻量级线程.协程拥有自己的寄存器上下文和栈.协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈.因此: 协程能保留上一次调用时的状态(即所有局部状态的一个特定组合),每次过程重入时,就相当于进入上一次调用的状态,换种说法:进入上一次离开时所处逻辑流的位置. 2.greenlet模块greenlet是一个用C实现的协程模块,相比与python自带的yield,它可以使你在任意函数之间随意切换,而不需把这个函数先声明为

python之进程、线程、协程的调度原理

进程.线程和协程的调度和运行原理总结. 进程.线程的调度策略介绍 linux的操作系统详细调度策略可参考:http://blog.csdn.net/gatieme/article/details/51872659 linux中的进程主要有三种调度策略: 优先级调度:将进程分为普通进程和实时进程: 先进先出(队列)调度:实时进程先创建的先执行,直到 轮转调度(时间片):达到一定的CPU执行时间后强制切换: 多进程程序的调度其实还是线程的调度,线程才是CPU调度的基本单位:在同一个进程内线程切换不会

线程进阶之线程队列、线程池和协程

本节目录: 1.线程队列 2.线程池 3.协程 一.线程队列 线程之间的通信我们列表行不行呢,当然行,那么队列和列表有什么区别呢? queue队列 :使用import queue,用法与进程Queue一样 queue is especially useful in threaded programming when information must be exchanged safely between multiple threads. class queue.Queue(maxsize=0)

golang 学习 (八)协程

一: 进程.线程 和 协程 之间概念的区别:        对于 进程.线程,都是有内核进行调度,有 CPU 时间片的概念,进行 抢占式调度(有多种调度算法)    (补充: 抢占式调度与非抢占(轮询任务调度)区别在于抢占式调度可以因为优先级高的任务抢占cpu,而轮询的不能) 对于 协程(用户级线程),这是对内核透明的,也就是系统并不知道有协程的存在,是完全由用户自己的程序进行调度的,因为是由用户程序自己控制,那么就很难像抢占式调度那样做到强制的 CPU 控制权切换到其他进程/线程,通常只能进行

Python学习经验之谈:关于协程的理解和其相关面试问题

都知道Python非常适合初学者学习来入门编程,昨天有伙伴留言说面试了Python岗位,问及了一个关于协程的问题,想了想还是跟大家出一篇协程相关的文章和在Python面试中可能会问及的相关面试问题.都是根据我自己的Python学习经验来写的,有这方面需求的伙伴可以认真阅读,也欢迎补充不足之处! 一.什么是协程 协程:实现协作式多任务,可以在程序执行内部中断,转而执行其他协程. 比如我们编写子程序(或者说函数),通常是利用“调用”来实现从 A 跳去 B,B 跳去 C,如果想回来调用方,必须等被调用