我们常听到电脑是单核,多核的,那么这是什么意思呢?单核cpu指的是一次只能运行一个任务。这个任务指的就是进程,进程就好比工厂中的车间。一个车间可以有很多工人,又要引入一个新的概念,线程,线程好比一个个工人,他们共同完成一个任务。这就意味着一个进程可以包括很多线程。进程空间是被线程共享的。
这时候要引入一个新的概念互斥锁,好比工厂中一些特殊的房间,当在被使用时就要上锁以防止其他人的访问,当不用时又要解锁,线程也是同理。
进程的狭义定义:一个正在运行的程序在操作系统中被视为一个进程。
到现在可能还是有一些疑问,当我们使用单核电脑,理论上只能运行一个进程,但是同时运行多个应用时,似乎感觉都在同时运行,这是为什么呢?因为存在“时间片轮转法”的调度算法,简单来说,就是进程间交替的频率很高,看起来就像同时进行。
并发与并行
并行:同时进行,只有具备多个cpu才能实现并行
并发:是伪并行,看起来是同时运行
并行与并发都表示cpu有同时处理多任务的能力。
到底是多任务并行还是并发是我们用Python代码把握不了的
阻塞状态:正在执行的进程由于某些事件而暂时无法执行,进程受到阻塞,进入就绪状态等待系统调用
对于阻塞有不同的处理方式:同步和异步
同步:遇到阻塞状态就等着,必须等到方法调用返回后,才能继续后续方法的执行
异步:遇到阻塞状态就去处理后续其它操作(既可以基于并行也可以基于并发,通常是基于并发)
进程通常分为以下几个状态
就绪状态
进程已经准备好,已经分配到资源。
执行/运行状态
进程处于就绪状态被调度后,进程进入执行状态
阻塞(Blocked)状态(耗时操作)
正在执行的进程由于某些事件而暂时无法执行,进程受到阻塞,进入就绪状态等待系统调用。
终止状态
进程结束,或出现错误,或被系统终止,进入终止状态。无法再执行
总结并行,并发,同步,异步关系:并行并发是指操作系统处理多个任务的能力。同步异步是指处理某一个任务的过程中,遇到阻塞后不同的处理行为。
因为异步处理更加高效,所以接下来是异步的具体操作:进程,线程,协程。
Python中进程的实现
multiprocess包:是管理进程的包,几乎包含进程有关的所有子模块。
Process模块:创建进程的模块,可以在主进程中创建子进程
创建进程
from multiprocessing import Process
def f():
print("我是子进程的任务")
if __name__=='__main__':#程序开始入口
print('主进程开始')
#创建一个进程p,给进程绑定任务
p=Process(target=f)#target是固有参数
#启动创建好的进程
p.start()
print('主进程结束')
接下来观察主进程与子进程的关系
os.getpod()获取自己的进程ID号
os.getppid()获取父进程的ID号
import os
from multiprocessing import Process
from time import sleep
def f():
print('我是子进程')
sleep(1)
print(f'子进程ID号:{os.getpid()}')
print(f'该子进程的父进程ID号:{os.getppid()}')
if __name__=='__main__':
print(f'主进程开始执行!主进程的ID号:{os.getpid()}')
#创建一个进程p,绑定一个任务
p=Process(target=f)
#启动创建好的进程
p.start()
print('主进程结束')
可以借此观察主进程和子进程关系
如何给子进程传参
from multiprocessing import Process
def f(n1,n2):
print(f"我是子进程的任务{n1,n2}")
if __name__=='__main__':#程序开始入口
print('主进程开始')
#创建一个进程p,给进程绑定任务
p=Process(target=f,args=(1,2))#target,args是固有参数
#启动创建好的进程
p.start()
print('主进程结束')
下面用同步,异步代码分别申请三个网址的时间,来分析相关性能
模拟实现同步:
import time
def get_request(url):
print(f'正在请求网址数据;{url}')
time.sleep(2)
print(f'请求结束f{url}')
if __name__=='__main__':#程序开始入口
start=time.time()
urls=['www.1.com','www,2,com','www.3.com']
for url in urls:
get_request(url)
print(f'总耗时{time.time()-start}')
因为每申请一个网址就sleep两秒,申请了三个网址。所以花了六秒
那么异步操作呢:
import time
from multiprocessing import Process
def get_request(url):
print(f'正在请求网址数据;{url}')
time.sleep(2)
print(f'请求结束f{url}')
if __name__ == '__main__': # 程序开始入口
start = time.time()
urls = ['www.1.com', 'www,2,com', 'www.3.com']
ps = []#储存创建好的子进程
for url in urls:
# 创建子进程,交替执行
p = Process(target=get_request, args=(url,)) # args=(url,)必须要加,因为是元组
ps.append(p)
# 启动子进程
p.start()
for p in ps:#使的每一个子进程都执行了join操作,意味着:主进程需要等待所有执行了join操作的子进程结束后再结束
p.join()
print(f'总耗时:{time.time() - start}')
在循环里面创建进程,再在另一个循环中进行join操作,不能把join操作与创建进程放在一个循环里,不然会失去异步操作。
注意进程间的数据是隔离的
p.join可以使主进程在子进程结束后结束
可以看出来异步操作明显更加高效
只需了解:也可以用继承面向对象的方式创建进程
守护进程(了解)
主进程结束后子进程必须强制结束
语法: p.daemon=True
import time
from multiprocessing import Process
def get_request(url):
print(f'正在请求网址数据;{url}')
time.sleep(2)
print(f'请求结束f{url}')
if __name__ == '__main__': # 程序开始入口
start = time.time()
p=Process(target=get_request,args=('www.1.com'))
#将当前p子进程设置为守护进程
p.daemon=True#该操作必须放在子进程启动操作之前
p.start()
print('主进程结束')
可以看出来主进程结束后子进程直接强制结束了
进程同步锁
当我们让几个进程中并发处理,但是它们之间没有顺序,一旦开始也不受我们控制
import os
import time
import random
from multiprocessing import Process
def work(n):
print(f'{n,os.getpid()}is running')
time.sleep(random.random())
print(f'{n,os.getpid()}is done')
if __name__=='__main__':
for i in range(5):
p=Process(target=work,args=(i,))
p.start()
可以看出来进程进行的顺序并不是按创建顺序进行的
这样不用锁会在后续实现模拟抢票功能中出现错误,当只剩一张票。A用户(进程)执行到一部分可能因为阻塞原因就突然去执行另一个进程(其他用户),会导致抢票出现混乱。所以应该给一个进程上锁,当一个进程执行完之后再去执行另一个进程。
下面将简单模拟抢票机制
from multiprocessing import Lock
from multiprocessing import Process
import time,json,random
def search():#查询文件中的剩余票数
fp=open('./db.txt','r')
dic=json.load(fp)#反序列化,将文件中json数据转换为字典
print(f'剩余车票数量为:{dic["count"]}')
def get():
fp=open('./db.txt','r')
dic=json.load(fp)
time.sleep(0.1)
if dic['count']>0:
time.sleep(0.2)
dic['count']-=1#一次只能购买一次票
time.sleep(0.1)
#购买车票后,余票数量变化,重新写回文件中
json.dump(dic,open('./db.txt','w'))
print('购票成功')
def task(lock):
lock.acquire()#上锁
search()#先查询
get()#后购买
lock.release()#解锁
if __name__=='__main__':
lock=Lock()
for i in range(3):#创建三个子进程
p=Process(target=task,args=(lock,))
p.start()
这里用苹果系统可能执行不成功,因为其与Windows底层实现锁机制有区别
线程
线程是系统调度资源的最小单位,同一个进程中的多个线程是共享进程中的资源的。创建线程的成本比创建进程消耗更低,所以在异步操作中操作线程比操作进程更多一些。多线程既可以实现并行,也可以实现并发,但大多数情况下实现的是并行。但在Python中只能实现并发,这是历史遗留问题(GLL锁)。
创建线程
除了调用的模块不一样,基本一样
from threading import Thread
def f(num):
print(f'num 的值是:{num}')
if __name__=='__main__':
#创建好一个子进程(在主进程中创建)
t=Thread(target=f,args=(1,))
t.start()
也可以面向对象的方式创建线程
线程中调用join方法跟进程一样
守护线程
from threading import Thread
import time
def work():
time.sleep(1)
print('子进程正在执行')
if __name__ == '__main__':
t=Thread(target=work)
t.daemon=True
t.start()
print('主进程结束')
线程共享同一进程资源
from threading import Thread
import time
def work():
global n
n=0
if __name__ == '__main__':
n=1#全局变量
t=Thread(target=work)
t.start()
print(n)
可以看出来子线程共享主进程的变量n,子线程修改n,主进程也跟着修改了。
多线程使用
运用的是面向对象的方式创建的线程
from threading import Thread
import time
class MyThread(Thread):
def __init__(self):
super().__init__()
def run(self):
print('当前子线程正在执行')
time.sleep(2)
print(('当前子线程执行结束'))
if __name__=='__main__':
start=time.time()
ts=[]
for i in range(3):
t=MyThread()
t.start()
ts.append(t)
for t in ts:
t.join()
print(f'总耗时:{time.time()-start}')
计算密集型任务:任务中大多数是进行计算的
经过验证在运行计算密集型任务时,多进程更加高效,因为可以享用多核的优势
IO密集型任务:任务中大多数是阻IO
IO密集型任务最好使用多线程,因为创建线程的代价更小一些
在爬虫中更多使用多线程,因为在爬虫中会出现反复发请求,返回数据的情况
多线程上锁
from threading import Thread,Lock
import time,random
def work():
global n
lock.acquire()
temp=n
time.sleep(random.random())
n=temp-1
lock.release()
if __name__=='__main__':
n=10
ts=[]
lock=Lock()
for i in range(10):
t=Thread(target=work)
t.start()
ts.append(t)
for t in ts:
t.join()
print('全局变量n的值为 ',n)
线程池
在内存池中的池化技术是一种内存管理技术,其向操作系统申请一块较大的内存,当程序需要内存时直接向内存池中申请,而不是向操作系统中申请,当程序释放内存时,并不是直接返回给操作系统,而是返还给内存池。大大提高任务效率
线程池在爬虫中也十分重要
import time
from multiprocessing.dummy import Pool
start=time.time()
urls=['www.1.com','www.2.com','www.3.com','www.4.com','www.5.com']
def get_request(url):
print(f'正在请求数据:{url}')
time.sleep(2)
print(f'请求结束{url}')
pool=Pool(5)#创建线程池对象,池中有五个线程对象,根据需求创建多少个线程对象
pool.map(get_request,urls)#表示urls中每个参数依次传给get_request函数
print(f'总耗时:{time.time()-start}')
pool.close()
map中已经实现了join
可以看出来执行五次依旧大约是两秒