目录
进程
进程的定义
- 进程是资源分配的最小单位
- 一个运行起来的程序就是一个进程
进程的注意点
- 程序并不能单独运行,只有将程序装载到内存中,系统为它分配资源才能运行,这种执行的程序就称之为进程
- 程序是指令的集合,它是进程运行的静态描述文本,进程是程序的一次执行活动,属于动态概念
- 在多道编程中,允许多个程序同时加载到内存中,在操作系统的调度下,可以实现并发的执行
- 进程的出现让每个用户感觉到自己独享CPU,因此,进程是为了在CPU上实现多道编程而提出的
- 进程之间有独立的内存,各进程之间不能相互访问
- 创建新进程需要对父进程进此复制
多道编程:在计算机内存中同时存放几道相互独立的程序,共享系统资源,相互穿插运行
单道编程:计算机内存中只允许一个程序运行
进程并发性:
- 在一个系统中,同时会存在多个进程被加载到内存中,同处于开始到结束之间的状态
- 对于一个单CPU系统来说,程序同时处于运行状态只是一种宏观上的概念,他们虽然都已经开始运行,但就微观而言,任意时刻,CPU上运行的程序只有一个
- 由于操作系统分时,让每个进程都觉得自己独占CPU等资源
注:如果是多核CPU(处理器)实际上是可以实现真正意义的同一时间点有多个线程同时运行
注意:进程具有独立的内存空间,所以互相之间不能访问对方数据
进程间互相访问数据的四种方式:
- 利用Queues实现父进程到子进程的数据传递(父子进程通信)
- 使用管道pipe实现同一程序下两个进程通信
- Managers实现同一程序下多个进程通信
- 借助redis,Rabbit MQ中间件进行数据传递(不同进程间通信)
进程池
为什么需要进程池:
- 一次性开启指定数量的进程
- 防止进程开启数量过多导致服务器压力过大
from multiprocessing import Process,Pool
import time,os
def foo(i):
time.sleep(2)
print("in the process",os.getpid()) #打印子进程的pid
return i+100
def call(arg):
print('-->exec done:',arg,os.getpid())
if __name__ == '__main__':
pool = Pool(3) #进程池最多允许5个进程放入进程池
print("主进程pid:",os.getpid()) #打印父进程的pid
for i in range(10):
#用法1 callback作用是指定只有当Foo运行结束后就执行callback调用的函数,父进程调用的callback函数
pool.apply_async(func=foo, args=(i,),callback=call)
#用法2 串行 启动进程不在用Process而是直接用pool.apply()
pool.apply(func=foo, args=(i,))
print('end')
pool.close() #关闭pool
pool.join() #进程池中进程执行完毕后再关闭,如果注释,那么程序直接关闭
进程和程序的区别
- 程序是一个机器代码指令和数据的集合,所以程序是一个静态的实体
- 进程是程序运行在数据集上的动态过程
- 进程是系统进行资源分配和调度的一个独立单位
- 一个程序对应多个进程,一个进程为多个程序服务
- 一个程序执行在不同的数据集上就成为不同的进程,可以用进程控制块来唯一标识每个进程
有了进程为什么还要线程?
进程优点:
提供了多道编程,让我们感觉我们每个人都拥有自己的CPU和其他资源,可以提高计算机的利用率
进程的两个重要缺点
- 进程只能在一个时间干一件事,如果想同时干两件事或多件事,进程就无能为力了。
- 进程在执行的过程中如果阻塞,即使进程中有些工作不依赖于输入的数据,也将无法执行(例如等待输入,整个进程就会挂起)。
线程
线程的定义
- 线程是操作系统的调度的最小单位
- 线程是进程的真正的执行者,是一些指令的集合(进程资源的拥有者)
- 同一进程下的读多个线程共享内存空间,数据直接访问(数据共享)
- 为了保证数据安全,必须使用线程锁
线程的注意点
- 线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运行单位
- 一条线程是指进程中的一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务
- 无论启动多少个线程,有多少个CPU,python在执行的时候在同一时刻只允许一个线程运行
- 所有在同一进程里的线程是共享同一块内存空间的,不同进程间内存空间不同
- 同一进程中的各线程可以相互访问资源,线程可以操作 同进程中的其他线程,但进程仅能操作子进程
- 两个进程想通信,必须要通过一个中间代理
- 对主线程修改可能会影响其他子线程,对主进程修改不会影响其他进程因为进程间内存相互独立,但是同一进程下的线程共享内存
GIL全局解释器锁
-
在python全局解释器下,保证同一时间仅有一个线程对资源操作
-
防止多个线程都修改数据
线程锁(互斥锁)
- 当一个线程对某个资源进行CPU计算的操作时加一个线程锁,只有当前线程计算完成主动释放锁,其他线程才能对其操作
- 可以解决还未计算完成,释放GIL锁后其他线程对这个资源操作导致混乱的问题
- 线程锁本质把线程中的数据加了一把互斥锁
- mysql中共享锁 & 互斥锁
- mysql共享锁:共享锁,所有线程都能读,而不能写
- mysql排它锁:排它,任何线程读取这个这个数据的权利都没有
- 加上线程锁之后所有其他线程,读都不能读这个数据
线程和进程的区别
- 进程包含线程
- 线程共享内存空间
- 进程内存是独立的(不可相互访问)
- 进程可以生成子进程,子进程之间不能互相访问
- 线程可以帮助应用程序同时做几件事情
for循环同时启动多个线程
import threading
import time
def sayhi(num): #定义每个线程要运行的函数
print("running on number:%s" %num)
time.sleep(3)
for i in range(10):
t = threading.Thread(target=sayhi,args=('t-%s'%i,))
t.start()
join()实现所有线程都执行结束后再执行主线程
import threading
import time
start_time = time.time()
def sayhi(num): #定义每个线程要运行的函数
print("running on number:%s" %num)
time.sleep(3)
t_objs = [] #将进程实例对象存储在这个列表中
for i in range(50):
t = threading.Thread(target=sayhi,args=('t-%s'%i,))
t.start() #启动一个线程,程序不会阻塞
t_objs.append(t)
print(threading.active_count()) #打印当前活跃进程数量
for t in t_objs: #利用for循环等待上面50个进程全部结束
t.join() #阻塞某个程序
print(threading.current_thread()) #打印执行这个命令进程
print("----------------all threads has finished.....")
print(threading.active_count())
print('cost time:',time.time() - start_time)
setDaemon()守护线程,主线程退出时,需要子线程随主线程退出
import threading
import time
start_time = time.time()
def sayhi(num): #定义每个线程要运行的函数
print("running on number:%s" %num)
time.sleep(3)
for i in range(50):
t = threading.Thread(target=sayhi,args=('t-%s'%i,))
t.setDaemon(True) #把当前线程变成守护线程,必须在t.start()前设置
t.start() #启动一个线程,程序不会阻塞
print('cost time:',time.time() - start_time)
线程实现并发
import requests
from concurrent.futures import ThreadPoolExecutor
def fetch_request(url):
result = requests.get(url)
print(result.text)
url_list = [
'https://www.baidu.com',
'https://www.google.com/', #google页面会卡住,知道页面超时后这个进程才结束
'http://dig.chouti.com/', #chouti页面内容会直接返回,不会等待Google页面的返回
]
pool = ThreadPoolExecutor(10) # 创建一个线程池,最多开10个线程
for url in url_list:
pool.submit(fetch_request,url) # 去线程池中获取一个线程,线程去执行fetch_request方法
pool.shutdown(True)
协程
协程的定义
- 协程又称微线程,是一种用户状态的轻量级线程
- 协程的本质是个单线程
- 协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈
- 协程主要作用是在单线程的条件下实现并发的效果,但实际上还是串行
协程的缺点(无法利用多核资源)
- 无法利用多核资源,协程的本质是个单线程,不能同时将单个CPU的多个核上,协程需要和进程配合才能运行在多CPU上
- 线程阻塞(Blocking)操作(如IO时)会阻塞掉整个程序
协程的优点
- 可以处理高并发
- 节省资源
- 无需线程上下文切换的开销(可以理解为协程切换就是在不同函数间切换,不用像线程那样切换上下文CPU)
- 用法: 最简单的方法是多进程+协程,既充分利用多核,又充分发挥协程的高效率,可获得极高的性能。
协程为何能处理大并发
- greeenlet遇到I/O手动切换
协程遇到I/O操作就切换,其实Gevent模块仅仅是对greenlet的封装,将I/O的手动切换变成自动切换
- 协程之所以快是因为遇到I/O操作就切换(最后只有CPU运算)
- 这里先演示用greenlet实现手动的对各个协程之间切换
- 其实Gevent模块仅仅是对greenlet的再封装,将I/O间的手动切换变成自动切换
greeenlet遇到I/O手动切换
from greenlet import greenlet
def test1():
print(12) #4 gr1会调用test1()先打印12
gr2.switch() #5 然后gr2.switch()就会切换到gr2这个协程
print(34) #8 由于在test2()切换到了gr1,所以gr1又从上次停止的位置开始执行
gr2.switch() #9 在这里又切换到gr2,会再次切换到test2()中执行
def test2():
print(56) #6 启动gr2后会调用test2()打印56
gr1.switch() #7 然后又切换到gr1
print(78) #10 切换到gr2后会接着上次执行,打印78
gr1 = greenlet(test1) #1 启动一个协程gr1
gr2 = greenlet(test2) #2 启动第二个协程gr2
gr1.switch() #3 首先gr1.switch() 就会去执行gr1这个协程
- Gevent遇到I/O自动切换
Gevent是一个第三方库,可以通过gevent实现并发同步或异步编程,Gevent原理是只要是遇到I/O操作就自动切换下一个协程
- Gevent 是一个第三方库,可以轻松通过gevent实现并发同步或异步编程
- 在gevent中用到的主要模式是Greenlet, 它是以C扩展模块形式接入Python的轻量级协程
- Greenlet全部运行在主程序操作系统进程的内部,但它们被协作式地调度。
- Gevent原理是只要遇到I/O操作就会自动切换到下一个协程
使用Gevent实现并发下载网页与串行下载网页时间比较
from urllib import request
import gevent,time
from gevent import monkey
monkey.patch_all() #把当前程序所有的I/O操作给我单独做上标记
def f(url):
print('GET: %s' % url)
resp = request.urlopen(url)
data = resp.read()
print('%d bytes received from %s.' % (len(data), url))
#1 并发执行部分
time_binxing = time.time()
gevent.joinall([
gevent.spawn(f, 'https://www.python.org/'),
gevent.spawn(f, 'https://www.yahoo.com/'),
gevent.spawn(f, 'https://github.com/'),
])
print("并行时间:",time.time()-time_binxing)
#2 串行部分
time_chuanxing = time.time()
urls = [
'https://www.python.org/',
'https://www.yahoo.com/',
'https://github.com/',
]
for url in urls:
f(url)
print("串行时间:",time.time()-time_chuanxing)
# 注:为什么要在文件开通使用monkey.patch_all()
# 1. 因为有很多模块在使用I / O操作时Gevent是无法捕获的,所以为了使Gevent能够识别出程序中的I / O操作。
# 2. 就必须使用Gevent模块的monkey模块,把当前程序所有的I / O操作给我单独做上标记
# 3.使用monkey做标记仅用两步即可:
第一步(导入monkey模块): from gevent import monkey
第二步(声明做标记) : monkey.patch_all()
Gevent实现简单的自动切换小例子
注:在Gevent模仿I/O切换的时候,只要遇到I/O就会切换,哪怕gevent.sleep(0)也要切换一次
import gevent
def func1():
print('\033[31;1m第一次打印\033[0m')
gevent.sleep(2) # 为什么用gevent.sleep()而不是time.sleep()因为是为了模仿I/O
print('\033[31;1m第六次打印\033[0m')
def func2():
print('\033[32;1m第二次打印\033[0m')
gevent.sleep(1)
print('\033[32;1m第四次打印\033[0m')
def func3():
print('\033[32;1m第三次打印\033[0m')
gevent.sleep(1)
print('\033[32;1m第五次打印\033[0m')
gevent.joinall([ # 将要启动的多个协程放到event.joinall的列表中,即可实现自动切换
gevent.spawn(func1), # gevent.spawn(func1)启动这个协程
gevent.spawn(func2),
gevent.spawn(func3),
])
# 运行结果:
# 第一次打印
# 第二次打印
# 第三次打印
# 第四次打印
# 第五次打印
# 第六次打印