多核CPU
linux
linux :程序员
cat /proc/cpuinfo
若是你不幸拥有一个多核CPU,你确定在想,多核应该能够同时执行多个线程。面试
若是写一个死循环的话,会出现什么状况呢?编程
打开Mac OS X的Activity Monitor,或者Windows的Task Manager,均可以监控某个进程的CPU使用率。浏览器
咱们能够监控到一个死循环线程会100%占用一个CPU。若是有两个死循环线程,在多核CPU中,能够监控到会占用200%的CPU,也就是占用两个CPU核心。要想把N核CPU的核心所有跑满,就必须启动N个死循环线程。缓存
试试用Python写个死循环:安全
1
2
3
4
5
6
7
8
9
10
|
import
threading, multiprocessing
def
loop():
x
=
0
while
True
:
x
=
x ^
1
for
i
in
range
(multiprocessing.cpu_count()):
t
=
threading.Thread(target
=
loop)
t.start()
|
启动与CPU核心数量相同的N个线程,在4核CPU上能够监控到CPU占用率仅有102%,也就是仅使用了一核。服务器
可是用C、C++或Java来改写相同的死循环,直接能够把所有核心跑满,4核就跑到400%,8核就跑到800%,为何Python不行呢?网络
由于Python的线程虽然是真正的线程,但解释器执行代码时,有一个GIL锁:Global Interpreter Lock,任何Python线程执行前,必须先得到GIL锁,而后,每执行100条字节码,解释器就自动释放GIL锁,让别的线程有机会执行。这个GIL全局锁实际上把全部线程的执行代码都给上了锁,因此,多线程在Python中只能交替执行,即便100个线程跑在100核CPU上,也只能用到1个核。多线程
GIL是Python解释器设计的历史遗留问题,一般咱们用的解释器是官方实现的CPython,要真正利用多核,除非重写一个不带GIL的解释器。
因此,在Python中,可使用多线程,但不要期望能有效利用多核。若是必定要经过多线程利用多核,那只能经过C扩展来实现,不过这样就失去了Python简单易用的特色。
不过,也不用过于担忧,Python虽然不能利用多线程实现多核任务,但能够经过多进程实现多核任务。多个Python进程有各自独立的GIL锁,互不影响。
多线程编程,模型复杂,容易发生冲突,必须用锁加以隔离,同时,又要当心死锁的发生。
Python解释器因为设计时有GIL全局锁,致使了多线程没法利用多核。
多cpu的运行,对应进程的运行状态;多核cpu的运行,对应线程的运行状态。
操做系统会拆分CPU为一段段时间的运行片,轮流分配给不一样的程序。对于多cpu,多个进程能够并行在多个cpu中计算,固然也会存在进程切换;对于单cpu,多个进程在这个单cpu中是并发运行,根据时间片读取上下文+执行程序+保存上下文。同一个进程同一时间段只能在一个cpu中运行,若是进程数小于cpu数,那么未使用的cpu将会空闲。
进程有本身的独立地址空间,每启动一个进程,系统就会为它分配地址空间,创建数据表来维护代码段、堆栈段和数据段,这种操做很是昂贵。而线程是共享进程中的数据的,使用相同的地址空间,所以CPU切换一个线程的花费远比进程要小不少,同时建立一个线程的开销也比进程要小不少。
对于多核cpu,进程中的多线程并行执行,执行过程当中存在线程切换,线程切换开销较小。对于单核cpu,多线程在单cpu中并发执行,根据时间片切换线程。同一个线程同一时间段只能在一个cpu内核中运行,若是线程数小于cpu内核数,那么将有多余的内核空闲。
1 单CPU中进程只能是并发,多CPU计算机中进程能够并行。
2单CPU单核中线程只能并发,单CPU多核中线程能够并行。
3 不管是并发仍是并行,使用者来看,看到的是多进程,多线程。
进程 vs. 线程
咱们介绍了多进程和多线程,这是实现多任务最经常使用的两种方式。如今,咱们来讨论一下这两种方式的优缺点。
首先,要实现多任务,一般咱们会设计Master-Worker模式,Master负责分配任务,Worker负责执行任务,所以,多任务环境下,一般是一个Master,多个Worker。
若是用多进程实现Master-Worker,主进程就是Master,其余进程就是Worker。
若是用多线程实现Master-Worker,主线程就是Master,其余线程就是Worker。
多进程模式最大的优势就是稳定性高,由于一个子进程崩溃了,不会影响主进程和其余子进程。(固然主进程挂了全部进程就全挂了,可是Master进程只负责分配任务,挂掉的几率低)著名的Apache最先就是采用多进程模式。
多进程模式的缺点是建立进程的代价大,在Unix/Linux系统下,用fork调用还行,在Windows下建立进程开销巨大。另外,操做系统能同时运行的进程数也是有限的,在内存和CPU的限制下,若是有几千个进程同时运行,操做系统连调度都会成问题。
多线程模式一般比多进程快一点,可是也快不到哪去,并且,多线程模式致命的缺点就是任何一个线程挂掉均可能直接形成整个进程崩溃,由于全部线程共享进程的内存。在Windows上,若是一个线程执行的代码出了问题,你常常能够看到这样的提示:“该程序执行了非法操做,即将关闭”,其实每每是某个线程出了问题,可是操做系统会强制结束整个进程。
在Windows下,多线程的效率比多进程要高,因此微软的IIS服务器默认采用多线程模式。因为多线程存在稳定性的问题,IIS的稳定性就不如Apache。为了缓解这个问题,IIS和Apache如今又有多进程+多线程的混合模式,真是把问题越搞越复杂。
线程切换
不管是多进程仍是多线程,只要数量一多,效率确定上不去,为何呢?
咱们打个比方,假设你不幸正在准备中考,天天晚上须要作语文、数学、英语、物理、化学这5科的做业,每项做业耗时1小时。
若是你先花1小时作语文做业,作完了,再花1小时作数学做业,这样,依次所有作完,一共花5小时,这种方式称为单任务模型,或者批处理任务模型。
假设你打算切换到多任务模型,能够先作1分钟语文,再切换到数学做业,作1分钟,再切换到英语,以此类推,只要切换速度足够快,这种方式就和单核CPU执行多任务是同样的了,以幼儿园小朋友的眼光来看,你就正在同时写5科做业。
可是,切换做业是有代价的,好比从语文切到数学,要先收拾桌子上的语文书本、钢笔(这叫保存现场),而后,打开数学课本、找出圆规直尺(这叫准备新环境),才能开始作数学做业。操做系统在切换进程或者线程时也是同样的,它须要先保存当前执行的现场环境(CPU寄存器状态、内存页等),而后,把新任务的执行环境准备好(恢复上次的寄存器状态,切换内存页等),才能开始执行。这个切换过程虽然很快,可是也须要耗费时间。若是有几千个任务同时进行,操做系统可能就主要忙着切换任务,根本没有多少时间去执行任务了,这种状况最多见的就是硬盘狂响,点窗口无反应,系统处于假死状态。
因此,多任务一旦多到一个限度,就会消耗掉系统全部的资源,结果效率急剧降低,全部任务都作很差。
计算密集型 vs. IO密集型
是否采用多任务的第二个考虑是任务的类型。咱们能够把任务分为计算密集型和IO密集型。
计算密集型任务的特色是要进行大量的计算,消耗CPU资源,好比计算圆周率、对视频进行高清解码等等,全靠CPU的运算能力。这种计算密集型任务虽然也能够用多任务完成,可是任务越多,花在任务切换的时间就越多,CPU执行任务的效率就越低,因此,要最高效地利用CPU,计算密集型任务同时进行的数量应当等于CPU的核心数。
计算密集型任务因为主要消耗CPU资源,所以,代码运行效率相当重要。Python这样的脚本语言运行效率很低,彻底不适合计算密集型任务。对于计算密集型任务,最好用C语言编写。
第二种任务的类型是IO密集型,涉及到网络、磁盘IO的任务都是IO密集型任务,这类任务的特色是CPU消耗不多,任务的大部分时间都在等待IO操做完成(由于IO的速度远远低于CPU和内存的速度)。对于IO密集型任务,任务越多,CPU效率越高,但也有一个限度。常见的大部分任务都是IO密集型任务,好比Web应用。
IO密集型任务执行期间,99%的时间都花在IO上,花在CPU上的时间不多,所以,用运行速度极快的c语言替换用Python这样运行速度极低的脚本语言,彻底没法提高运行效率。对于IO密集型任务,最合适的语言就是开发效率最高(代码量最少)的语言,脚本语言是首选,C语言最差。
异步IO
考虑到CPU和IO之间巨大的速度差别,一个任务在执行的过程当中大部分时间都在等待IO操做,单进程单线程模型会致使别的任务没法并行执行,所以,咱们才须要多进程模型或者多线程模型来支持多任务并发执行。
现代操做系统对IO操做已经作了巨大的改进,最大的特色就是支持异步IO。若是充分利用操做系统提供的异步IO支持,就能够用单进程单线程模型来执行多任务,这种全新的模型称为事件驱动模型,Nginx就是支持异步IO的Web服务器,它在单核CPU上采用单进程模型就能够高效地支持多任务。在多核CPU上,能够运行多个进程(数量与CPU核心数相同),充分利用多核CPU。因为系统总的进程数量十分有限,所以操做系统调度很是高效。用异步IO编程模型来实现多任务是一个主要的趋势。
对应到Python语言,单进程的异步编程模型称为协程,有了协程的支持,就能够基于事件驱动编写高效的多任务程序。咱们会在后面讨论如何编写协程。
分布式进程
在Thread和Process中,应当优选Process,由于Process更稳定,并且,Process能够分布到多台机器上,而Thread最多只能分布到同一台机器的多个CPU上。
Python的multiprocessing模块不但支持多进程,其中managers子模块还支持把多进程分布到多台机器上。一个服务进程能够做为调度者,将任务分布到其余多个进程中,依靠网络通讯。因为managers模块封装很好,没必要了解网络通讯的细节,就能够很容易地编写分布式多进程程序。
举个例子:若是咱们已经有一个经过Queue通讯的多进程程序在同一台机器上运行,如今,因为处理任务的进程任务繁重,但愿把发送任务的进程和处理任务的进程分布到两台机器上。怎么用分布式进程实现?
原有的Queue能够继续使用,可是,经过managers模块把Queue经过网络暴露出去,就可让其余机器的进程访问Queue了。
咱们先看服务进程,服务进程负责启动Queue,把Queue注册到网络上,而后往Queue里面写入任务:
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
|
import
random, time, queue
from
multiprocessing.managers
import
BaseManager
# 发送任务的队列:
task_queue
=
queue.Queue()
# 接收结果的队列:
result_queue
=
queue.Queue()
# 从BaseManager继承的QueueManager:
class
QueueManager(BaseManager):
pass
# 把两个Queue都注册到网络上, callable参数关联了Queue对象:
QueueManager.register(
'get_task_queue'
,
callable
=
lambda
: task_queue)
QueueManager.register(
'get_result_queue'
,
callable
=
lambda
: result_queue)
# 绑定端口5000, 设置验证码'abc':
manager
=
QueueManager(address
=
('
', 5000), authkey=b'
abc')
# 启动Queue:
manager.start()
# 得到经过网络访问的Queue对象:
task
=
manager.get_task_queue()
result
=
manager.get_result_queue()
# 放几个任务进去:
for
i
in
range
(
10
):
n
=
random.randint(
0
,
10000
)
print
(
'Put task %d...'
%
n)
task.put(n)
# 从result队列读取结果:
print
(
'Try get results...'
)
for
i
in
range
(
10
):
r
=
result.get(timeout
=
10
)
print
(
'Result: %s'
%
r)
# 关闭:
manager.shutdown()
print
(
'master exit.'
)
|
当咱们在一台机器上写多进程程序时,建立的Queue能够直接拿来用,可是,在分布式多进程环境下,添加任务到Queue不能够直接对原始的task_queue进行操做,那样就绕过了QueueManager的封装,必须经过manager.get_task_queue()得到的Queue接口添加。
而后,在另外一台机器上启动任务进程(本机上启动也能够):
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
|
import
time, sys, queue
from
multiprocessing.managers
import
BaseManager
# 建立相似的QueueManager:
class
QueueManager(BaseManager):
pass
# 因为这个QueueManager只从网络上获取Queue,因此注册时只提供名字:
QueueManager.register(
'get_task_queue'
)
QueueManager.register(
'get_result_queue'
)
# 链接到服务器,也就是运行task_master.py的机器:
server_addr
=
'127.0.0.1'
print
(
'Connect to server %s...'
%
server_addr)
# 端口和验证码注意保持与task_master.py设置的彻底一致:
m
=
QueueManager(address
=
(server_addr,
5000
), authkey
=
b
'abc'
)
# 从网络链接:
m.connect()
# 获取Queue的对象:
task
=
m.get_task_queue()
result
=
m.get_result_queue()
# 从task队列取任务,并把结果写入result队列:
for
i
in
range
(
10
):
try
:
n
=
task.get(timeout
=
1
)
print
(
'run task %d * %d...'
%
(n, n))
r
=
'%d * %d = %d'
%
(n, n, n
*
n)
time.sleep(
1
)
result.put(r)
except
Queue.Empty:
print
(
'task queue is empty.'
)
# 处理结束:
print
(
'worker exit.'
)
|
https://www.jb51.net/article/120706.htm
进程切换与线程切换区别和代价
对于程序员来讲,咱们在编程时其实是不怎么操心内存问题的,对于使用Java、Python、JavaScript等动态类型语言的程序员来讲更是如此,自动内存回收机制的引入使得使用这类语言的程序员几乎彻底不用关心内存问题;即便对于编译型语言C/C++来讲,程序员须要关心的也仅仅是内存的申请和释放。
总的来讲,做为程序员(不管使用什么类型的语言)咱们根本就不关心数据以及程序被放在了物理内存的哪一个位置上(设计实现操做系统的程序员除外),咱们能够简单的认为咱们的程序独占内存,好比在32位系统下咱们的进程占用的内存空间为4G;而且咱们能够申请超过物理内存大小的空间,好比在只有256MB的系统上程序员能够申请1G大小的内存空间,这种假设极大的解放了程序员的生产力。
而这种假设实现的背后功臣就是虚拟内存。
虚拟内存是操做系统为每一个进程提供的一种抽象,每一个进程都有属于本身的、私有的、地址连续的虚拟内存,固然咱们知道最终进程的数据及代码必然要放到物理内存上,那么必须有某种机制能记住虚拟地址空间中的某个数据被放到了哪一个物理内存地址上,这就是所谓的地址空间映射,也就是虚拟内存地址与物理内存地址的映射关系,那么操做系统是如何记住这种映射关系的呢,答案就是页表,页表中记录了虚拟内存地址到物理内存地址的映射关系。有了页表就能够将虚拟地址转换为物理内存地址了,这种机制就是虚拟内存。
每一个进程都有本身的虚拟地址空间,进程内的全部线程共享进程的虚拟地址空间。
如今咱们就能够来回答这个面试题了。
进程切换与线程切换的一个最主要区别就在于进程切换涉及到虚拟地址空间的切换而线程切换则不会。由于每一个进程都有本身的虚拟地址空间,而线程是共享所在进程的虚拟地址空间的,所以同一个进程中的线程进行线程切换时不涉及虚拟地址空间的转换。
举一个不太恰当的例子,线程切换就比如你从主卧走到次卧,反正主卧和次卧都在同一个房子中(虚拟地址空间),所以你无需换鞋子、换衣服等等。可是进程切换就不同了,进程切换就比如从你家到别人家,这是两个不一样的房子(不一样的虚拟地址空间),出发时要换好衣服、鞋子等等,到别人家后还要再换鞋子等等。
所以咱们能够形象的认为线程是处在同一个屋檐下的,这里的屋檐就是虚拟地址空间,所以线程间切换无需虚拟地址空间的切换;而进程则不一样,两个不一样进程位于不一样的屋檐下,即进程位于不一样的虚拟地址空间,所以进程切换涉及到虚拟地址空间的切换,这也是为何进程切换要比线程切换慢的缘由。
有的同窗可能仍是不太明白,为何虚拟地址空间切换会比较耗时呢?
如今咱们已经知道了进程都有本身的虚拟地址空间,把虚拟地址转换为物理地址须要查找页表,页表查找是一个很慢的过程,所以一般使用Cache来缓存经常使用的地址映射,这样能够加速页表查找,这个cache就是TLB,Translation Lookaside Buffer,咱们不须要关心这个名字只须要知道TLB本质上就是一个cache,是用来加速页表查找的。因为每一个进程都有本身的虚拟地址空间,那么显然每一个进程都有本身的页表,那么当进程切换后页表也要进行切换,页表切换后TLB就失效了,cache失效致使命中率下降,那么虚拟地址转换为物理地址就会变慢,表现出来的就是程序运行会变慢,而线程切换则不会致使TLB失效,由于线程线程无需切换地址空间,所以咱们一般说线程切换要比较进程切换块,缘由就在这里。
单核cpu和多核cpu
都是一个cpu,不一样的是每一个cpu上的核心数。
多核cpu是多个单核cpu的替代方案,多核cpu减少了体积,同时也减小了功耗。
一个核心只能同时执行一个线程。
进程和线程
理解
进程是操做系统进行资源(包括cpu、内存、磁盘IO等)分配的最小单位。
线程是cpu调度和分配的基本单位。
咱们打开的聊天工具,浏览器都是一个进程。
进程可能有多个子任务,好比聊天工具要接受消息,发送消息,这些子任务就是线程。
资源分配给进程,线程共享进程资源。
对比
对比 进程 线程
定义 进程是程序运行的一个实体的运行过程,是系统进行资源分配和调配的一个独立单位 线程是进程运行和执行的最小调度单位
系统开销 建立撤销切换开销大,资源要从新分配和收回 仅保存少许寄存器的内容,开销小,在进程的地址空间执行代码
拥有资产 资源拥有的基本单位 基本上不占资源,仅有不可少的资源(程序计数器,一组寄存器和栈)
调度 资源分配的基本单位 独立调度分配的单位
安全性 进程间相互独立,互不影响 线程共享一个进程下面的资源,能够互相通讯和影响
地址空间 系统赋予的独立的内存地址空间 由相关堆栈寄存器和和线程控制表TCB组成,寄存器可被用来存储线程内的局部变量
线程切换
cpu给线程分配时间片(也就是分配给线程的时间),执行完时间片后会切换都另外一个线程。
切换以前会保存线程的状态,下次时间片再给这个线程时才能知道当前状态。
从保存线程A的状态再到切换到线程B时,从新加载线程B的状态的这个过程就叫上下文切换。
而上下切换时会消耗大量的cpu时间。
线程开销
上下文切换消耗
线程建立和消亡的开销
线程须要保存维持线程本地栈,会消耗内存
串行,并发与并行
串行
多个任务,执行时一个执行完再执行另外一个。
比喻:吃完饭再看视频。
并发
多个线程在单个核心运行,同一时间一个线程运行,系统不停切换线程,看起来像同时运行,其实是线程不停切换。
比喻: 一会跑去厨房吃饭,一会跑去客厅看视频。
并行
每一个线程分配给独立的核心,线程同时运行。
比喻:一边吃饭一边看视频。
多核下线程数量选择
计算密集型
程序主要为复杂的逻辑判断和复杂的运算。
cpu的利用率高,不用开太多的线程,开太多线程反而会由于线程切换时切换上下文而浪费资源。
IO密集型
程序主要为IO操做,好比磁盘IO(读取文件)和网络IO(网络请求)。
由于IO操做会阻塞线程,cpu利用率不高,能够开多点线程,阻塞时能够切换到其余就绪线程,提升cpu利用率。
基础补充:
CPU逻辑核心数和物理核心数是什么意思
一、物理CPU:
物理CPU就是计算机上实际配置的CPU个数。
在linux上能够打开cat /proc/cpuinfo 来查看,其中的physical id就是每一个物理CPU的ID,能找到几个physical id就表明计算机实际有几个CPU。
在linux下能够经过指令 grep ‘physical id’ /proc/cpuinfo | sort -u | wc -l 来查看物理CPU个数。
二、cpu核数:
linux的cpu核心总数也能够在/proc/cpuinfo里面经过指令cat /proc/cpuinfo查看的到,其中的core id指的是每一个物理CPU下的cpu核的id,能找到几个core id就表明计算机有几个核心。
也可使用指令cat /proc/cpuinfo | grep “cpu cores” | wc -l来统计cpu的核心总数。
三、逻辑CPU:
操做系统可使用逻辑CPU来模拟出真实CPU的效果。在以前没有多核处理器的时候,一个CPU只有一个核,而如今有了多核技术,其效果就好像把多个CPU集中在一个CPU上。
当计算机没有开启超线程时,逻辑CPU的个数就是计算机的核数。而当超线程开启后,逻辑CPU的个数是核数的两倍。
实际上逻辑CPU的数量就是平时称呼的几核几线程中的线程数量,在linux的cpuinfo中逻辑CPU数就是processor的数量。
CPU中心那块隆起的芯片就是核心,是由单晶硅以必定的生产工艺制造出来的。
CPU全部的计算、接受/存储命令、处理数据都由核心执行,各类CPU核心都具备固定的逻辑结构,一级缓存、二级缓存、执行单元、指令级单元和总线接口等逻辑单元都会有科学的布局。