并发总结

 

并发总结

并发

高并发(High Concurrency)是互联网分布式系统架构设计中必须考虑的因素之一,它通常是指,通过设计保证系统能够同时并行处理很多请求。

高并发相关常用的一些指标有响应时间(Response Time),吞吐量(Throughput),每秒查询QPS(Query Per Second),并发用户数等。

并发的手段主要有:垂直扩展(Scale Up)与水平扩展(Scale Out)。垂直扩展可以通过提升单机硬件性能,或者提升单机架构性能,来提高并发性,但单机性能总是有极限的,因此,水平扩展也是必要的。

水平扩展的方法大致有:

  1. CDN,通过DNS的方式进行扩展,增加节点

  2. Nginx,单个节点通过Nginx负载均衡配置多个设备

  3. 数据库,将原本存储在一台服务器上的数据(缓存,数据库),用hash的方法拆分到不同服务器上去

  4. 当然还可以在客户端、应用层做一些处理,提高并发能力

    垂直扩展,主要是通过系统架构来实现高并发。

任务类型主要分为IO密集型和CPU密集型。IO密集型任务主要占用IO,计算消耗很少,因此对于这类任务需要将其IO占用与CPU占用分开,否则将浪费CPU的时间。CPU密集型任务,主要是消耗CPU进行计算,因此必要时需要将计算分散到多个CPU进行,这样才可以减少时间。

目前网络任务大多是IO密集型任务,瓶颈一般在网络IO上。因此本文主要讨论这类问题。

对于单机来说,实现高并发主要是通过:多进程、多线程、协程、IO多路复用。

多进程

进程创建:

一般来说,一个程序就是一个进程,它有自己的虚拟地址空间,进程表示一个运行的程序,程序的代码段,数据段这些都是存放在磁盘中的,在运行时加载到内存中。所以虚拟内存面向的是磁盘,虚拟页是对磁盘文件的分配,然后被缓存到物理内存的物理页中。

所以存储资源是操作系统由虚拟内存机制来管理和分配的。进程是操作系统分配存储资源的最小单元。

因此每个进程之间都是独立的。Unix/Linux操作系统提供了一个fork()系统调用,产生新的进程,新的进程会复制子进程产生之前的数据,以及所有的代码。一个进程至少有一个线程。

进程池:

如果要启动大量的子进程,可以用进程池的方式批量创建子进程,如果需要,直接从进程池中获取一个进程使用即可。

进程间通信:

进程间通信方式有很多,通常有管道(包括无名管道和命名管道)、消息队列、信号量、Socket、Streams等。其中 Socket和Streams支持不同主机上的两个进程通信。

进程调度:

进程创建、管理、调度都是由os自动操作的,一个进程一般只能占用一个CPU。

多线程

Linux的线程本质上是一种轻量级的进程,是通过clone系统调用来创建的。进程是操作系统分配存储资源的最小单元,那么线程就是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一个进程下的所有线程共享该进程的资源。因此不管是单进程多线程 还是 多进程单线程,其实调度的都是线程。

C语言利用了Pthreads库来真正创建了线程这个数据结构。Linux采用了1:1的模型,即C语言的Pthreads库创建的线程实体1:1对应着内核创建的一个KSE(Kernal Scheduling Entry, 内核调度实体)。Pthreads运行在用户空间,KSE运行在内核空间。

在程序中可以将线程绑定到具体的CPU上。

在Python中,多线程并不能做到多核,因为Python有GIL锁机制。

线程也可以构建类似于进程池的线程池。

线程间通信:

锁、信号量、条件变量、共享内存等

 

协程

Coroutine,翻译成”协程“,Coroutine是编译器级的,Process和Thread是操作系统级的。通过插入相关的代码使得代码段能够实现分段式的执行,重新开始的地方是yield关键字指定的,一次一定会跑到一个yield对应的地方。

协程是轻量级线程,拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈。与多线程有些类似,但协程调用是在一个线程内进行的,是单线程,切换的开销小,因此效率上略高于多线程。线程之间需要使用同步机制来避免产生全局资源的竟态,这就不可避免产生了休眠、调度、切换上下文一类的系统开销,而且线程调度还会产生时序上的不确定性。

同时可以避免线程同步带来的各种问题:竞争、死锁等

协程间的调用是逻辑上可控的,时序上确定的,可谓一切尽在掌握中。

 

IO多路复用

进程、线程、协程,这三种方法只是三种工具,为不同问题提供不同的解决方法,不同的问题可能需要用到不同的方法去解决。

网络任务中,IO多路复用被经常采用,对于高并发的网络任务,一般采用异步非阻塞的方式处理,因为IO耗时远远大于CPU,而IO请求个数非常频繁。

传统的网络服务器(如nginx、squid等)都采用了 EDSM (event-driven state machine,事件驱动状态机) 机制并发处理请求,这是一种异步处理的方式,通过使用callback 方法避免阻塞线程。

EDSM最常见的方式就是I/O事件的异步回调。基本上都会有一个叫做dispatcher的单线程主循环(又叫event loop),用户通过向dispatcher注册回调函数(又叫event handler)来实现异步通知,从而不必在原地空耗资源干等。在dispatcher主循环中通过select()/epoll()等系统调用来等待各种I/O事件的发生,当内核检测到事件触发并且数据可达或可用时,select()/epoll()会返回从而使dispatcher调用相应的回调函数来对处理用户的请求。

如果采用进程、线程的方式对收到的请求进行回调,那么需要创建进程池或者线程池,当有回调的时候,从进程池或者线程池中获取一个进程或者线程去处理回调的事件。

如果采用协程的方式对请求进行 “回调”。那么,整个过程都是单线程的。这种处理本质上就是将一堆相互独立(disjoint)的回调实现同步控制,就像串联在一个顺序链表上,不存在进程/线程的切换。

协程是在单线程中使用同步编程思想来实现异步的处理流程,从而实现单线程能并发处理成百上千个请求,而且每个请求的处理过程是线性的,逻辑上可控的,时序上确定的。

 

对比:

  1. 线程执行开销小,但不利于资源的管理和保护;而进程正相反。进程上下文切换要保存页表,文件描述符表,信号控制数据和进程信息等数据。线程上下文切换是很轻量级的。

  2. 线程适合于在SMP(多核处理机)机器上运行,而进程则可以跨机器迁移。

  3. 进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,

  4. 进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率。

 

总结:

每个进程拥有独立的虚拟内存地址空间,会真正地拥有独立与父进程之外的物理内存。并且由于进程拥有独立的内存地址空间,导致了进程之间无法利用直接的内存映射进行进程间通信。

并发的本质是同时运行多个任务,并发非常重要的问题之一就是:共享资源的问题。而进程恰恰很难在逻辑上表示共享资源,需要通过消耗较大的其他方式进行资源的同步,如:管道、消息队列、信号量、Socket等。

而线程可以很简单地表示共享资源的问题,一个进程的所有线程都是共享这个进程的同一个虚拟地址空间的,也就是说从线程的角度来说,它们看到的物理资源都是一样的,这样就可以通过共享变量的方式来表示共享资源,也就是直接共享内存的方式解决了线程通信的问题。而线程也表示一个独立的逻辑流,这样就完美解决了进程的一个大难题。但是,线程同样需要用锁或者其他方式去解决资源的同步问题,这也会带来额外的消耗。

协程不存在资源同步问题,因为协程运行在一个线程中,只要逻辑正确,就不需要用到锁。但是由于协程是在一个线程中的,而线程是OS调度的最小粒度,因此只靠协程没法利用多核的优势。协程的作用主要是用于异步IO,让线程不用等待IO返回,或者不用挂起线程,减少线程切换的次数。所以,一般来说需要协程与进程/线程配合起来使用,达到更好的效果

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值