在进一步之前,让我们先回顾一下各种上下文切换技术。

不过首先说明一点术语。当我们说“上下文”的时候,指的是程序在执行中的一个状态。通常我们会用调用栈来表示这个状态——栈记载了每个调用层级执行到哪里,还有执行时的环境情况等所有有关的信息。

当我们说“上下文切换”的时候,表达的是一种从一个上下文切换到另一个上下文执行的技术。而“调度”指的是决定哪个上下文可以获得接下去的CPU时间的方法。

进程

进程是一种古老而典型的上下文系统,每个进程有独立的地址空间,资源句柄,他们互相之间不发生干扰。

每个进程在内核中会有一个数据结构进行描述,我们称其为进程描述符。这些描述符包含了系统管理进程所需的信息,并且放在一个叫做任务队列的队列里面。

很显然,当新建进程时,我们需要分配新的进程描述符,并且分配新的地址空间(和父地址空间的映射保持一致,但是两者同时进入COW状态)。这些过程需要一定的开销。

进程状态

忽略去linux内核复杂的状态转移表,我们实际上可以把进程状态归结为三个最主要的状态:就绪态,运行态,睡眠态。这就是任何一本系统书上都有的三态转换图。

就绪和执行可以互相转换,基本这就是调度的过程。而当执行态程序需要等待某些条件(最典型就是IO)时,就会陷入睡眠态。而条件达成后,一般会自动进入就绪。

阻塞

当进程需要在某个文件句柄上做IO,这个fd又没有数据给他的时候,就会发生阻塞。具体来说,就是记录XX进程阻塞在了XX fd上,然后将进程标记为睡眠态,并调度出去。当fd上有数据时(例如对端发送的数据到达),就会唤醒阻塞在fd上的进程。进程会随后进入就绪队列,等待合适的时间被调度。

阻塞后的唤醒也是一个很有意思的话题。当多个上下文阻塞在一个fd上(虽然不多见,但是后面可以看到一个例子),而且fd就绪时,应该唤醒多少个上下文呢?传统上应当唤醒所有上下文,因为如果仅唤醒一个,而这个上下文又不能消费所有数据时,就会使得其他上下文处于无谓的死锁中。

但是有个著名的例子——accept,也是使用读就绪来表示收到的。如果试图用多个线程来accept会发生什么?当有新连接时,所有上下文都会就绪,但是只有第一个可以实际获得fd,其他的被调度后又立刻阻塞。这就是惊群问题thundering herd problem。

现代linux内核已经解决了这个问题,方法惊人的简单——accept方法加锁。

(inet_connection_sock.c:inet_csk_wait_for_connect)

线程

线程是一种轻量进程,实际上在linux内核中,两者几乎没有差别,除了一点——线程并不产生新的地址空间和资源描述符表,而是复用父进程的。
但是无论如何,线程的调度和进程一样,必须陷入内核态。

二、传统网络服务模型

进程模型

为每个客户分配一个进程。优点是业务隔离,在一个进程中出现的错误不至于影响整个系统,甚至其他进程。Oracle传统上就是进程模型。缺点是进程的分配和释放有非常高的成本。因此Oracle需要连接池来保持连接减少新建和释放,同时尽量复用连接而不是随意的新建连接。

线程模型

为每客户分配一个线程。优点是更轻量,建立和释放速度更快,而且多个上下文间的通讯速度非常快。缺点是一个线程出现问题容易将整个系统搞崩溃。

一个例子

py_http_fork_thread.py

在这个例子中,线程模式和进程模式可以轻易的互换。

如何工作的:

  1. 父进程监听服务端口

  2. 在有新连接建立的时候,父进程执行fork,产生一个子进程副本

  3. 如果子进程需要的话,可以exec(例如CGI)

  4. 父进程执行(理论上应当先执行子进程,因为exec执行的快可以避免COW)到accept后,发生阻塞

  5. 上下文调度,内核调度器选择下一个上下文,如无意外,应当就是刚刚派生的子进程

  6. 子进程进程进入读取处理状态,阻塞在read调用上,所有上下文均进入睡眠态

  7. 随着SYN或者数据报文到来,CPU会唤醒对应fd上阻塞的上下文(wait_queue),切换到就绪态,并加入调度队列

  8. 上下文继续执行到下一个阻塞调用,或者因为时间片耗尽被挂起

评价


  • 同步模型,编写自然,每个上下文可以当作其他上下文不存在一样的操作,每次读取数据可以当作必然能读取到。

  • 进程模型自然的隔离了连接。即使程序复杂且易崩溃,也只影响一个连接而不是在整个系统。

  • 生成和释放开销很大(效率测试的进程fork和线程模式开销测试),需要考虑复用。

  • 进程模式的多客户通讯比较麻烦,尤其在共享大量数据的时候。

性能

thread模式,虚拟机:

1: 909.27 2: 3778.38 3: 4815.37 4: 5000.04 10: 4998.16 50: 4881.93 100: 4603.24 200: 3445.12 500: 1778.26 (出现错误)

fork模式,虚拟机:

1: 384.14 2: 435.67 3: 435.17 4: 437.54 10: 383.11 50: 364.03 100: 320.51 (出现错误)

thread模式,物理机:

1: 6942.78 2: 6891.23 3: 6584.38 4: 6517.23 10: 6178.50 50: 4926.91 100: 2377.77

注意在python中,虽然有GIL,但是一个线程陷入到网络IO的时候,GIL是解锁的。因此从调用开始到调用结束,减去CPU切换到其他上下文的时间,是可以多线程的。现象是,在此种状况下可以观测到短暂的python CPU用量超过100%。

如果执行多个上下文,可以充分利用这段时间。所观测到的结果就是,只能单核的python,在小范围内,其随着并发数上升,性能居然会跟着上升。如果将这个过程转移到一台物理机上执行,那么基本不能得出这样的结论。这主要是因为虚拟机上内核陷入的开销更高。

三、C10K 问题

描述

当同时连接数在10K左右时,传统模型就不再适用。实际上在效率测试报告的线程切换开销一节可以看到,超过1K后性能就差的一塌糊涂了。

进程模型的问题:

在C10K的时候,启动和关闭这么多进程是不可接受的开销。事实上单纯的进程fork模型在C1K时就应当抛弃了。

Apache的prefork模型,是使用预先分配(pre)的进程池。这些进程是被复用的。但即便是复用,本文所描述的很多问题仍不可避免。

线程模式的问题

从任何测试都可以表明,线程模式比进程模式更耐久一些,性能更好。但是在面对C10K还是力不从心的。问题是,线程模式的问题出在哪里呢?

内存?

有些人可能认为线程模型的失败首先在于内存。如果你这么认为,一定是因为你查阅了非常老的资料,并且没仔细思考过。

你可能看到资料说,一个线程栈会消耗8M内存(linux默认值,ulimit可以看到),512个线程栈就会消耗4G内存,而10K个线程就是80G。所以首先要考虑调整栈深度,并考虑爆栈问题。

听起来很有道理,问题是——linux的栈是通过缺页来分配内存的(How does stack allocation work in Linux?),不是所有栈地址空间都分配了内存。因此,8M是最大消耗,实际的内存消耗只会略大于实际需要的内存(内部损耗,每个在4k以内)。但是内存一旦被分配,就很难回收(除非线程结束),这是线程模式的缺陷。

这个问题提出的前提是,32位下地址空间有限。虽然10K个线程不一定会耗尽内存,但是512个线程一定会耗尽地址空间。然而这个问题对于目前已经成为主流的64位系统来说根本不存在。

内核陷入开销?

所谓内核陷入开销,就是指CPU从非特权转向特权,并且做输入检查的一些开销。这些开销在不同的系统上差异很大。

线程模型主要通过陷入切换上下文,因此陷入开销大听起来有点道理。实际上,这也是不成立的。线程在什么时候发生陷入切换?正常情况下,应当是IO阻塞的时候。同样的IO量,难道其他模型就不需要陷入了么?只是非阻塞模型有很大可能直接返回,并不发生上下文切换而已。

效率测试报告的基础调用开销一节,证实了当代操作系统上内核陷入开销是非常惊人的小的(10个时钟周期这个量级)。

线程模型的问题在于切换成本高

熟悉linux内核的应该知道,近代linux调度器经过几个阶段的发展。

  1. linux2.4的调度器

  2. O(1)调度器

  3. CFS

实际上直到O(1),调度器的调度复杂度才和队列长度无关。在此之前,过多的线程会使得开销随着线程数增长(不保证线性)。

O(1)调度器看起来似乎是完全不随着线程的影响。但是这个调度器有显著的缺点——难于理解和维护,并且在一些情况下会导致交互式程序响应缓慢。
CFS使用红黑树管理就绪队列。每次调度,上下文状态转换,都会查询或者变更红黑树。红黑树的开销大约是O(logm),其中m大约为活跃上下文数(准确的说是同优先级上下文数),大约和活跃的客户数相当。

因此,每当线程试图读写网络,并遇到阻塞时,都会发生O(logm)级别的开销。而且每次收到报文,唤醒阻塞在fd上的上下文时,同样要付出O(logm)级别的开销。

分析

O(logm)的开销看似并不大,但是却是一个无法接受的开销。因为IO阻塞是一个经常发生的事情。每次IO阻塞,都会发生开销。而且决定活跃线程数的是用户,这不是我们可控制的。更糟糕的是,当性能下降,响应速度下降时。同样的用户数下,活跃上下文会上升(因为响应变慢了)。这会进一步拉低性能。

问题的关键在于,http服务并不需要对每个用户完全公平,偶尔某个用户的响应时间大大的延长了是可以接受的。在这种情况下,使用红黑树去组织待处理fd列表(其实是上下文列表),并且反复计算和调度,是无谓的开销。

未完待续、、、、、、

本文转自 xjtuhit 51CTO博客,原文链接:http://blog.51cto.com/51reboot/1954878

Python 中的进程、线程、协程、同步、异步、回调(一)相关推荐

  1. 简要说明__python3中的进程/线程/协程

    多任务可以充分利用系统资源,极大提升程序运行效率,多任务的实现往往与 多线程,多进程,多协程有关 稳定性: 进程 > 线程 > 协程 系统资源占用量:进程 > 线程 > 协程 ...

  2. Python之进程+线程+协程(异步、selectors模块、阻塞、非阻塞IO)

    文章目录 一.IO多路复用 二.selectors模块 本篇文字是关于IO多路复用的更深入一步的总结,上一篇 Python之进程+线程+协程(事件驱动模型.IO多路复用.select与epoll)对I ...

  3. linux的进程/线程/协程系列5:协程的发展复兴与实现现状

    协程的发展复兴与实现现状 前言 本篇摘要: 1. 协同制的发展史 1.1 协同工作制的提出 1.2 自顶向下,无需协同 1.3 协同式思想的应用 2. 协程的复兴 2.1 高并发带来的问题 2.2 制 ...

  4. Linux的进程/线程/协程系列4:进程知识深入总结:上篇

    Linux的进程/线程/协程系列4:进程/线程相关知识总结 前言 本篇摘要: 1. 进程基础知识 1.1 串行/并行与并发 1.2 临界资源与共享资源 1.3 同步/异步与互斥 1.4 进程控制原语 ...

  5. 进程 线程 协程 各自的概念以及三者的对比分析

    文章目录 1 进程 2 线程 3 进程和线程的区别和联系 3.1 区别 3.2 联系 4 举例说明进程和线程的区别 5 进程/线程之间的亲缘性 6 协程 线程(执行一个函数)和协程的区别和联系 协程和 ...

  6. linux的进程/线程/协程系列3:查看linux内核源码——vim+ctags/find+grep

    linux的进程/线程/协程系列3:查看linux内核源码--vim+ctags/find+grep 前言 摘要: 1. 下载linux内核源码 2. 打标签方法:vim+ctags 2.1 安装vi ...

  7. linux的进程/线程/协程系列1:进程到协程的演化

    linux的进程/线程/协程系列1:进程到协程的演化 前言 摘要: 1. 一些历史:批处理时代 2. 现代操作系统启动过程 3. 进程(process)的出现 4. 线程(thread)与线程池 5. ...

  8. 4.19 python 网络编程和操作系统部分(TCP/UDP/操作系统概念/进程/线程/协程) 学习笔记

    文章目录 1 网络编程概念 1)基本概念 2)应用-最简单的网络通信 2 TCP协议和UDP协议进阶(网络编程) 1)TCP协议和UDP协议基于socket模块实现 2)粘包现象 3)文件上传和下载代 ...

  9. 并发编程:进程,线程,协程,异步

    并发编程(不是并行)目前有四种方式:多进程.多线程.协程和异步. 多进程编程在python中有类似C的os.fork,更高层封装的有multiprocessing标准库 多线程编程python中有Th ...

  10. python中IO多路复用、协程

    一.IO多路复用 IO多路复用:检测多个socket是否已经发生变化(是否已经连接成功/是否已经获取数据)(可读/可写) import socket def get_data(key):client ...

最新文章

  1. windows-服务端口
  2. arduino上传项目出错_Arduino入门前你该知道的事儿
  3. 对Coverage进行编辑
  4. YTU 2412: 帮警长数一数【循环、分支简单综合】
  5. AC日记——字符串位移包含问题 1.7 19
  6. 前端学习(2974):组件重定向
  7. PAT-013 L1-013. 计算阶乘和
  8. cogs2514 艺术
  9. 制造企业发展遇瓶颈?低代码平台助其逆风翻盘!
  10. 新网银行模型竞赛点评-小微风控算法大赛-早期风险识别
  11. 二硫键交联的巯基化壳聚糖水凝胶/pH、离子强度敏感性的壳聚糖水凝胶CS-GA-ASP
  12. Quorum简介部署
  13. 查看win11激活状态
  14. 怎么查找计算机的内存卡,如何查看自己电脑的内存卡的牌子?
  15. (八十一)探索hidl-gen使用及IWifi.hal 实现
  16. Dockerfile构建Nginx镜像、镜像优化(多阶段构建,最小化镜像构建)
  17. 关于KEIL5最新版没有ATMEL(含89C51芯片)的情况
  18. gitlab runner实现自动化部署
  19. vue实例中调用外部js_js文件中引用vue实例对象
  20. npm ERR missing script: server npm ERR npm ERR Did you mean this? npm ERR serve npm ERR A

热门文章

  1. 【音频】Faad源码交叉编译
  2. linux驱动:TI+DM8127+GPIO(三)之omap_hwmod中添加GPIO资源
  3. jvm调优工具_JVM性能调优监控工具jps、jstack、jmap、jhat、hprof使用详解
  4. kali扫描内网ip_来,我们聊聊内网渗透!
  5. 极小连通子图和极大连通子图_强连通分量与拓扑排序
  6. linux diff 补丁文件夹,LINUX下制作补丁文件 diff,patch
  7. win10商店打不开_科技资讯:Win10系统电脑的应用商店老是打不开还闪退怎么办
  8. 哈夫曼树的生成及哈夫曼编码
  9. 【js】四种自定义对象的常见方法
  10. 操作系统导论第四章课后习题