python 协程 线程,python的协程和多线程
目录1。多进程1.2 os模块fork模式实现多进程1.2多处理模块创建多进程1.3多处理模块提供一个池类来表示进程池对象1.4进程间通信1.4.1队列1.4.2管道
2.多线程2.1创建多线程2.2线程同步2.3全局解释器锁(GIL) 3。目前有四种并发编程方式(非并行):多进程、多线程、协同和异步。
多进程编程有类似python中C的os.fork,多进程标准库封装在更高的层次。python中提供多线程编程和线程异步编程。在linux下,主要有三种实现:选择、轮询和epoll协同学。yield通常在python中提到,关于协同学的库主要有Greenlet、Stackless、Gevent、Eventlet等。从多进程、多线程、协同进程、分布式进程四个方面熟悉爬虫开发中进程和线程的灵活运用。
1.多进程Python实现多进程主要有两种方式。一种方法是在os模块中使用fork方法,另一种方法是使用多处理模块。两种方法的区别在于,前者只适用于Unix/Linux操作系统,不适用于Windows,而后者是跨平台实现。现在很多爬虫程序都运行在Unix/Linux操作系统上,所以本节对这两种方法都进行了解释。
1.2 os模块fork方法实现多进程Python的os模块封装了常见的系统调用,包括fork方法。fork方法来源于Unix/Linux操作系统中提供的一个fork系统调用,很特别。普通方法调用一次返回一次,而fork方法调用一次返回两次。原因是操作系统将当前进程(父进程)复制成进程的副本(子进程)。这两个进程几乎相同,所以fork方法分别在父进程和子进程中返回。子进程总是返回0,父进程返回子进程的ID。* *下面举例说明Python使用fork方法创建流程。os模块中的getpid方法用于获取当前进程的id,getppid方法用于获取父进程的ID。
导入操作系统
if __name__==__main__ :
打印“当前进程(%s)开始.”%(os.getpid())
pid=os.fork()
如果pid为0:
打印“分叉中的错误”
elif pid==0:
打印“我是子进程(%s),我的父进程是(%s),(os.getpid(),
os.getppid())
否则:
print I(%s)创建了一个chlid进程(%s)。(os.getpid(),pid)$ python os3.py
当前进程(3113)开始.
我(%s)创建了一个chlid进程(%s)。(3113, 3114)
我是子进程(%s),我的父进程是(%s) (3114,3113)
1.2多重处理模块创建多重处理模块提供了一个进程类来描述一个进程对象。创建子流程时,只需传入一个执行函数及其参数,就可以完成流程实例的创建。用start()方法启动流程,用join()方法同步流程。以下示例使用以下代码演示了创建多进程的过程:
#!/usr/bin/python
#编码:utf-8
导入操作系统
从多重处理导入流程
#子进程要执行的代码
def run_proc(名称):
“打印”子进程%s (%s)正在运行.% (name,os.getpid())
if __name__==__main__ :
print 父进程% s . % OS . getpid()
对于范围(5)中的I:
p=Process(target=run_proc,args=(str(i),))
“打印”过程将开始
开始()
连接()
打印“流程结束”$ python os4.py
父进程3810。
流程将开始。
流程将开始。
子进程0 (3811)正在运行.
流程将开始。
流程将开始。
子进程1 (3812)正在运行.
流程将开始。
子进程2 (3813)正在运行.
子进程3 (3814)正在运行.
子进程4 (3815)正在运行.
流程结束。上面介绍了创建进程的两种方法,但是要启动大量的子进程,更常见的是使用进程池批量创建子进程,因为当被操作的对象数量较少时,可以直接使用多处理中的进程来动态生成多个进程。如果有数百或数千个目标,手动限制进程的数量就太麻烦了。这时候,进程池pool就该发挥作用了。
1.3多处理模块提供了一个池类来表示进程池对象。pool可以提供指定数量的进程供用户调用,默认的大小是CPU核心的数量。当一个新的请求提交到池中时,如果池未满,将创建一个新的进程来执行该请求;但是,如果池中的进程数达到了指定的最大值,请求将一直等待,直到有
流程结束时,将创建一个新流程来处理它。下面的示例演示了进程池的工作流。代码如下:
#编码:utf-8
从多处理导入池
导入操作系统,时间,随机
定义运行任务(名称):
“打印”任务%s (pid=%s)正在运行.% (name,os.getpid())
time.sleep(random.random() * 3)
打印任务%s结束% name
if __name__==__main__ :
打印“当前进程% s . % OS . getpid()
p=池(进程=3)
对于范围(5)中的I:
p.apply_async(run_task,args=(i,))
打印“正在等待所有子流程完成.”
p.close()
连接()
打印“所有子流程完成”$ python os5.py
当前进程4238。
等待所有子流程完成.
等待所有子流程完成.
等待所有子流程完成.
等待所有子流程完成.
等待所有子流程完成.
任务0 (pid=4239)正在运行.
任务1 (pid=4241)正在运行.
任务2 (pid=4240)正在运行.
任务2结束。
任务3 (pid=4240)正在运行.
任务3结束。
任务4 (pid=4240)正在运行.
任务1结束。
任务0结束。
任务4结束。
所有子流程完成。上面的程序首先创建了一个容量为3的进程池,并依次向进程池中添加了5个任务。从运行结果可以看出,虽然添加了五个任务,但是一开始只运行三个任务,一次最多运行三个进程。当一个任务完成后,又依次增加新的任务时,用于任务执行的进程仍然是原来的进程,可以通过进程的pid看到。
注意:当池对象调用join()方法时,它将等待所有子进程完成执行。在调用join()之前,需要调用close()。调用close()后,就无法继续添加新进程了。
1.4进程间通信如果创建了大量的进程,那么进程间通信是必不可少的。Python提供了多种进程间通信方式,如队列、管道、值数组等。本节主要解释队列和管道。队列管道和队列管道的区别在于,管道常用于两个进程之间的通信,而队列则用于实现多个进程之间的通信。
1.4.1队列首先解释一下队列通信方式。队列是一个多进程安全队列,可以用来实现多个进程之间的数据传输。有两种方法:Put和Get可以执行队列操作:
put方法用于将数据插入队列,它有两个可选参数:blocked和timeout。
如果blocked为True(默认值)并且timeout为正数,则此方法将阻止超时规范。
直到队列中有剩余空间。如果超时,一个队列。将引发完整的异常。如果
Blocked为False,但队列已满,并且队列。将立即抛出完整的异常。Get方法可以从队列中读取和删除元素。类似地,Get方法有两个可选参数:
阻塞和超时。如果blocked为真(默认值)并且timeout为正,则
如果在等待时间内没有取出任何元素,队列。将引发空异常。如果被阻止是
假的,分两种情况:如果长队有一个值可用,则立即返回该值;否则,如果队列为空,则立即抛出排队。空的异常。下面通过一个例子进行说明:在父进程中创建三个子进程,两个子进程往长队中写入数据,一个子进程从长队中读取数据。程序示例如下:
#编码:utf-8
从多重处理导入流程,队列
导入操作系统,时间,随机
# 写数据进程执行的代码:
def proc_write(q,URL):
打印(进程(%s)正在写入.% os.getpid())
对于全球资源定位器(Uniform Resource Locator)中的网址:
q.put(网址)
打印(将%s放入队列.% url)
time.sleep(random.random())
# 读数据进程执行的代码:
def proc_read(q):
打印(进程(%s)正在读取.% os.getpid())
虽然正确:
url=q.get(True)
打印(从队列中获取% s“% URL”
if __name__==__main__ :
# 父进程创建排队,并传给各个子进程:
q=队列()
proc _ writer 1=Process(target=proc _ write,args=(q,[url_1 , url_2 , url_3]))
proc _ writer 2=Process(target=proc _ write,args=(q,[url_4 , url_5 , url_6]))
proc _ reader=Process(target=proc _ read,args=(q,))
# 启动子进程proc_writer,写入:
proc_writer1.start()
proc_writer2.start()
# 启动子进程proc_reader,读取:
proc_reader.start()
# 等待过程编写器结束:
proc_writer1.join()
proc_writer2.join()
#过程_读者进程里是死循环,无法等待其结束,只能强行终止:
proc _ readertermin ate()$ python os6。巴拉圭
进程(4996)正在写入.
将url_1放入队列.
进程(4997)正在写入.
将url_4放入队列.
进程(4999)正在读取.
从队列中获取url_1 .
从队列中获取网址_4 .
将url_5放入队列.
从队列中获取网址_5 .
将url_6放入队列.
从队列中获取网址_6 .
将url_2放入队列.
从队列中获取网址_2 .
将url_3放入队列.
从队列中获取网址_3 .
管道最后介绍一下管的通信机制,管道常用来在两个进程间进行通信,两个进程分别位于管道的两端。
管方法返回(连接1、连接2)代表一个管道的两个端管道。方法有双层公寓参数,如果双层公寓参数为真(默认值),那么这个管道是全双工模式,也就是说conn1和conn2均可收发。若双层公寓为错误,连接一只负责接收消息,连接2只负责发送消息发送。和收到方法分别是发送和接收消息的方法。例如,在全双工模式下,可以调用连接1 .发送发送消息,conn1.recv接收消息。如果没有消息可接收,接收方法会一直阻塞。如果管道已经被关闭,那么收到方法会抛出埃费罗尔。
下面通过一个例子进行说明:创建两个进程,一个子进程通过管发送数据,一个子进程通过管接收数据。程序示例如下:
导入多重处理
随机导入
导入时间,操作系统
定义过程发送(管道,网址):
对于全球资源定位器(Uniform Resource Locator)中的网址:
"打印"进程(%s)发送:% s"%(操作系统。getpid(),url)
管道发送(网址)
time.sleep(random.random())
定义过程_接收(管道):
虽然正确:
"打印"进程(%s)版本:% s"%(操作系统。getpid()、pipe.recv())
time.sleep(random.random())
if __name__==__main__ :
管道=多重处理。管道()
p1=多重处理Process(target=proc_send,args=(pipe[0],[url_ str(i) for i in range(10) ])
p2=多重处理。进程(target=proc_recv,args=(pipe[1])
p1.start()
p2.start()
p1.join()
p2.join()$ python os7.py
进程(5454)发送:url_0
进程(5455)版本:url_0
进程(5454)发送:url_1
进程(5454)发送:url_2
进程(5455)版本:url_1
进程(5454)发送:url_3
进程(5454)发送:url_4
进程(5455)版本:url_2
进程(5455)版本:url_3
进程(5454)发送:url_5
进程(5454)发送:url_6
进程(5455)版本:url_4
进程(5455)版本:url_5
进程(5455)版本:url_6
进程(5454)发送:url_7
进程(5454)发送:url_8
进程(5454)发送:url_9
进程(5455)版本:url_7
进程(5455)版本:url_8
进程(5455)版本:url_9注意以上多进程程序运行结果的打印顺序在不同的系统和硬件条件下略有不同。
2.多线程多线程类似于同时执行多个不同程序,多线程运行有如下优点:
您可以将长时间运行的任务放在后台。用户界面可以更有吸引力。例如,用户点击一个按钮来触发某些事件的处理,可以弹出一个进度条来显示处理进度。可以加快程序的运行速度。在一些需要等待的任务的实现上,比如用户输入、文件读写、网络数据收发等。
程就更受用了。在这种情况下,我们可以释放一些宝贵的资源,比如内存占用。Python的标准库提供了两个模块:线程和线程化。线程是低级模块,线程是高级模块,封装线程。在大多数情况下,我们只需要使用高级模块线程。
2.1使用线程模块创建多线程模块一般通过两种方式创建多线程:第一种方式是传入一个函数并创建一个线程实例,然后调用start方法开始执行;第二种方式是直接从线程继承并创建一个线程类。线程,然后重写__init__方法和run方法。
首先介绍第一种方法,通过一个简单的例子演示创建多线程的过程。该计划如下:
#编码:utf-8
随机导入
导入时间,线程
#新线程执行的代码:
定义线程运行(URL):
打印当前%s正在运行.% threading.current _线程()。名字
对于url中的URL:
打印“% s-% s”%(threading . current _ thread()。名称、网址)
time.sleep(random.random())
打印“%s”已结束% threading.current _线程()。名字
打印“%s”正在运行.% threading.current _线程()。名字
t1=线程。Thread(target=thread_run,name=Thread_1 ,args=([url_1 , url_2 , url_3],))
t2=线程。Thread(target=thread_run,name=Thread_2 ,args=([url_4 , url_5 , url_6],))
t1.start()
t2.start()
t1.join()
t2.join()
打印“%s”已结束% threading.current _线程()。名称$ python thr1.py
主线程正在运行.
当前线程_1正在运行.
线程1-URL 1
当前线程_2正在运行.
线程2-URL 4
线程2-URL 5
线程1-URL 2
线程1-URL 3
线程2-URL 6
Thread_2结束。
Thread_1结束。
主线程已结束。第二种方法是从threading继承并创建一个thread类。Thread这里,重写方法一的程序,程序如下:
#编码:utf-8
随机导入
导入线程
导入时间
类myThread(线程。线程):
def __init__(自身,名称,URL):
threading.Thread.__init__(self,name=name)
self.urls=urls
定义运行(自身):
打印当前%s正在运行.% threading.current _线程()。名字
对于self.urls中的url:
打印“% s-% s”%(threading . current _ thread()。名称、网址)
time.sleep(random.random())
打印“%s”已结束% threading.current _线程()。名字
打印“%s”正在运行.% threading.current _线程()。名字
t1=myThread(name=Thread_1 ,urls=[url_1 , url_2 , url_3])
t2=myThread(name=Thread_2 ,urls=[url_4 , url_5 , url_6])
t1.start()
t2.start()
t1.join()
t2.join()
2.2线程同步如果多个线程联合修改某个数据,可能会出现意想不到的结果。为了保证数据的正确性,需要同步多个线程。简单的线程同步可以通过使用线程对象的Lock和RLock来实现,两者都有获取方法和释放方法。对于一次只允许一个线程操作的数据,操作可以放在获取和释放方法之间。
对于锁对象,如果一个线程连续执行两次获取操作,第二次获取将挂起该线程,因为在第一次获取之后没有释放。这将导致锁对象永远不会被释放,从而使线程死锁。RLock对象允许线程多次获取它,因为它通过计数器变量维护线程获取的次数。每个获取操作都必须有一个与之对应的释放操作。所有释放操作完成后,其他线程可以申请RLock对象。下面是一个简单的例子来演示线程同步的过程:
导入线程
mylock=线程。RLock()
数量=0
类myThread(线程。线程):
def __init__(self,name):
threading.Thread.__init__(self,name=name)
定义运行(自身):
全局编号
虽然正确:
mylock.acquire()
打印“%s”已锁定,编号:% d“%(threading . current _ thread()。名称、编号)
如果num=4:
mylock.release()
打印“%s”已发布,编号:% d“%(threading . current _ thread()。名称、编号)
破裂
数量=1
打印“%s”已发布,编号:% d“%(threading . current _ thread()。名称、编号)
mylock.release()
if __name__==__main__ :
thread1=myThread(Thread_1 )
thread2=myThread(Thread_2 )
thread1.start()
thread2.start()$ python th3.py
Thread_1已锁定,数量:0
线程_1已释放,编号:1
线程_1已锁定,数量:1
线程_1已释放,数量:2
线程1已锁定,数量:2
线程_1已释放,编号:3
线程1已锁定,数量:3
线程_1已释放,数量:4
线程1已锁定,数量:4
线程_1已释放,数量:4
线程2已锁定,数量:4
线程_2已释放,数量:4
2.3全局解释器锁(GIL)Python的原解释器CPython中有一个GIL(全局解释器锁)。因此,在解释和执行Python代码时,会产生一个互斥锁来限制线程对共享资源的访问,直到解释器遇到I/O操作或者操作次数达到一定数量,才会释放GIL。因为全局解释器锁的存在,不能调用多个CPU核心,只能使用一个核心。所以CPU密集型操作不建议多线程,多进程优先。那么多线程适合什么样的应用场景呢?对于IO密集型的操作,多线程可以明显提高效率,比如Python爬虫的开发。爬虫大部分时间都在等待socket返回数据,网络IO的操作延迟远大于CPU。
3.协程,又称微线程、纤程,是一种用户级的轻量级线程。协同进程有自己的寄存器上下文和堆栈。协调调度切换时,寄存器上下文和堆栈保存到其他地方,切换回来时,恢复之前保存的寄存器上下文和堆栈。所以协进程可以保持上次调用的状态,每次进程重新进入,就相当于进入了上次调用的状态。
在并发编程中,协程类似于线程。每个协程代表一个执行单元,它有自己的本地数据,并与其他协程共享全局数据和其他资源。教练需要用户编写自己的调度逻辑。对于CPU来说,它其实是一个单线程,所以CPU不用考虑如何调度和切换上下文,节省了CPU切换开销,所以一定程度上比多线程要好。那么如何在Python中实现协同学呢?
Python通过yield为协同学提供了基本的支持,但并不完整。使用第三方gevent库是更好的选择,gevent提供了相对完整的协同学支持。Gevent是基于协同学的Python网络函数库。greenlet用于在libev事件循环的顶部提供具有高级并发性的API。主要特点如下:
基于libev的快速事件循环,这是Linux上的epoll机制。基于greenlet的轻量级执行单元。API重用Python标准库的内容。支持SSL的协作套接字。DNS查询可以通过线程池或者c-ares实现。有了猴子打补丁功能,第三方模块就变得协同了。Gevent对协同过程的支持本质上是greenlet的切换工作。greenlet的工作流程如下:如果访问网络的IO操作出现阻塞,greenlet会显式切换到另一个没有阻塞的代码段执行,在原来的阻塞条件消失后,再自动切换回原来的代码段继续处理。因此,greenlet是一种排列合理的串行模式。
由于IO操作非常耗时,它经常使程序处于等待状态。有了gevent自动为我们切换协程,就保证了总有一个greenlet在运行,而不是等待IO,这也是协程一般比多线程更高效的原因。由于切换是在IO操作时自动完成的,gevent需要修改Python自带的一些标准库,实现一些常用块的协同跳转,比如socket、select等。这个过程是在启动时通过monkey补丁完成的。下面的例子演示了gevent的使用过程,代码如下:
来自
郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。