多进程和多线程
多任务
多任务就是同一时间运行多个程序,比如你可能一变听着歌,一变摸着鱼、一边还看着博客。。。。。。这就是生活中的场景。
单核CPU实现多任务原理:操作系统轮流让各个人物交替执行,QQ执行2us,切换到微信,再执行2us,再切换到浏览器,再执行2us……。表面上看,每个人物反复执行下去,但是CPU调度执行速度太快了,导致我们感觉运行所有任务都在同时执行一样。
多核CPU实现多任务的原理:真正的是执行多任务只能在多核CPU上实现,但是由于任务数量远远多于CPU核心数量,所以,操作系统也会自动把很多任务轮流调度到每个核心上执行。
并发和并行
-
并发:
当多个线程在操作时,如果系统只有一个CPU,则它根本不可能真正同时进行一个以上的线程,它只能把CPU运行时间划分为若干个时间段,再将时间段分配给各个线程执行,在一个时间段的线程代码运行时,其它线程处于挂起状,这种方式我们称之为并发。
-
并行:
当系统有一个以上CPU时,则线程的操作有可能非并发。当一个CPU执行一个线程时,另一个CPU可以执行另一个线程,两个线程互不抢占CPU资源,可以同时进行,这种方式我们称之为并行。
实现多任务的方式:
- 多进程模式;
- 多线程模式;
- 协程
多进程
进程是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算集结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程的程序的实体。
对于操作系统来说,一个任务就是一个进程。比如打开QQ就是启动了一个进程,在打开一个记事本启动一个记事本进程。如果打开两个记事本就是打开两个记事本进程。
优点:
- 稳定性高,一个进程崩溃了,不会影响其他进程。
缺点:
- 创建进程的开销巨大
- 操作系统能同时运行的进程数目有限
创建进程
fork()
Linux和Unix操作系统提供了一个fork()函数创建新的进程,这也就意味着该函数仅仅适用于Linux和Unix平台。
fork()函数比较特殊,python的os.fork()是唯一调用一次返回两次的函数,因操作系统将当前的进程(父进程)复制了一份新的进程(子进程),然后分别在父进程和子进程内返回。
fork()本质上是内建函数,通过os模块导入。
子进程永远返回0,父进程返回子进程的PID。每个子进程的创建都会在父进程中留下标记PID(子进程身份信息),当子进程溯源其父进程的时候,通过getppid()就可以找到父进程的PID。
语法:
功能:为当前进程创建一个子进程
参数:无
返回值:0和子进程PID(在父进程中)
< 0 子进程创建失败
= 0 在子进程中的返回值
> 0 在父进程中的返回值
特点:
-
子进程会继承父进程全部代码段(包括fork()前所定义的所有内容)
-
子进程拥有自己独立的信息标识,如PID
-
父、子进程独立存在,在各自存储空间上运行,互不影响
-
创建父子进程执行不同的内容是多任务中固定方法。
import os
pid = os.fork()
if pid < 0:
print("建立进程失败")
elif pid == 0:
print("这是子进程")
else:
print("这是父进程")
windows环境下创建进程
引入multiprocessing包中的Process
import os
import time
from multiprocessing import Process
def task1():
while True:
time.sleep(1)
# getpid()获取进程号,getppid()获取父进程号
print("这是任务1==============", os.getpid(), '***', os.getppid())
def task2():
while True:
time.sleep(2)
print("这是任务2==============", os.getpid(), '***', os.getppid())
if __name__ == '__main__':
# 父进程是执行代码的时候建立的进程。
# 子进程
p1 = Process(target=task1, name='任务1')
p2 = Process(target=task2, name='任务2')
print(p1.name)
print(p2.name)
p1.start()
p2.start()
代码改进:
import os
import time
from multiprocessing import Process
def task1(s):
while True:
time.sleep(s)
# getpid()获取进程号,getppid()获取父进程号
print("这是任务1==============", os.getpid(), '***', os.getppid())
def task2(s):
while True:
time.sleep(s)
print("这是任务2==============", os.getpid(), '***', os.getppid())
if __name__ == '__main__':
# 父进程是执行代码的时候建立的进程。
# 创建两个子进程
# 向task中传参数使用args和kwargs
p1 = Process(target=task1, name='任务1', kwargs={'s':1})
p2 = Process(target=task2, name='任务2', args=(2,))
print(p1.name)
print(p2.name)
p1.start()
p2.start()
小结:
from multiprocessing import Process process = Process(target=函数名,name=进程的名字, args=(给函数传递的参数,如果只有一个后面要加逗号), kwargs={字典形式传参}) # 这就是Process对象 # 对象调用方法 process.start() # 启动进程并执行任务。 process.run() # 执行任务,但是没有启动进程。 process.terminate() # 终止
进程对全局变量的访问
import time
from multiprocessing import Process
# 定义一个全局变量
n = 1
def task1(s):
while True:
time.sleep(s)
global n
n += 1
# getpid()获取进程号,getppid()获取父进程号
print("这是任务1==============", n)
def task2(s):
while True:
time.sleep(s)
global n
n += 1
print("这是任务2==============", n)
if __name__ == '__main__':
n = 1
# 父进程是执行代码的时候建立的进程。
# 创建两个子进程
# 向task中传参数使用args和kwargs
p1 = Process(target=task1, name='任务1', kwargs={'s': 1})
p2 = Process(target=task2, name='任务2', args=(2,))
print(p1.name)
print(p2.name)
p1.start()
p2.start()
每个进程操作一份全局变量。
这个变量与可变不可变无关,定义一个全局的列表,每个任务都往里面添加数据,最后出现的结果和上面的执行结果类似,一个进程执行一份。
自定义进程
'''自定义进程'''
# 进程:自定义
from multiprocessing import Process
from time import sleep
# 继承Process类
class MyProcess(Process):
def __init__(self, name):
super(MyProcess, self).__init__()
self.name = name
# 重写run方法
def run(self, ):
n = 1
while True:
print('{}--->自定义进程:{}'.format(n, self.name))
n += 1
if __name__ == '__main__':
p = MyProcess('小明')
p.start()
sleep(1)
p.terminate()
进程池
当需要创建的子进程数量不多时,可以直接利用multiprocessing中的Process动态生成多个进程。但是如果是上百甚至上千目标,手动的区创建进程的工作量巨大,此时可以用到multiprocessing模块提供的Pool方法。初始化Pool时,可以指定一个最大进程数,当有新的请求提交道Pool中时,如果池还没有满,那么就会创建一个新的进程用来执行改请求:如果池中的进程数已经达到指定的最大值,那么该请求就会等待,知道池中有进程结束,才会创建新的进程来执行。
Pool中有阻塞式和非阻塞式。
阻塞式:添加一个执行一个,执行完后返回。然后添加下一个
非阻塞式: 直接全部添加到池子里,立刻返回(这个返回并不是函数返回)去添加下一个,并没有等待其他的进程完毕。但是回调函数是等待任务完成之后才调用(一个完了回调一个)。
非阻塞式进程
异步执行函数apply_async()
def apply_async(self, func, args=(), kwds={}, callback=None,
error_callback=None):
'''
Asynchronous version of `apply()` method.
'''
self._check_running()
result = ApplyResult(self, callback, error_callback)
self._taskqueue.put(([(result._job, 0, func, args, kwds)], None))
return result
进程池使用
import os
import time
from multiprocessing import Pool
# 非阻塞式
from random import random
def task1(task_name):
print("开始做任务了", task_name)
start = time.time()
time.sleep(random() * 2)
end = time.time()
print('完成{}用时:'.format(task_name), (end - start), '进程id:', os.getpid())
if __name__ == '__main__':
pool = Pool(5)
tasks = ['任务1', '任务2', '任务3', '任务4', '任务5', '任务6', '任务7']
for task in tasks:
# 使用非阻塞式池子
pool.apply_async(task1, args=(task,))
pool.close() # 添加任务结束
pool.join() #
print('over')
进程池大小为5,完成一个出一个任务。然后后备任务进入池子。直到没有进入。
仔细观察进程id可以发现,让出来的进程id会在下一次使用。
使用回调函数
import os
import time
from multiprocessing import Pool
# 非阻塞式
from random import random
def task1(task_name):
print("开始做任务了", task_name)
start = time.time()
time.sleep(random() * 2)
end = time.time()
# print('完成{}用时:'.format(task_name), (end - start), '进程id:', os.getpid())
return '完成{}用时{} --进程id{}'.format(task_name, (end - start), os.getpid())
container = []
def callback_func(string):
container.append(string)
# print(string)
if __name__ == '__main__':
pool = Pool(5)
tasks = ['任务1', '任务2', '任务3', '任务4', '任务5', '任务6', '任务7']
for task in tasks:
# 使用非阻塞式池子
pool.apply_async(task1, args=(task,), callback=callback_func)
pool.close() # 添加任务结束
pool.join() # 相当于一堵墙,执行完之后才执行后面的。
for c in container:
print(c)
print("over")
在apply_asuync()函数中的第一个传入的是任务函数,callback是回调函数,是每次任务函数执行完成后的返回值给回调函数处理。
进程池的好处:
可以重复利用进程(进程复用)。并且管理进程吞吐量。
非阻塞式代码注意:如果没有pool.close()、 pool.join()代码执行以下就结束了。非阻塞式进程同生共死,当执行语句结束,进程池中进程也不执行了。
阻塞式
特点:添加一个执行一个。如果一个任务不结束另一个任务进不来。
# 阻塞式
import os
import time
from multiprocessing import Pool
# 非阻塞式
from random import random
def task1(task_name):
print("开始做任务了", task_name)
start = time.time()
time.sleep(random() * 2)
end = time.time()
# return '完成{}用时{} --进程id{}'.format(task_name, (end - start), os.getpid())
print('完成{}用时{} --进程id{}'.format(task_name, (end - start), os.getpid()))
if __name__ == '__main__':
pool = Pool(5)
tasks = ['任务1', '任务2', '任务3', '任务4', '任务5', '任务6', '任务7']
for task in tasks:
pool.apply(task1, args=(task,))
pool.close()
pool.join() # 也可以理解为插队
print("over")
guan
观察进程id发现使用完之后的进程会复用。
小结
进程池:
创建进程池对象pool = Pool(max)
阻塞的:pool.apply()
非阻塞的:apply_async()
停止添加进程:pool.close()
让主进程让步:pool.join()
进程间通信
- 使用队列,队列的特点(FIFO)
from multiprocessing import Queue
q = Queue(5)
q.put('A')
q.put('B')
q.put('C')
q.put('D')
q.put('E')
print(q.qsize())
if not q.full(): # 判断队列是否满 q.empty() 判断队列是否是空的
q.put('F', timeout=3) # put() 如果queue满了则只能等待,除非有‘空地’则添加成功
else:
print('队列已满!')
# 获取队列的值
print(q.get(timeout=2))
print(q.get(timeout=2))
print(q.get(timeout=2))
print(q.get(timeout=2))
print(q.get(timeout=2))
# print(q.get(timeout=2))
补充:put与get方法是两个阻塞方法:put不到值程序夯住,get不到程序也夯住。
put_nowait与get_nowait方法是两个非阻塞方法:put_nowait没有值的话不等,get_nowait取不到值也不等了,程序不会夯住,但是一定要做异常处理!
进程通信
# 进程间通信
from multiprocessing import Process, Queue
from time import sleep
def download(q):
images = ['pic1.jpg', 'pic2.jpg', 'pic3.jpg']
for image in images:
print('正在下载:', image)
sleep(0.5)
q.put(image)
def getfile(q):
while True:
try:
file = q.get(timeout=5)
print('{}保存成功!'.format(file))
except:
print('全部保存完毕!')
break
if __name__ == '__main__':
q = Queue(2)
# 一共两个进程就不创建进程池了。
# 两个进程:一个下载进程、一个检测下载。
p1 = Process(target=download, args=(q,))
p2 = Process(target=getfile, args=(q,))
# p2.start()
p1.start()
# p1.join() # 如果不加join的话就是
p2.start() # p2进程先启动的效果和p1先启动的效果
p2.join()
print('===========')
多线程
线程,优势被称为轻量级进程,是程序执行流的最小单元。一个标准的线程由线程id、当前指令指针、寄存器集合和堆栈组成。另外线程是进程的一个实体,是被系统独立调度和分配的基本单位,线程自己不拥有资源,只拥有一点在运行中必不可少的资源,但是它可与同属一个进程的其它线程共享进程所拥有的全部资源。一个线程可以创建和撤销另一个线程,同一进程中的多个线程之间可以并发执行。就绪状态是指线程具备运行的所有条件,逻辑上可以运行,在等待处理机;运行状态是指线程占有处理机正在运行;阻塞状态是指线程在等待一个事件(如某个信号量),逻辑上不可执行。每个程序都至少有一个线程,若程序只有一个线程,那就是程序本身。
线程是程序中一个单一的顺序控制流程。进程内有一个相对独立的、可调度的执行单元,是系统独立调度和分派CPU的基本单位指令运行时的程序的调度单元。在某个程序中同时运行多个线程完成不同的工作,称为多线程。
多线程:是指从软件或者硬件上实现多个线程并发执行的技术。具有多线程能力的计算机因有硬件支持而能够在同一时间执行多于一个线程,进而提升整体处理性能。具有这种能力的系统包括对称多处理机、多核心处理器以及芯片级多处理或者同时多线程处理器。在一个程序中,这些独立运行的程序片段叫做“线程”(Thread),利用它编程的概念叫做“多线程处理”。具有多线程能力的计算机因有硬件支持而能够在同一时间执行多于一个线程,进而提升整体处理性能。
优点:
- 使用线程可以把占据长时间的程序中的任务放在后台去处理。
- 用户界面可以更加吸引人,比如用户点击了一个按钮去触发某些事件的处理,可以弹出一个进度条来显示处理的进度。
- 程序的运行速度可能加快
- 在一些等待的任务实现上如果输入、文件读写和网络收发数据等,线程就比较有用了。这种情况下我们可以释放一些珍贵的资源如内存占用等等。
Python通过两个标准库thread和threading提供对线程的支持。thread提供了低级别的、原始的线程以及一个简单的锁。
threading模块提供了其他方法
- threading.currentThread():返回当前的线程变量
- threading.enumerate():返回一个包含正在运行的线程的list。正在运行指线程启动后、结束前,不包括启动
- threading:activeCount():返回正在运行的线程数量,与len(threading.enumerate())有相同的功能。
线程状态:
- 新建状态:新建了一个线程对象
- 就绪状态:线程对象创建后,其他线程调用了该对象的start方法。该状态的线程位于“可运行线程池”中,变得可运行,只是在等待获取cpu的使用权。即在就绪状态的进程除cpu之外,其它的运行所需资源都已全部获得。
- 运行状态:就绪状态的线程获取了CPU,执行程序代码。
- 阻塞状态:阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。线程进入就绪状态,才有机会转到运行状态。
- 死亡状态:线程执行完了或者因异常退出了run()方法,进程结束生命周期。
import threading
from time import sleep
def download(n):
images = ['pic1.jpg', 'pic2.jpg', 'pic3.jpg']
for image in images:
print('正在下载:', image)
sleep(n)
print('下载{}成功!'.format(image))
def listen():
musics = ['music1', 'music2', 'music3', 'music4']
for music in musics:
sleep(0.5)
print('正在听{}歌!'.format(music))
if __name__ == '__main__':
# 线程对象
t1 = threading.Thread(target=download, name='aa', args=(2,))
t1.start()
t2 = threading.Thread(target=listen, name='aa')
t2.start()
# 这部分相当于主线程运行。
# n = 1
# while True:
# print(n)
# sleep(1.5)
# n += 1
在前面的进程访问全局变量的时候,每个进程都有自己的一份变量进行操作,不共享资源。那多个线程呢?
import threading
from time import sleep
ticket = 1000
def run1():
global ticket
for i in range(100):
sleep(0.1)
ticket -= 1
# def run2():
# global ticket
# for i in range(100):
# ticket -= 1
if __name__ == "__main__":
# 创建线程
th1 = threading.Thread(target=run1, name="th1")
th2 = threading.Thread(target=run1, name="th2")
th3 = threading.Thread(target=run1, name="th3")
th4 = threading.Thread(target=run1, name="th4")
# 启动
th1.start()
th2.start()
th3.start()
th4.start()
th1.join()
th2.join()
th3.join()
th4.join()
print('money:', ticket)
从运行结果可以看出,线程对全局变量是共享的。
这是数比较小的情况,如果数字特别大呢?(一个线程执行的时间很长)
'''
多线程
GIL 全局解释器锁
Python没有实现多线程的要求
'''
import threading
n = 0
# 任务1
def task1():
global n
for i in range(1000000):
n += 1
print('---->task1中的n值是:', n)
# 任务2
def task2():
global n
for i in range(1000000):
n += 1
print('---->task2中的n值是:', n)
if __name__ == '__main__':
th1 = threading.Thread(target=task1)
th2 = threading.Thread(target=task2)
th1.start()
th2.start()
th1.join()
th2.join()
print("最后打印n:", n)
声明:每次执行的结果还不一样,但是都没有到达2000000。
这是为什么?
在底层执行n+=1时执行的是n = n + 1
可能在你还没完成这一步计算的时候,就被抢走了。所以加法没有执行完成就被迫赋值。再一个实例:在买票的时候,多个线程进入数据空间的时候可能读取到的数据都是还没有被操作的数据比如还有一张票,然后三个线程读取到的都是这个信息,然后用户都买了,但是此时数据空间中没有3张你给用户的体验是买了三张这时数据空间的数据就被污染了。这时发生的事情就是数据不安全。
为了解决这个问题我们可以给线程上锁(lock),当一个线程在执行的时候其他的线程无法操作。这样就保证了数据的安全性。但是缺点是时间会变慢。
Python底层只要用到线程默认加锁。但是为什么数据过大之后还是出现了错误呢?这是因为如果运算律达到一定的速度锁就被自动释放了。
这时我们就可以总结什么时候用进程什么时候用线程:
线程:耗时操作(文件下载、IO)
进程:计算密集型(计算量比较大)
继续上面的问题,我们可不可以手动加一把锁来保证安全性呢?
共享数据
如果多线程共同对某个数据修改,则可能出现不可预料的结果,为了保证数据的正确性,需要对多个线程进行同步。
同步:一个一个的完成,一个做完另一个才能进来
但是效率会降低。
使用Thread对象的lock和Rlock可以实现简单的线程同步,这两个对象都有acquire和release方法,对于那些需要每次只只允许一个线程操作的数据,可以将其操作放在acquire和release方法之间。
多线程的优势在于可以同时运行多个任务(至少感觉起来是这样)。但是当线程需要共享数据时,可能存在数据的不同步的问题。为了避免这种情况,引入了锁的概念。
import threading
import random
import time
lock = threading.Lock()
list1 = [0] * 10 # 10个0
def task1():
# 申请使用线程锁,如果已经上锁了,则等待锁的释放。
lock.acquire() # 阻塞
for i in range(len(list1)):
list1[i] = random.randint(0, 10)
time.sleep(0.5)
lock.release() # 释放锁
def task2():
lock.acquire()
for i in range(len(list1)):
print('-->', list1[i])
time.sleep(0.5)
lock.release()
if __name__ == '__main__':
# 创建线程
t1 = threading.Thread(target=task1, name='task1')
t2 = threading.Thread(target=task2, name='task2')
# t2.start()
t1.start()
t2.start()
自行测试的时候可以将线程1和2的start顺序调换进行观察情况。
死锁
开发过程中使用线程,在线程间共享多个资源的时候,如果两个线程分别占有一部分资源并且同时等待对方的资源,就会造成死锁。尽管死锁很少发生,但是一旦发生就会造成应用停止响应,程序不做任何事情。
from threading import Thread, Lock
import time
# 准备两把锁
lockA = Lock()
lockB = Lock()
class MyThread1(Thread):
def run(self):
if lockA.acquire(): # 如果可以获取
print(self.name + '获取了A锁')
time.sleep(0.1)
if lockB.acquire():
print(self.name + '又获取了B锁')
lockB.release()
lockA.release()
class MyThread2(Thread):
def run(self):
if lockB.acquire(): # 如果可以获取
print(self.name + '获取了B锁')
time.sleep(0.1)
if lockA.acquire():
print(self.name + '又获取了A锁')
lockB.release()
lockA.release()
if __name__ == '__main__':
t1 = MyThread1()
t2 = MyThread2()
t1.start()
t2.start()
注意左边的红点,无法完成结束。为了理解怎么产生死锁(资源分配不当)
避免死锁:
解决:1. 重构代码(代价较大) 2. 在acquire中加参数timeout。
线程典型应用–生产者消费者
线程之间的通讯也是通过队列。
生产者消费者:两个线程之间的通信
Python的queue模块中提供了同步的、线程安全的队列类,包括FIFO队列Queue,LIFO队列LifoQueue,和优先级队列PriorityQueue。这些队列都实现了锁原理(可以理解为原子操作,即要么不做,要么就做完),能够在多线程中直接使用。
可以使用队列来实现线程间的同步。
import threading
import queue
import random
import time
def produce(q):
'''生产者'''
i = 0
while i < 10:
num = random.randint(1, 100)
q.put('生产者产生数据:%d' % num)
print('生产者产生数据:%d' % num)
time.sleep(1)
i += 1
q.put(None)
# 完成任务
q.task_done()
def consume(q):
while True:
item = q.get()
if item is None:
break
print("消费者获取到:%s" % item)
time.sleep(4)
# 完成任务
q.task_done()
if __name__ == '__main__':
q = queue.Queue(10)
arr = []
# 创建生产者
th = threading.Thread(target=produce, args=(q,))
th.start()
# 创建消费者
tc = threading.Thread(target=consume, args=(q,))
tc.start()
th.join()
tc.join()
print("-----end-----")
可以自行修改参数进行理解。
协程
协程:微线程
实现方式:生成器完成
使用:
- 耗时操作:网络请求、网络下载(爬虫)、网络上传、I/O操作(文件读写),阻塞
- 只要出现堵塞立马进行切换。
import time
def task1():
for i in range(3):
print('A' + str(i))
yield
time.sleep(1)
def task2():
for i in range(3):
print('B' + str(i))
yield
time.sleep(2)
if __name__ == '__main__':
# 两个生成器
g1 = task1()
g2 = task2()
while True:
try:
next(g1)
next(g2)
except:
break
使用greenlet完成协程任务
# greenlet 完成协程任务
# greenlet封装了yield
import time
from greenlet import greenlet
def a(): # 任务A
for i in range(5):
print('A' + str(i))
# a任务切换b
gb.switch()
time.sleep(0.1)
def b(): # 任务B
for i in range(5):
print('B' + str(i))
# b任务切换c
gc.switch()
time.sleep(0.1)
def c(): # 任务C
for i in range(5):
print('C' + str(i))
# c任务切换a
ga.switch()
time.sleep(0.1)
if __name__ == '__main__':
# 创建爱你greenlet要将任务传进去,传的任务就是run函数
ga = greenlet(a)
gb = greenlet(b)
gc = greenlet(c)
# 切换执行greenlet
ga.switch()
观察任务内容可以加深理解,“协程是程序员调度的”这句话。
gevent实现自动切换任务
参考链接:greenlet和gevent
greenlet已经实现了协程,但是这个工人切换,是不是觉得太麻烦了,不要着急,python还有一个比greenlet更强大的并且能够自动切换任务的模块gevent
其原理是当一个greentlet遇到IO(指的是input ouput输入输出,比如网络、文件操作等)操作时,比如访问网络,就自动切换到其他的greenlet,等到IO完成,再适当的时候切换回来继续执行。
由于IO操作非常耗时,经常使程序处于等待状态,有了gevent我们自动切换协程,就保证总有greenlet在运行,而不是等待IO。
import time
import gevent
from gevent import monkey
# 如果没有猴子补丁,进程之间不进行自动切换
monkey.patch_all()
def a(): # 任务A
for i in range(5):
print('A' + str(i))
time.sleep(0.1)
def b(): # 任务B
for i in range(5):
print('B' + str(i))
time.sleep(0.1)
def c(): # 任务C
for i in range(5):
print('C' + str(i))
time.sleep(0.1)
if __name__ == '__main__':
# spawn是类方法
g1 = gevent.spawn(a)
g2 = gevent.spawn(b)
g3 = gevent.spawn(c)
# 阻塞主进程不结束。
g1.join()
g2.join()
g3.join()
print('---------------')
在使用gevent的时候一般都会使用猴子补丁monkey.patch_all()来实现协程间的自动切换。
使用案例–多线程爬虫
# 案例
import gevent
from gevent import monkey
import urllib.request
# 使用猴子补丁
monkey.patch_all()
def download(url):
response = urllib.request.urlopen(url)
content = response.read()
print('下载了{}的数据,长度:{}'.format(url, len(content)))
if __name__ == '__main__':
urls = ['http://www.163.com', 'http://www.qq.com', 'http://www.baidu.com']
g1 = gevent.spawn(download, urls[0])
g2 = gevent.spawn(download, urls[1])
g3 = gevent.spawn(download, urls[2])
g1.join()
g2.join()
g3.join()
# gevent.joinall(g1, g2, g3) # 更加省事
- 将爬虫代码封装
- 建立线程
- 使用猴子补丁自动切换协程。