python的进程线程协程
进程
进程定义
1. 进程是资源分配最小单位
2. 当一个可执行程序被系统执行(分配内存等资源)就变成了一个进程
进程定义拓展回答内容
- 程序并不能单独运行,只有将程序装载到内存中,系统为它分配资源才能运行,这种执行的程序就称之为进程
- 程序和进程的区别就在于:程序是指令的集合,它是进程运行的静态描述文本;进程是程序的一次执行活动,属于动态概念
- 在多道编程中,我们允许多个程序同时加载到内存中,在操作系统的调度下,可以实现并发地执行。
- 进程的出现让每个用户感觉到自己独享CPU,因此,进程就是为了在CPU上实现多道编程而提出的。
- 进程之间有自己独立的内存,各进程之间不能相互访问
- 创建一个新线程很简单,创建新进程需要对父进程进行复制
多道编程概念
- 多道编程: 在计算机内存中同时存放几道相互独立的程序,他们共享系统资源,相互穿插运行
- 单道编程: 计算机内存中只允许一个的程序运行
有了进程为什么还要线程?
- 进程优点:
提供了多道编程,让我们感觉我们每个人都拥有自己的CPU和其他资源,可以提高计算机的利用率 - 进程的两个重要缺点
a. 第一点:进程只能在一个时间干一件事,如果想同时干两件事或多件事,进程就无能为力了。
b. 第二点:进程在执行的过程中如果阻塞,即使进程中有些工作不依赖于输入的数据,也将无法执行(例如等待输入,整个进程就会挂起)。
c. 例如,我们在使用qq聊天, qq做为一个独立进程如果同一时间只能干一件事,那他如何实现在同一时刻 即能监听键盘输入、又能监听其它人给你发的消息
d. 你会说,操作系统不是有分时么?分时是指在不同进程间的分时呀
e. 即操作系统处理一会你的qq任务,又切换到word文档任务上了,每个cpu时间片分给你的qq程序时,你的qq还是只能同时干一件事呀
进程池
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() #进程池中进程执行完毕后再关闭,如果注释,那么程序直接关闭。
进程和程序的区别
- 程序只是一个普通文件,是一个机器代码指令和数据的集合,所以,程序是一个静态的实体
- 而进程是程序运行在数据集上的动态过程,进程是一个动态实体,它应创建而产生,应调度执行因等待资源或事件而被处于等待状态,因完成任务而被撤消
- 进程是系统进行资源分配和调度的一个独立单位
4.一个程序对应多个进程,一个进程为多个程序服务(两者之间是多对多的关系) - 一个程序执行在不同的数据集上就成为不同的进程,可以用进程控制块来唯一地标识每个进程
线程
线程定义
- 线程是操作系统调度的最小单位
- 它被包含在进程之中,是进程中的实际运作单位
- 进程本身是无法自己执行的,要操作cpu,必须创建一个线程,线程是一系列指令的集合
线程定义拓展回答内容
- 线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位
- 一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务
- 无论你启多少个线程,你有多少个cpu, Python在执行的时候会淡定的在同一时刻只允许一个线程运行
- 进程本身是无法自己执行的,要操作cpu,必须创建一个线程,线程是一系列指令的集合
- 所有在同一个进程里的线程是共享同一块内存空间的,不同进程间内存空间不同
- 同一个进程中的各线程可以相互访问资源,线程可以操作同进程中的其他线程,但进程仅能操作子进程
- 两个进程想通信,必须要通过一个中间代理
- 对主线程的修改可能回影响其他子线程,对主进程修改不会影响其他进程因为进程间内存相互独立,但是同一进程下的线程共享内存
进程和线程的区别
- 进程包含线程
- 线程共享内存空间
- 进程内存是独立的(不可互相访问)
- 进程可以生成子进程,子进程之间互相不能互相访问(相当于在父级进程克隆两个子进程)
- 在一个进程里面线程之间可以交流。两个进程想通信,必须通过一个中间代理来实现
- 创建新线程很简单,创建新进程需要对其父进程进行克隆。
- 一个线程可以控制或操作同一个进程里面的其它线程。但进程只能操作子进程。
- 父进程可以修改不影响子进程,但不能修改。
- 线程可以帮助应用程序同时做几件事
for循环同时启动多个线程
import threading
import 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.start()
t.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)
GIL全局解释器锁:保证同一时间仅有一个线程对资源有操作权限
作用:在一个进程内,同一时刻只能有一个线程执行
说明:python多线程中GIL锁只是在CPU操作时(如:计算)才是串行的,其他都是并行的,所以比串行快很多
- 为了解决不同线程同时访问同一资源时,数据保护问题,而产生了GIL
- GIL在解释器的层面限制了程序在同一时间只有一个线程被CPU实际执行,而不管你的程序里实际开了多少条线程
- 为了解决这个问题,CPython自己定义了一个全局解释器锁,同一时间仅仅有一个线程可以拿到这个数据
- python之所以会产生这种不好的状况是因为python启用一个线程是调用操作系统原生线程,就是C接口
- 但是这仅仅是CPython这个版本的问题,在PyPy,中就没有这种缺陷
线程锁
- 当一个线程对某个资源进行CPU计算的操作时加一个线程锁,只有当前线程计算完成主动释放锁,其他线程才能对其操作
- 这样就可以防止还未计算完成,释放GIL锁后其他线程对这个资源操作导致混乱问题
用户锁使用举例
import time
import threading
lock = threading.Lock() #1 生成全局锁
def addNum():
global num #2 在每个线程中都获取这个全局变量
print('--get num:',num )
time.sleep(1)
lock.acquire() #3 修改数据前加锁
num -= 1 #4 对此公共变量进行-1操作
lock.release() #5 修改后释放
Semaphore(信号量)
- 互斥锁 同时只允许一个线程更改数据,而Semaphore是同时允许一定数量的线程更改数据
- 比如厕所有3个坑,那最多只允许3个人上厕所,后面的人只能等里面有人出来了才能再进去
- 作用就是同一时刻允许运行的线程数量
线程池实现并发
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的栈里,协程拥有自己的寄存器上下文和栈,
- 协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈
- 协程能保留上一次调用时的状态(即所有局部状态的一个特定组合),每次过程重入时,就相当于进入上一次调用的状态
- 协程最主要的作用是在单线程的条件下实现并发的效果,但实际上还是串行的(像yield一样)
协程缺点(无法利用多核资源)
协程的本质是个单线程,它不能同时将 单个CPU 的多个核用上,协程需要和进程配合才能运行在多CPU上线程阻塞(Blocking)操作(如IO时)会阻塞掉整个程序
协程为何能处理大并发1:Greenlet遇到I/O手动切换
- 协程之所以快是因为遇到I/O操作就切换(最后只有CPU运算)
- 这里先演示用greenlet实现手动的对各个协程之间切换
- 其实Gevent模块仅仅是对greenlet的再封装,将I/O间的手动切换变成自动切换
协程为何能处理大并发2:Gevent遇到I/O自动切换
- Gevent 是一个第三方库,可以轻松通过gevent实现并发同步或异步编程
- 在gevent中用到的主要模式是Greenlet, 它是以C扩展模块形式接入Python的轻量级协程
- Greenlet全部运行在主程序操作系统进程的内部,但它们被协作式地调度。
- Gevent原理是只要遇到I/O操作就会自动切换到下一个协程
5. 使用协程处理并发
注:Gevent只用起一个线程,当请求发出去后gevent就不管,永远就只有一个线程工作,谁先回来先处理
import gevent
import requests
from gevent import monkey
monkey.patch_all()
# 这些请求谁先回来就先处理谁
def fetch_async(method, url, req_kwargs):
response = requests.request(method=method, url=url, **req_kwargs)
print(response.url, response.content)
# ##### 发送请求 #####
gevent.joinall([
gevent.spawn(fetch_async, method='get', url='https://www.python.org/', req_kwargs={}),
gevent.spawn(fetch_async, method='get', url='https://www.google.com/', req_kwargs={}),
gevent.spawn(fetch_async, method='get', url='https://github.com/', req_kwargs={}),
])