多线程与多进程

你好,我是悦创。今天,我给大家讲讲多进程与多线程。

  1. 全局解释器锁
  2. 多线程测试
  3. 避免 GIL
  4. 多线程与多进程
  5. Lock 锁
  6. 递归锁 RLOCK
  7. 多进程
  8. 进程通信
  9. 进程池与线程池
  10. 作业

你好,我是悦创。今天,我给大家讲讲多进程与多线程。我的公众号:AI 悦创,博客地址:https://www.aiyc.top/

1. 全局解释器锁

全局解释器锁 (英语:Global Interpreter Lock,缩写 GIL

计算机程序设计语言解释器 用于 同步线程 的一种机制,它使得任何时刻仅有 一个线程 在执行,即便在 多核心处理器 上,使用 GIL 的解释器也只允许同一时间执行一个线程。常见的使用 GIL 的解释器有 CPythonRuby MRI

如果,你对上面的不理解,也没有问题。通俗的解释就是:你电脑是 一核或者多核 ,还是你得代码写了了多个线程,但因为 GIL 锁的存在你也就只能运行一个线程,无法同时运行多个线程。

接下来,我们来用个图片来解释一下:


比如图中,假如你开了两个线程(Py thread1Py tread2),

  1. 当我们线程一(Py thread1)开始执行时,这个线程会去我们的解释器中申请到一个锁。也就是我们的 GIL 锁;
  2. 然后,解释器接收到一个请求的时候呢,它就会到我们的 OS 里面,申请我们的系统线程;
  3. 系统统一你的线程执行的时候,就会在你的 CPU 上面执行。(假设你现在是四核 CPU);
  4. 而我们的另一个线程二(py thread2)也在同步运行。
  5. 而线程二在向这个解释器申请 GIL 的时候线程二会卡在这里(Python 解释器),因为它的 GIL 锁已经被线程一给拿走了(也就是说:他要进去执行,必须拿到这把锁);
  6. 线程二要运行的话,就必须等我们的线程一运行完成之后(也就是把我们的 GIL 释放之后(图片中的第 5 步)线程二才能拿到这把锁);
  7. 当线程二拿到这把锁之后就和线程一的运行过程一样。
① Create > ② GIL > ③ 申请原生线程(OS) > ④ CPU 执行(如果有其他线程,都会卡在 Python 解释器的外边)

这个锁其实是 Python 之父想一劳永逸解决线程的安全问题(也就是禁止多线程同时运行)

2. 多线程测试

为了更加直观,我这里使用把每种线程代码单独写出来并做对比:

单线程裸奔:(这也是一个主线程(main thread))

import timedef start():    for i in range(1000000):        i += i    return# 不使用任何线程(裸着来)def main():    start_time = time.time()    for i in range(10):        start()    print(time.time()-start_time)if __name__ == '__main__':    main()

输出:

6.553307056427002

注意:因为每台电脑的性能不一样,所运行的结果也相对不同(请按实际情况分析)


接下来我们写一个多线程

我们先创建个字典 (threadnametime) 来存储我们每个线程的名称与对应的时间

import threading,timedef start():    for i in range(1000000):        i += i    return# # 不使用任何线程(裸着来)# def main():#     start_time = time.time()#     for i in range(10):#         start()#     print(time.time()-start_time)# if __name__ == '__main__':#     main()def main():    start_time = time.time()    thread_name_time = {}# 我们先创建个字典 (thread_name_time) 用来来存储我们每个线程的名称与对应的时间    for i in range(10):        # 也就是说,每个线程顺序执行        thread = threading.Thread(target=start)# target=写你要多线程运行的函数,不需要加括号        thread.start()# 上一行开启了线程,这一行是开始运行(也就是开启个 run)        thread_name_time[i] = thread # 添加数据到我们的字典当中,这里为什么要用 i 做 key?这是因为这样方便我们 join    for i in range(10):        thread_name_time[i].join()    #   join() 等待线程执行完毕(也就是说卡在这里,这个线程执行完才会执行下一步)    print(time.time()-start_time)if __name__ == '__main__':    main()

输出

6.2037984102630615
# 6.553307056427002 裸奔# 6.2037984102630615 单线程顺序执行# 6.429047107696533 线程并发

我们可以看到,速度上的区别不大。

多线程并发不如单线程顺序执行快

这是得不偿失的

造成这种情况的原因就是 GIL

这里是计算密集型,所以不适用

在我们执行加减乘除或者图像处理的时候,都是在从 CPU 上面执行才可以。Python 因为 GIL 存在,同一时期肯定只有一个线程在执行,这样这样就是造成我们开是个线程和一个线程没有太大区别的原因。

而我们的网络爬虫大多时候是属于 IO 密集与计算机密集

2.1 IO 密集与计算机密集 [I:Input O:Output]

在这里插入图片描述


BIOS:B:Base、I:Input、O:Output、S:System

也就是你电脑一开机的时候就会启动。

1. 计算密集型

在上面的时候,我们开启了两个线程,如果这两个线程要同时执行,那同一时期 CPU 上只有一个线程在执行。

那从上图可知,那这两个线程就需要频繁的在上下文切换。

Ps:我们这个绿色表示我们这个线程正在执行,红色代表阻塞。

所以,我们可以明显的观察到,线程的上下文切换也是需要消耗资源的(时间-ms)不断的归还和拿取 GIL 等,切换上下文。明显造成很大的资源浪费。

2. IO 密集型

我们现在假设,有个服务器程序(Socket)也就是我们新开的一个程序(也就是我们网络爬虫的最底层)开始爬取目标网页了,我们那个网页呢,有两个线程同时运行,我们线程二已经请求成功开始运行了,也就是上图的 (Thread 2)绿色一条路过去。

而我们的线程一(Thread 1)- Datagram(这里它开启了一个 UDP),然后等待数据建立(也就是等待哪些 HTML、CSS 等数据返回)也就是说,在 Ready to receive(recvfrom)之间都是准备阶段。这样就是有一段时间一直阻塞,而我们的线程二可以一直无停歇也不用切换上下文就一直在运行。这样的 IO 密集型就有很大的好处

IO 密集型,这样就把我们等待的时间计算进去了,节省了大部分时间。

这里我们需要注意的是,我们的多线程是运行在 IO 密集型上的,我们得区分清楚。

还有就是,资源等待,比如有时候我们使用浏览器发起了一个 Get 请求,那浏览器图标上面在转圈圈的时候就是我们请求资源等待的时间,(也就是图上面的 Datagram 到 Ready to receive )数据建立到数据接收(就是转圈圈的时间)。我们完全就不需要执行它,就让它等待就好。这个时候让另一个线程去执行就好

换言之就是:第一个线程,我们爬取那个网页转圈圈的时候让另一个线程继续爬取。这样就避免了资源浪费。(把时间都利用起来)

注意: 请求资源是不需要 CPU 进行计算的,CPU 参与是很少的,而我们第一个例子,计算数字的 for 循环中,是需要 CPU 进行计算的。


3. 避免 GIL

1571888520939

前面开头已经提到:因为 GIL 的存在,所以不管我们开了多少线程,同一时间始终只有一个线程在执行。那我们该如何避免 GIL 呢?

那这样的话,我们不开线程不就行,(它的的存在已经无法避免,那我们选择不使用它不就相当于不存在嘛)。那这是,你会想:那不开线程我们开啥呢?

问的好!

我们来开:进程,那怎么说?别急!请听我细细道来。

比方你有 3 个 CPU(当然,你可能有更多,这里就按 3CPU来为例子),那我们就开 3 个进程就好。一个 CPU 上运行就好。

Ps:我们的进程是可以同时运行的。

我们可以看一下下面的图片:

任务管理器

1571966626435

我们 任务管理 上的每一项都是一个进程。

多进程比多线程不好的地方是什么呢?

多进程的创建和销毁开销也会更大,成本高。

你可能线程可以开许多的线程,但你的进程就是看你的 CPU 数量。

进程间无法看到对方数据,需要使用栈或者队列进行获取。

每个进程之间都是独立的。

就好像我们上面的谷歌浏览器和我们的 Pycharm 是没有任何关系的,谷歌浏览器上面的数据肯定不可能让 Pycharm 看到。这就是我们所说的进程之间的独立性。

如果你想要一个进行抓取数据,一个进行调用数据,那这时是不能直接调用的,需要你自己定义个结构才能使用。>>> 编程复杂度提升。


4. 多线程与多进程

前面的基础讲完了,接下来我们继续来正式进入主题。

4.1 多线程以及非守护线程

# !/usr/bin/python3# -*- coding: utf-8 -*-# @Author:AI 悦创 @DateTime :2019/10/25 9:50 @Function :功能  Development_tool :PyCharm# code is far away from bugs with the god animal protecting#    I love animals. They taste delicious.import threading, timedef start():    time.sleep(1)    print(threading.current_thread().name) # 当前线程名称    print(threading.current_thread().is_alive()) # 当前线程状态    print(threading.current_thread().ident) # 当前线程的编号print('start')# 要使用多线程哪个函数>>>target=函数,name=给这个多线程取个名字# 如果你不起一个名字的话,那那它会自己去起一个名字的(pid)也就是个 ident# 类似声明thread = threading.Thread(target=start,name='my first thread')# 每个线程写完你不 start()的话,就类似只是声明thread.start()print('stop')

输出

"C:\Program Files\Python37\python.exe" C:/daima/pycharm_daima/爬虫大师班/知识点/多线程/多线程以及非守护线程.pystartstopmy first threadTrue2968Process finished with exit code 0

如果有参数的话,我们就对多线程参数进行传参数。代码示例:

import threading, timedef start(num):    time.sleep(num)    print(threading.current_thread().name)    print(threading.current_thread().isAlive())    print(threading.current_thread().ident)print('start')thread = threading.Thread(target=start,name='my first thread', args=(1,))thread.start()print('stop')

解析:

我认认真看一下我们的运行结果,

| start || ------------------- || stop || my first thread || True || 2968 |

我们会发现并不是按我们正常的逻辑执行这一系列的代码。

而是,先执行完 start 然后就直接 stop 然后才会执行我们函数的其他三项。

一个线程它就直接贯穿到底了。也就是先把我们主线程里面的代码运行完,然后才会运行它里面的代码。

我们的代码并不是当代码执行到 thread.start() 等它执行完再执行 print('stop') 。而是,我们线程执行到thread.start() 继续向下执行,同时再执行里面的代码(也就是start()函数里面的代码)。(不会卡在 thread.start() 那里) 也不会随着主线程结束而结束

因为,程序在执行到 print('stop') 之后就是主线程结束,而里面开的线程是我们自己开的。当我们主线程执行这个 stop 就已经结束了。

这种不会随着主线程结束而销毁的,这种线程它叫做:非守护线程

  1. 主线程会跳过创建的线程继续执行;
  2. 直到创建线程运行完毕;
  3. 程序结束;

既然,有非守护线程。那就还有守护线程。

4.2 守护线程

如果要修改成守护线程,那你就得在 thread.start() 前面加一个:

thread.setDaemon(True)

需要在我们启动之前设置。

import threading, timedef start(num):    time.sleep(num)    print(threading.current_thread().name) # 当前线程的名字    print(threading.current_thread().isAlive())    print(threading.current_thread().ident)print('start')thread = threading.Thread(target=start,name='my first thread', args=(1,))thread.setDaemon(True)thread.start()print('stop')

我们来看看运行的结果

startstop

我们可以看见,程序直接运行:start、stop,执行到 print('stop') 它就结束了。也就随着我们的主线程结束而结束。并不管它里面还有什么没有执行完。(也不会管他里面的 time.sleep())我们的主线程一结束,我们的守护线程就会随着主线程一起销毁。

我们日常启动的是非守护线程,守护线程用的较少。

守护线程会伴随主线程一起结束,setDaemon 设置为 True 即可。

学员问题:任务管理器上面超过五六个进程。都是进程的话,怎么能开那么多呢?

答:我们一个 CPU 不止能执行一个进程,就比如我的一个 CPU 里面密麻麻有许多进程。(比方我现在开六个进程)并发执行的。只不过计算机执行的速度非常快,这里我简单讲一下哈。这是计算机原理的课。

不管是任何操作系统,现在就拿单核操作系统来说:我们假设现在只有一个 CPU ,一个 CPU 里面六个进程,同一时间它只有一个进程在运行。不过我们计算执行速度非常快,这个程序执行完,它就会执行一个上下文切换,执行下一个。(因为,它执行的速度非常快,你就会感觉是并发执行一样。)

实际上,一个 CPU 同一时间只有一个进程在执行,一个进程里面它只有一个线程在执行。(当然,这个单核是五六年前了。现在肯定至少有双核。

那就说有第二个 CPU 了。

而第二个和 CPU 上面又有许多个 进程,两个 CPU 是互不相干。

那这时候,第一个 CPU 上面运行一个进程,而我们的第二个 CPU 上面也有一个进程,两个是互补相干。 (就相当于你开了两台电脑。)

但是同一个 CPU 在同一时间只有一个就进程。(不管你(电脑)速度多么快,实际上本质上(在那一秒)只有一个进程在执行。如果你是双核,那就有两个进程。(四核就有四个进程)


Python 有个不好的地方,刚刚上面讲到,如果我们有两个 CPU 那就有两个进程在执行(那四个 CPU 就是四个进程在执行),但是因为 Python 当中存在着 GIL,它即使有四个 CPU 每次也只有一个线程能进去,也就是说:同一时间当中,一个 CPU 上的一个进程中的一个线程在执行。剩下的都不能运行,我们的 Python 不能利用多核。

如果,大家用的是 C、Java、Go 这种的就没有这个说法了。

5. Lock 锁

接下来是比较难的知识点,比方说我们现在有两个线程,一个是求加一千万次,另一个是减一千万次。按原本得计划来说,一个加一千万一个减一千万结果应该还是零。可是最终得结果并不是等于零,我们多运行几次会发现几次得出来得结果并不相同。多线程代码如下:

import threadingimport timenumber = 0def addNumber(i):    time.sleep(i)    global number    for i in range(1000000):        number += 1    print("加",number)def downNumber(i):    time.sleep(i)    global number    for i in range(1000000):        number -= 1    print("减",number)print("start") # 输出一个开始thread = threading.Thread(target = addNumber, args=(2,)) #开启一个线程(声明)thread2 = threading.Thread(target = downNumber, args=(2,)) # 开启第二个线程(声明)thread.start() # 开始thread2.start() # 开始thread.join()thread2.join()# join 阻塞在这里,直到我们得阻塞线程执行完毕才会向下执行print("外", number)print("stop")

就算单线程也会出现两个值:1000000 与 -1000000,两个函数谁先运行就是输出谁的结果,为什么呢?因为两个函数调用的是全局变量 number 所以,如果先运行加法函数,加法得到的结果是 1000000 ,那全局下的 number 的值也会变成:1000000 ,那减法的操作亦然就是 0。反过来也是一个意思。

import threadingimport timenumber = 0def addNumber():    global number    for i in range(1000000):        number += 1    print("加",number)    return numberdef downNumber():    global number    for i in range(1000000):        number -= 1    print("减",number)    return numbersum_num = downNumber() + addNumber()print("Result", sum_num)# 输出减 -1000000加 0Result -1000000# 修改以下代码,其他不变:sum_num = addNumber() + downNumber()# 输出加 1000000减 0Result 1000000

由上面的多线程代码,我可以发现结果:两个线程操作同一个数字,最后得到的数字是混乱的。为什么说是混乱的呢?

我们现在所要做的是一个赋值,number += 1 其实也就是 number = number + 1,的这个操作。而在我们的 Python 当中,我们是先:计算右边的,然后赋值给左边的,一共两步。

我先来看一下正确的运行流程:

# 我们的 number = 0# 第一步是先运行我们的代码:a = number + 1 # 等价于 0+1=1 # 也就是先运行右边的,然后赋值给 anumber = a # 然后,再把 a 的结果赋值个 number# 上面运行完加法之后,我们加下来运行减肥的操作。b = number - 1 # 等价于 1-1 = 0# 然后,赋值个 number# 最后 number 等于 0number = 0

上面的过成是正确的流程,可在多线程里面呢?

number = 0 # 开始初始值 0a = number+1 # 等价于 0+1=1# 这个地方要注意!!!# 在运行完上面一步的时候,还没来得急把结果赋值给 number# 就开始运行减法操作:b = number-1 # 等价于 0-1=-1# 然后,这两个运行结束之后就被赋值:number=b # b = -1number=a # a = 1# 最终得结果为:number = 1

上面就是我们刚才结果错乱得原因,也就是说:我们计算和赋值是两部,但是该多线程它没有顺序执行,这也就是我们所说的线程不安全。

因为,执行太快了,两个线程交互交织在一起,最终得到我们这个错误结果。以上就是线程不安全的问题。

这就是需要 Lock 锁,给它上一把锁,来达到我们 number 的效果,这个时候为了避免错误,我们要给他上一把锁了。

import threadingimport timelock = threading.Lock() # 创建一个最简单的 读写锁number = 0def addNumber():    global number    for i in range(1000000):        lock.acquire() # 先获取        number += 1        # 中间的这个过程让他强制有这个计算和赋值的过程,也就是让他执行完这两个操作,后再切换。        # 这样就不会完成计算后,还没来的及赋值就跑到下一个去了。        # 这样也就防止了线程不安全的情况        lock.release() # 再释放def downNumber():    global number    for i in range(1000000):        lock.acquire()        number -= 1        lock.release()print("start") # 输出一个开始thread = threading.Thread(target = addNumber) #开启一个线程(声明)thread2 = threading.Thread(target = downNumber) # 开启第二个线程(声明)thread.start() # 开始thread2.start() # 开始thread.join()thread2.join()# join 阻塞在这里,直到我们得阻塞线程执行完毕才会向下执行print("外", number)print("stop")# 输出start外 0stop

在代码:lock.acquire() 与 lock.release() 中间的这个过程让它强制有这个计算和赋值的过程,也就是让他执行完这两个操作,后再切换。这样就不会完成计算后,还没来的及赋值就跑到下一个去了。这样也就防止了线程不安全的情况。

然后,就是我们第一个线程拿到这把锁的 lock.acquire() 了,那另一个线程就会在 lock.acquire() 阻塞了,直到我们另一个线程把 lock.release() 锁释放,然后拿到锁执行,就这样不断地切换拿锁执行。

死锁:就是前面的线程拿到锁之后,运行完却不释放锁,下一个线程在等待前一个线程释放锁,这种就是死锁。说的直白一点就是,相互等待。就像照镜子一样,你中有我,我中有你。也就是在没有 release 的这种情况。(你等我表白,我等你表白)

6. 递归锁 RLOCK

再次复用,一个锁可以再嵌套一个锁。向我们上面的普通锁,一个线程里面,你只能获取一次。如果获取第二次就会报错。

递归锁什么时候用呢?需要更低精度的,力度更小,为了更小的力度。

import threadingimport timeclass Test:    rlock = threading.RLock()    def __init__(self):        self.number = 0    def execute(self, n):        # 原本是获取锁和释放锁,那如果有时候你忘记了写 lock.release() 那就变成了死锁。        # 而 with 可以解决这个问题。        with Test.rlock:            # with 内部有个资源释放的机制            self.number += n    def add(self):        with Test.rlock:            self.execute(1)    def down(self):        with Test.rlock:            self.execute(-1)def add(test):    for i in range(1000000):        test.add()def down(test):    for i in range(1000000):        test.down()if __name__ == '__main__':    thread = Test() # 实例化    t1 = threading.Thread(target=add, args=(thread,))    t2 = threading.Thread(target=down, args=(thread,))    t1.start()    t2.start()    t1.join()    t2.join()    print(t.number)

我们会发现这个递归锁是比较耗费时间的,也就死我们获取锁与释放锁都是进行上下文切换导致资源消耗的,所以说开启的锁越多,所耗费的资源也就越多,程序的运行速度也就越慢。一些大的工程很少上这么多的锁,因为这个锁的速度会拖慢你整个程序的运行速度。所以得思考好,用不用这些东西。

7. 多进程

多线程在 IO 密集型用的比较多,也就是在爬虫方面用的比较多。而 CPU 密集型根本就不用多线程。

我们一般的策略是,多进程加多线程,这样的结合是最好。我需要用到这个库:

import multiprocessing
import multiprocessingimport timedef start(i):    time.sleep(3)    print(i)    # current process    # 当前进程    print(multiprocessing.current_process().name) # 当前进程的名字    print(multiprocessing.current_process().pid) # 进程控制符    print(multiprocessing.current_process().is_alive()) # 判断进程是否存活    # 因为,我们有些进程卡死,所以我就要自己把进程卡死if __name__ == '__main__':    print('start')    p = multiprocessing.Process(target=start, args=(1,), name='p1')    p.start()    print('stop')

PID(进程控制符)英文全称为 Process Identifier,它也属于电工电子类技术术语。

PID 就是各进程的身份标识,程序一运行系统就会自动分配给进程一个独一无二的 PID。进程中止后 PID 被系统回收,可能会被继续分配给新运行的程序。

PID 一列代表了各进程的进程 ID,也就是说,PID 就是各进程的身份标识。

在实际调试中,只能先大致设定一个经验值,然后根据调节效果修改。

image-20200229140520623

8. 进程通信

Python 多进程之间是默认无法通信的,因为是并发执行的。所以需要借助其他数据结构。

举个例子:

你一个进程抓取到数据,要给另一个进程用,就需要进程通信。

队列:就像排队一样,先进先出。也就是你先放进去的数据,也就先取出数据。

栈:主要用在 C 和 C++ 上的数据结构。主要存储用户自定义的数据。它是后进先出。先进去的垫在底层,后进的在上面。

from multiprocessing import Process, Queue# Process :进程# Queue :队列# import multiprocessingdef write(q):    # multiprocessing.current_process().name    # multiprocessing.current_process().pid    # multiprocessing.current_process().is_alive()    print("Process to write: {}" .format(Process.pid))    for i in range(10):        print("Put {} to queue...".format(i))        q.put(i) # 把数字放到我们的队列里面去def read(q):    print("Process to read: {}" .format(Process.pid))    while True:        # 这里为什么要使用 while 呢?因为我们要不断的循环,队列当中有可能没有数据,所以需要一直循环获取。        # 当然,你也可以直接指定循环的次数        value = q.get() # 获取队列中的数据(队列中没有数据就会阻塞在那里)        print("Get {} from queue." .format(value))# 所以就有以下策略:一个线程抓取 url 放入队列之中,另一个队列解析if __name__ == '__main__':    # 父进程创建 Queue ,并传给各个子进程:    q = Queue() # 队列    pw = Process(target=write, args=(q, ))    pr = Process(target=read, args=(q, ))    # 启动子进程 pw ,写入:    pw.start()    # 启动子进程 pr, 读取:    pr.start()    # 等待 pw 结束    pw.join()

举个实操的小例子:

from multiprocessing import Process, Queueimport requestsfrom lxml import etreeheaders = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.116 Safari/537.36'}def spider_url(queue):    session = requests.Session()    session.headers = headers    html = requests.get('https://www.baidu.com')    xml = etree.HTML(html.text)    url = xml.xpath("//div[@class="f-tag"]")    queue.put(url)def parse_url(queue):    while True:        value = queue.get()        titl = value[0]if __name__ == '__main__':    queue = Queue()    spider_url = Process(target=spider_url, args=(queue,))    parse_url = Process(target=parse_url, args=(queue,))    spider_url.start()    parse_url.start()    spider_url.join()    parse_url.join()

9. 进程池与线程池

为什么需要进程池与线程池呢,我就用前面我们在进行上下文切换的时候会有资源消耗,而在这个基础上,创建线程与删除线程都是需要消耗更多的资源。而这个池就节省了资源消耗,这样我们就不用进行创建和销毁了,只要获取里面的使用即可。

9.1 进程池

第一种方法(多任务):

from multiprocessing import Pooldef function_square(data):    result = data*data    return resultif __name__ == '__main__':    inputs = [i for i in range(100)]    # inputs = (i for i in range(100))    # inputs = list(range(100))    pool = Pool(processes=4) # 如果你不指定数目的化,它就会根据你电脑状态,自行创建。    # 按你的电脑自动创建相应的数目    # map 把任务交给进程池    # pool.map(function, iterable)    pool_outputs = pool.map(function_square, inputs)    # pool_outputs = pool.map(function_square, (2,3, 4, 5))    pool.close()    pool.join()    print("Pool     :", pool_outputs)

第二种方法(单任务):

from multiprocessing import Pooldef function_square(data):    result = data*data    return resultif __name__ == '__main__':    pool = Pool(processes=4) # 如果你不指定数目的化,它就会根据你电脑状态,自行创建。(按你的电脑自动创建相应的数目)    # map 把任务交给进程池    # pool.map(function, iterable)    pool_outputs = pool.apply(function_square, args=(10, ))    pool.close()    pool.join()    print("Pool     :", pool_outputs)

使用 from multiprocessing import Pool:引入进程池 ,那这个进程池,它是可以可以提供指定数量进程池,如果有新的请求提交到进程池,如果这个进程池还没有满的话,就创建新的进程来执行请求。 如果池满的话,就会先等待。

# 那么,我们可以首先声明这个进程池;# 然后,使用 map 方法,那其实这个 map 方法和正常的 map 方法是一致的。# map:# pool = Pool()# pool.map(main, [i*10 for i in range(10)])# 第一个参数:他会将数组中的每一个元素拿出来,当作函数的一个个参数,然后创建一个个进程,放到进程池里面去运行。# 第二个参数:构造一个数组,然后也就是 0 到 90 的这么一个循环,那我们直接使用 list 构造一下

9.3 实战(猫眼 TOP100 + re + multiprocessing)

# !/usr/bin/python3# -*- coding: utf-8 -*-# @Author:AI 悦创 @DateTime :2020/2/12 15:23 @Function :功能  Development_tool :PyCharm# code is far away from bugs with the god animal protecting#    I love animals. They taste delicious.# https://maoyan.com/board/4?offset=0# https://maoyan.com/board/4?offset=10# https://maoyan.com/board/4?offset=20# https://maoyan.com/board/4?offset=30import requests,re,jsonfrom requests.exceptions import RequestExceptionfrom multiprocessing import Pool # 引入进程池headers = {    'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.87 Safari/537.36'}session = requests.Session()session.headers = headersdef get_one_page(url):    try:        response = session.get(url)        if response.status_code == 200:            return response.text        return None    except RequestException:        return Nonedef parse_one_page(html):    pattern = re.compile('<dd>.*?board-index.*?>(\d+)</i>.*?data-src="(.*?)".*?name.*?><a'                         +'.*?>(.*?)</a>.*?star">(.*?)</p>.*?releasetime">(.*?)</p>.*?integer">'                          +'(.*?)</i>.*?fraction">(.*?)</i>.*?</dd>', re.S)    # 标签的开始和结尾都要写出来!!!    items = re.findall(pattern, html)    # 使用 yield 把这个方法变成一个生成器    # 要把返回的结果做成一个键值对的形式    for item in items:        yield {            'index': item[0],            'image': item[1],            'title': item[2],            'actor': item[3].strip()[17:],            'time': item[4][5:],            'score': item[5]+item[6]        }def write_to_file(content):    # print(type(content))    # with open('result.txt', 'a') as f:    with open('result.txt', 'a', encoding='utf-8') as f:        # 字典转换成字符串        # f.write(json.dumps(content) + '\n') # 中文编码变成 Unicode        f.write(json.dumps(content, ensure_ascii=False) + '\n')        f.close()def main(offset):    url = f'https://maoyan.com/board/4?offset={offset}'    html = get_one_page(url)    for item in parse_one_page(html):        print(item)        write_to_file(item)# 1.0# if __name__ == '__main__':#     for i in range(10): # range(0, 100, 10)#         main(i*10)# 2.0if __name__ == '__main__':    pool = Pool()    pool.map(main, [i*10 for i in range(10)])# 优化,如果你要秒抓的话,使用 from multiprocessing import Pool # 引入进程池 ,当然我们目的不是秒抓,而是学习一下多进程的用法# 那么这个进程池,他是可以可以提供指定数量进程池,如果有新的请求提交到进程池,如果这个进程池还没有满的话,就创建新的进程来执行请求。# 如果池满的话,就会先等待# 那么,我们可以首先声明这个进程池;# 然后,使用 map 方法,那其实这个 map 方法和正常的 map 方法是一致的。# map:# pool = Pool()# pool.map(main, [i*10 for i in range(10)])# 第一个参数:他会将数组中的每一个元素拿出来,当作函数的一个个参数,然后创建一个个进程,放到进程池里面去运行。# 第二个参数:构造一个数组,然后也就是 0 到 90 的这么一个循环,那我们直接使用 list 构造一下

9.2 线程池

我找了许多包,这个包还是不错的:Pip install threadpool

# project = 'Code', file_name = '线程池', author = 'AI 悦创'# time = '2020/3/3 0:05', product_name = PyCharm# code is far away from bugs with the god animal protecting#    I love animals. They taste delicious.import timeimport threadpool# 执行比较耗时的函数,需要开多线程def get_html(url):    time.sleep(3)    print(url)# 按原本的单线程运行时间为:300s# 而多线程池的化:30s# 使用多线程执行 telent 函数urls = [i for i in range(100)]pool = threadpool.ThreadPool(10) # 建立线程池# 提交任务给线程池requests = threadpool.makeRequests(get_html, urls)# 开始执行任务for req in requests:    pool.putRequest(req)pool.wait()

作业

将你原先写过的任何一个爬虫程序改为多线程或者多进程。

阅读全文: http://gitbook.cn/gitchat/activity/5e6063f9f3d3ae72aa50a121

您还可以下载 CSDN 旗下精品原创内容社区 GitChat App ,阅读更多 GitChat 专享技术内容哦。

FtooAtPSkEJwnW-9xkCLqSTRpBKX

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值