文章目录
- 一、进程
- 1.创建多进程
- 2.查看进程id
- 3.进程池
- 4.进程间的互相通信
- 二、线程
- 1.threading线程模块
- 2.创建多线程
- 3.互斥锁
- 4.死锁
- 5.线程间的互相通信
- 三、协程
- 1.认识协程
- 2.gevent模块在爬虫中的应用
- 四、多线程、多进程、协程的区别
分类 | 定义 |
---|---|
程序 | 一个应用可以当做一个程序,比如qq软件。 |
进程 | 程序运行的最小的资源分配单位,一个程序可以有多个进程。每个进程中至少有一个线程 |
线程 | cpu最小的调度单位,必须依赖进程而存在。 |
协程 | 又称微线程、纤程。是一种比线程更加轻量级的并发方式,它不需要线程上下文切换的开销,可以在单线程中实现并发 |
一、进程
- 多进程是指在同一台计算机中同时运行多个独立的进程。每个进程都有自己的地址空间,可用于执行一些特定的任务。这些进程可以同时执行,从而提高了程序的性能和效率。多进程可以在多核计算机上实现真正的并行计算,可以同时运行多个程序,从而大大提高了计算机的利用率。
- 在多进程编程中,进程之间是独立的,它们各自运行自己的程序,有自己的地址空间和系统资源,不会相互干扰。每个进程都有自己的PID(进程标识符),它是一个唯一的标识符,用于在系统中识别该进程。多进程可以通过IPC(进程间通信)来实现数据共享和通信。
- Python的多进程模块
multiprocessing
提供了一种方便的方法来创建和管理多个进程。它可以在单个Python解释器中创建多个进程,从而实现并行计算。此模块还提供了一些方便的功能,如进程池、进程通信和共享内存等。 - 多进程编程可以在很多场景中提高程序的运行效率,如CPU密集型任务、I/O密集型任务、并行计算、大规模数据处理和机器学习等。但需要注意多进程之间的数据同步和通信,以避免数据竞争和死锁等问题。
1.创建多进程
①方式1:
import time
from multiprocessing import Process # 进程模块
def sing():
for i in range(1, 6):
print('我在唱歌第{}句歌词'.format(i))
time.sleep(1)
def dance():
for i in range(1, 6):
print('我在跳舞第{}段舞蹈'.format(i))
time.sleep(1)
if __name__ == '__main__':
t1 = Process(target=sing) # ------------target=函数名
t2 = Process(target=dance)
t1.start() # -------------------------启动该进程
t2.start()
方式②:进程类创建
import os
import time
from multiprocessing import Process
class SubProcess(Process):
def __init__(self, x):
super().__init__()
self.x = x
def run(self): # 将父类的run函数重写,进程启动时候调用此方法
for i in range(3):
print('启动进程', i, os.getpid())
time.sleep(1)
if __name__ == '__main__':
p = SubProcess(3)
p.start()
p1 = SubProcess(3)
p1.start()
'''
启动进程 0 22064
启动进程 0 10628
启动进程 1 22064
启动进程 1 10628
启动进程 2 22064
启动进程 2 10628
'''
2.查看进程id
import os
from multiprocessing import Process
def fun1(n):
print('参数为', n, '进程id为', os.getpid(), '父进程id为', os.getppid()) # 主进程id使用getppid方法
def fun2(n):
print('参数为', n, '进程id为', os.getpid(), '父进程id为', os.getppid()) # 子进程采用getpid方法
if __name__ == '__main__':
print('父进程:', os.getpid())
t1 = Process(target=fun1, args=(32,))
t2 = Process(target=fun2, args=(3232432,)) # 传入参数时候是按元组方式进行传入
t1.start()
t2.start()
"""
父进程: 22624
参数为 32 进程id为 23272 父进程id为 22624
参数为 3232432 进程id为 11100 父进程id为 22624
"""
3.进程池
1.为什么使用进程池
由于进程启动的开销比较大,使用多进程的时候会导致大量内存空间被消耗。为了防止这种情况发生可以使用进程池,(由于启动线程的开销比较小,所以不需要线程池这种概念,多线程只会频繁得切换cpu导致系统变慢,并不会占用过多的内存空间)
2.进程池中常用方法
函数名 | 作用 |
---|---|
apply() | 同步执行(串行) |
apply_async() | 异步执行(并行) |
join() | 主进程等待所有子进程执行完毕。必须在close或terminate()之后。 |
close() | 等待所有进程结束后,才关闭进程池。 |
3.举例使用
from multiprocessing import Pool
import time
def downLoad(movie):
for i in range(5):
print(movie, '下载进度%.2f%%' % ((i + 1) / 5 * 100))
time.sleep(1)
return movie
def alert(name):
print(name, '下载完成!')
if __name__ == '__main__':
movies = ['蔡徐坤吃饭视频', '蔡徐坤洗澡视频', '蔡徐坤化妆视频', '蔡徐坤擦口红视频']
p = Pool(2)
for movie in movies:
p.apply_async(downLoad, args=(movie,), callback=alert) # -----------callback函数回调,执行完第一个函数毁掉
p.close()
p.join() # ----------------------------使主进程堵塞知道p进程执行完毕
"""
蔡徐坤吃饭视频 下载进度20.00%
蔡徐坤洗澡视频 下载进度20.00%
蔡徐坤吃饭视频 下载进度40.00%
蔡徐坤洗澡视频 下载进度40.00%
蔡徐坤洗澡视频 下载进度60.00%
蔡徐坤吃饭视频 下载进度60.00%
蔡徐坤洗澡视频 下载进度80.00%
蔡徐坤吃饭视频 下载进度80.00%
蔡徐坤吃饭视频 下载进度100.00%
蔡徐坤洗澡视频 下载进度100.00%
蔡徐坤化妆视频 下载进度20.00%
蔡徐坤洗澡视频 下载完成!
蔡徐坤擦口红视频 下载进度20.00%
蔡徐坤吃饭视频 下载完成!
蔡徐坤擦口红视频 下载进度40.00%
蔡徐坤化妆视频 下载进度40.00%
蔡徐坤擦口红视频 下载进度60.00%
蔡徐坤化妆视频 下载进度60.00%
蔡徐坤化妆视频 下载进度80.00%
蔡徐坤擦口红视频 下载进度80.00%
蔡徐坤化妆视频 下载进度100.00%
蔡徐坤擦口红视频 下载进度100.00%
蔡徐坤化妆视频 下载完成!
蔡徐坤擦口红视频 下载完成!
"""
4.进程间的互相通信
进程之间是各个独立的,要是进程与进程之间需要互相使用变量或者空间等等该怎么操作呢?方式有很多,这里我们使用Python的Queue队列(是一种先进先出的存储数据结构)
举例:
from multiprocessing import Queue
a=Queue() # 队列的最大容纳量
a.put(1)#传入数据
a.put(2)
a.put(3)
while a.qsize()>0:#求出队列现在含有数据的长度,每取出一个数据长度会-1
print(a.get(),end=' ')#取出数据
# empty和full方法
b=Queue(3)
print(b.empty())#队列空返回True
b.put(1)
b.put(1)
b.put(1)
print(b.full())#队列满返回True
结果:1 2 3 True True
二、线程
- 线程是程序中一个更加轻量级的多路径执行实现方式。在系统级别,程序会根据系统为其提供的执行时间以及其他程序需要的执行时间统一调度执行。在程序内部,存在承载着不同任务的一个或多个同时或近乎同时执行的线程。实际上系统本身会管理线程的执行,调度其运行在某个核心上或在其他线程需要执行的时候强制中段该线程的执行。
- 在非并发程序中,只有一个线程的执行。这个线程开始和结束于程序的main函数,并由一个接一个的不同方法或函数来实现程序的全部行为。相比之下,支持并发的程序可以启动一个线程,并按照需求增加线程以创建额外的执行路径。每一条新路径都有独立的自定义启动入口,在程序的main函数中独立运行代码。
- 而多线程是指操作系统在单个进程内支持多个并发执行路径的能力,可以提高cpu的利用率。
- python中使用
threading
模块来操作多线程。
1.threading线程模块
该模块的一些常见方法
函数名 | 作用 |
---|---|
threading.currentThread() | 返回当前的线程变量 |
threading.enumerate() | 返回一个包含正在运行的现成的list |
threading.activeCount () | 求正在运行的线程的列表长度 |
2.创建多线程
import threading
t=threading.Thread(
target= 函数名, # 需要执行的函数的名字
args=(参数1,参数2...), # 上述函数的参数,是个元组,无参数可不写,单个参数记住加,
callback=回调函数 # 不需要回调可不写
)
t.start() # 线程启动
①创建方式1:
import threading
import time
def sing():
for i in range(1, 6):
print('我在唱歌第{}句歌词'.format(i))
time.sleep(1)
def dance():
for i in range(1, 6):
print('我在跳舞第{}段舞蹈'.format(i))
time.sleep(1)
if __name__ == '__main__':
t1 = threading.Thread(target=sing)
t2 = threading.Thread(target=dance)
t1.start()
t2.start()
②创建方式2:通过线程类
使用线程类的方法,需要继承父类的init方法,这里有几种继承方式
调用父类的init方法有两种:
#方法1:
super().init()
#方法2:
super(MyThread, self).__init__()
#方法3:
threading.Thread.__init__(self)
然后我们需要重写run方法,最后生成多个类对象,然后start调用
# -*- coding: utf-8 -*-
import time
from threading import Thread
class Test(Thread):
def __init__(self):
super().__init__()
def sing(self):
time.sleep(3)
print('鸡你太美~~~~~')
def run(self):
self.sing()
if __name__ == '__main__':
# 五个线程
for i in range(5):
Test().start()
该程序运行后,理论上五次循环需要15s左右,但是使用该线程类,会实现并发运行,约等于3s左右运行
3.互斥锁
多个线程对公有变量处理时,容易造成数据的混乱,造成线程不安全
1.概念
互斥锁:当多个线程几乎同时修改某一个共享数据的时候,需要进行同步控制。线程同步能够保证多个线程安全访问“竞争资源”,最简单的同步机制就是引用互斥锁。互斥锁为资源引入一个状态:锁定/非锁定状态。某个线程要更改共享数据时,先将其锁定,此时资源状态为“锁定”,其它线程不能更改;直到当前线程释放资源,将资源变成"非锁定"状态,其它的线程才能再次锁定该资源。互斥锁保证了每次只有一个线程进行“写操作”,从而保证多个线程数据正确性。
2.锁的好处
- 确定了某段代码只能由一个线程从头到尾完整地执行。
- 保证全局变量的安全
3.锁的坏处
- 阻止了多线程的并发执行,包含锁的某段代码实际上只能以单线程模块执行,效率大大地下降了。
- 由于可以存在多个锁,不同的线程持有不同的锁,并试图获取对方持有的锁的时,可能会造成“死锁”。
3.使用步骤
from threading import Lock
l=LOCK() #创建方式
l.acquire()# 上锁方式
l.release()#-解锁方式
4.使用举例
举例:假设两个线程t1和t2都要对num进行+1操作,t1和t2都各自对num修改10次,num最终的值应该为20。紧接着我们把10次改为100000000次,由于多线程并发访问,有可能产生不一样的结果。
代码示例:
from threading import Thread
g_num = 0
def test1():
global g_num
for i in range(1000000):
# g_num += 1
b = g_num + 1
g_num = b
print("---test1---g_num=%d"%g_num)
def test2():
global g_num
for i in range(1000000):
a = g_num + 1
g_num = a
print("---test2---g_num=%d"%g_num)
if __name__ == '__main__':
p1 = Thread(target=test1)
p1.start()
p2 = Thread(target=test2)
p2.start()
#--------------------运行结果------------------------
---test2---g_num=1559989
---test1---g_num=1516811
生成结果和我们预想的不一样,是因为没有控制多个线程对同一资源的访问,对数据造成破坏,使得线程运行的结果达不到预期。这种现象我们称为“线程不安全”,想要解决这种问题,我们使用互斥锁:
- (1)p1被调用的时候,获取g_num=0,然后上一把锁,即不允许其它线程操作num。
- (2)对num进行加1
- (3)解锁,g_num = 1,其它的线程就可以使用g_num的值,而且g_num的值是而不是原来的0
- (4)同理其它线程在对num进行修改时,都要先上锁,处理完成后再解锁。在上锁的整个过程中,不允许其它线程访问,保证了数据的正确性。
加入互斥锁后的代码:
import threading,time
g_num = 0#全局变量
def w1():
global g_num
for i in range(10000000):
mutexFlag = mutex.acquire(True)#上锁
if mutexFlag:
g_num+=1
mutex.release()#解锁
print("test1---g_num=%d"%g_num)
def w2():
global g_num
for i in range(10000000):
mutexFlag = mutex.acquire(True)# 上锁
if mutexFlag:
g_num+=1
mutex.release()# 解锁
print("test2---g_num=%d" % g_num)
if __name__ == "__main__":
mutex = threading.Lock()#创建锁
t1 = threading.Thread(target=w1)
t1.start()
t2 = threading.Thread(target=w2)
t2.start()
4.死锁
1.死锁的产生
在线程间共享多个资源的时候,如果两个线程分别占有一部分资源并且同时等待对方的资源时,就会造成死锁。尽管死锁很少发生,但一旦发生就会造成应用的停止响应。常见有这两种情况:
- 同一个线程先后两次调用lock,在第二次调用时,由于锁已经被自己占用,该线程会挂起等待自己释放锁,由于该线程已被挂起而没有机会释放锁,因此 它将一直处于挂起等待状态,变为死锁;
- 线程A获得了锁1,线程B获得了锁2,这时线程A调用lock试图获得锁2,结果是需要挂起等待线程B释放锁2,而这时线程B也调用lock试图获得锁1,结果是需要挂起等待线程A释放锁1,于是线程A和B都在等待对方释放自己才释放,从而造成两个都永远处于挂起状态,造成死锁。
2.避免死锁发生
要最大可能地避免、预防和解除死锁。需要在系统设计、线程调度等方面注意如何确定资源的合理分配算法,避免线程永久占据系统资源。此外,也要防止线程在处于等待状态的情况下占用资源,在系统运行过程中,对线程发出的每一个系统能够满足的资源申请进行动态检查,并根据检查结果决定是否分配资源,若分配后系统可能发生死锁,则不予分配,否则予以分配。因此,对资源的分配要给予合理的规划。
5.线程间的互相通信
- 关于线程与线程之间可以借助
from queue import Queue
来实现,此处不举例。 - 进程与进程,线程与线程之间的通信借助队列来实现,一般常常配合redis来实现。
三、协程
1.认识协程
线程和协程都是实现并发编程的方式,但它们有一些不同的特点和应用场景。协程通常具有以下特点:
- 协程中的代码可以暂停执行,并且在需要的时候可以恢复执行。
- 多个协程可以在同一线程中并发执行,但是任意时刻只有一个协程在执行。
- 协程通常是基于事件循环(Event Loop)实现的,事件循环负责调度协程的执行。
线程的并发编程通常会受到多线程竞争、死锁、上下文切换等问题的限制。在 Python 中,使用多线程编程需要注意线程安全、GIL等问题。而协程是一种轻量级的并发方式,它是在用户空间中实现的,并不依赖于操作系统的调度。协程可以在同一个线程中实现并发,不需要进行上下文切换,因此执行效率非常高。协程通常使用事件循环(Event Loop)来调度协程的执行,事件循环会在协程需要等待 IO 操作或者其他协程时,暂停当前协程的执行,执行其他协程,从而实现并发执行的效果。
协程在Python中历经了多次完善:
- Python2.x 对协程的支持比较有限,通过 yield 关键字支持的生成器实现了一部分协程的功能但不完全
- 第三方库 gevent 对协程有更好的支持。
- Python3.4 中提供了 asyncio 模块。
- Python3.5 中引入了 async/await 关键字。
- Python3.6 中 asyncio 模块更加完善和稳定。
- Python3.7 中内置了 async/await 关键字。
- …
协程的应用场景:
- 网络编程:协程可以帮助我们实现高并发的网络应用。
- 异步IO:协程可以帮助我们高效地处理异步IO操作。
- 数据库操作:协程可以帮助我们实现高并发的数据库应用。
- 任务调度:协程可以帮助我们实现高效的任务调度系统。
2.gevent模块在爬虫中的应用
由于使用协程的方式有很多种,这里我使用gevent写一个爬虫demo
gevent模块简介
- Python通过yield提供了对协程的基本支持,但是不完全。而第三方的gevent为Python提供了比较完善的协程支持。
- gevent是第三方库,通过greenlet实现协程,其基本思想是: 当一个greenlet遇到IO操作时,比如访问网络,就自动切换到其他的greenlet,等到IO操作完成,再在适当的时候切换回来继续执行。由于IO操作非常耗时,经常使程序处于等待状态,有了gevent为我们自动切换协程,就保证总有greenlet在运行,而不是等待IO。
- 使用gevent,可以获得极高的并发性能,但gevent通常只能在Unix/Linux下运行,在Windows下不保证正常安装和运行。
安装和依赖
依赖于greenlet 、library,支持python 2.6+ 、python 3.3+
pip install gevent
gevent协程爬虫示例
# coding: utf-8
# 在导入其他库和模块前,先把monkey模块导入进来,并运行monkey.patch_all()。这样,才能先给程序打上补丁。
from gevent import monkey # 从gevent库里导入了monkey模块,这个模块能将程序转换成可异步的程序
monkey.patch_all() # 它的作用其实就像你的电脑有时会弹出“是否要用补丁修补漏洞或更新”一样。它能给程序打上补丁,让程序变成是异步模式,而不是同步模式。它也叫“猴子补丁”。
import gevent
import requests
import time
def get_response(url): # 定义一个函数,用来执行解析网址和爬取内容
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36'}
res = requests.get(url, headers=headers) # 发出请求
print(res.status_code)
if __name__ == '__main__':
start = time.time() # 开始时间
# 构建100个请求任务
url_list = []
for i in range(100):
url = 'https://www.baidu.com/'
url_list.append(url)
# 使用协程
tasks_list = []
for url in url_list:
# 用gevent.spawn()创建任务,此任务可以调用cra(url)函数,参数1函数名,后边为该函数需要的参数,按顺序写
task = gevent.spawn(get_response, url)
tasks_list.append(task) # 将任务加入列表
# 调用gevent库里的joinall方法,能启动执行tasks_list所有的任务。
gevent.joinall(tasks_list)
end = time.time() # 结束时间
print(end - start)
另外我们可以配合多进程+协程使用,可以更快提速。
四、多线程、多进程、协程的区别
优点 | 缺点 | |
---|---|---|
多进程 | 1.每个进程互相独立,不影响主程序的稳定性,子进程崩溃没关系; 2. 通过增加CPU,就可以容易扩充性能; 3.每个子进程都有2GB地址空间和相关资源,总体能够达到的性能上限非常大 。 | 1.逻辑控制复杂,需要和主程序交互; 2.需要跨进程边界,如果有大数据量传送,就不太好,适合小数据量传送、密集运算 多进程调度开销比较大。 |
多线程 | 1.所有线程可以共享内存和变量 2.线程的执行开销比进程小。 | 1.每个线程与主程序共用地址空间,受限于2GB地址空间; 2. 线程之间的同步和加锁控制比较麻烦; 3.一个线程的崩溃可能影响到整个程序的稳定性; |
协程(相比于线程) | 1.更加轻量级,占用的资源更少; 2.不需要进行上下文切换,执行效率更高; 3.可以使用事件循环进行调度,实现高并发的效果; 4.不会受到 GIL 的限制。 | 1.无法利用多核 CPU 2.调试困难等问题。。 |
在实际开发中,选择的并发方式从具体实际开发来进行选择。最好是采用结合的方式,来进行并发提速。