前言

在爬虫学习的过程中,一旦爬取的数量过大,很容易带来效率问题,为了能够快速爬取我们想要的内容。为此我们可以使用多线程或者多进程来处理。

多线程和多进程是不一样的!一个是 threading 库,一个是 multiprocessing 库。而多线程 threading 在 Python 里面被称作鸡肋的存在!关于 Python 多线程有这样一句名言——“Python下多线程是鸡肋,推荐使用多进程!”

为什么称 Python 多线程是鸡肋?原因如下:

在大多数环境中,单核CPU情况下,本质上某一时刻只能有一个线程被执行,多核 CPU 则可以支持多个线程同时执行。但是在 Python 中,无论 CPU 有多少核,CPU在同一时间只能执行一个线程。这是由于 GIL 的存在导致的。

GIL 的全称是 Global Interpreter Lock(全局解释器锁),是 Python 设计之初为了数据安全所做的决定。Python 中的某个线程想要执行,必须先拿到 GIL。可以把 GIL 看作是执行任务的“通行证”,并且在一个 Python 进程中,GIL 只有一个。拿不到通行证的线程,就不允许进入 CPU 执行。GIL 只在 CPython 解释器中才有,因为 CPython 调用的是 c 语言的原生线程,不能直接操作 CPU,只能利用 GIL 保证同一时间只能有一个线程拿到数据。在 PyPy 和 JPython 中没有 GIL。

在 Python 多线程下,每个线程的执行方式:

  1. 拿到公共数据
  2. 申请GIL
  3. Python解释器调用操作系统原生线程
  4. cpu执行运算
  5. 当该线程执行一段时间消耗完,无论任务是否已经执行完毕,都会释放GIL
  6. 下一个被CPU调度的线程重复上面的过程

任何Python线程执行前,必须先获得GIL锁,然后,每执行100条字节码,解释器就自动释放GIL锁,让别的线程有机会执行。这个GIL全局锁实际上把所有线程的执行代码都给上了锁,所以,多线程在Python中只能交替执行,即使100个线程跑在100核CPU上,也只能用到1个核。

那么是不是python的多线程就完全没用了呢?

Python 针对不同类型的任务,多线程执行效率是不同的:

  • 对于 CPU 密集型任务(各种循环处理、计算等等),由于计算工作多,ticks 计数很快就会达到阈值,然后触发 GIL 的释放与再竞争(多个线程来回切换是需要消耗资源的),所以 Python 下的多线程对 CPU 密集型任务并不友好。
  • IO 密集型任务(文件处理、网络通信等涉及数据读写的操作),多线程能够有效提升效率(单线程下有 IO 操作会进行 IO 等待,造成不必要的时间浪费,而开启多线程能在线程 A 等待时,自动切换到线程 B,可以不浪费 CPU 的资源,从而能提升程序执行效率)。所以 Python 的多线程对 IO 密集型任务比较友好。

实际中的建议

Python 中想要充分利用多核 CPU,就用多进程。因为每个进程有各自独立的 GIL,互不干扰,这样就可以真正意义上的并行执行。在 Python 中,多进程的执行效率优于多线程(仅仅针对多核 CPU 而言)。同时建议在 IO 密集型任务中使用多线程,在计算密集型任务中使用多进程。

Python 创建多线程

1.直接利用函数创建多线程

在 Python3 中,Python 提供了一个内置模块 threading.Thread,可以很方便地让我们创建多线程。
threading.Thread()一般接收两个参数:

  • target(线程函数名):要放置线程让其后台执行的函数,由我们自已定义,注意不要加();
  • args(线程函数的参数):线程函数名所需的参数,以元组的形式传入。若不需要参数,可以不指定。
import time
from threading import Thread# 自定义线程函数。
def main(name="Python"):for i in range(2):print("hello", name)time.sleep(2)# 创建线程01,不指定参数
thread_01 = Thread(target=main)
# 启动线程01
thread_01.start()# 创建线程02,指定参数,注意逗号
thread_02 = Thread(target=main, args=("MING",))
# 启动线程02
thread_02.start()

输出结果:

hello Python
hello MING
hello Python
hello MING

2. 使用 Threading 模块构建类对象

使用 Threading 模块创建线程,直接从 threading.Thread 继承,然后重写 init 方法和 run 方法。

import time
from threading import Threadclass MyThread(Thread):def __init__(self, name="Python"):# 注意,super().__init__() 一定要写# 而且要写在最前面,否则会报错。super().__init__()self.name=namedef run(self):for i in range(2):print("hello", self.name)time.sleep(2)if __name__ == '__main__':# 创建线程01,不指定参数thread_01 = MyThread()# 创建线程02,指定参数thread_02 = MyThread("MING")thread_01.start()thread_02.start()

输出结果:

hello Python
hello MING
hello MING
hello Python

上述是最基本的多线程创建方法,看起来很简单,但在实际的应用中,会复杂许多。

接下来,关于我实际学习中的多线程练习,我会一一记录下来。

多线程练习

在讲述下面的线程同步前,我先用个实例描述一下线程未同步。

import threading
import timeclass myThread(threading.Thread):def __init__(self, threadID, name, counter):threading.Thread.__init__(self)self.threadID = threadIDself.name = nameself.counter = counterdef run(self):print('Starting '+self.name)print_time(self.name, self.counter, 3)print("Exiting " + self.name)def print_time(threadName, delay, count):while count:time.sleep(1)print('{0} processing {1}'.format(threadName, time.ctime(time.time())))count -= 1if __name__ == '__main__':threadList = ["Thread-1", "Thread-2"]threads = []threadID = 1for tName in threadList:thread = myThread(threadID, tName, threadID)thread.start()threads.append(thread)threadID += 1# for t in threads:#     t.join()print("Exiting Main Thread")

输出结果:

Starting Thread-1Starting Thread-2Exiting Main Thread
Thread-1 processing Sat May 11 11:06:11 2019
Thread-2 processing Sat May 11 11:06:12 2019
Thread-1 processing Sat May 11 11:06:12 2019
Thread-1 processing Sat May 11 11:06:13 2019
Exiting Thread-1
Thread-2 processing Sat May 11 11:06:14 2019
Thread-2 processing Sat May 11 11:06:16 2019
Exiting Thread-2

由输出结果中可以看出,第二个线程在打印过程中没有回车打印,说明线程间没有同步。
线程同步——Lock

多线程中,所有变量都由所有线程共享,所以,任何一个变量都可以被任何一个线程修改,因此,线程之间共享数据最大的危险在于多个线程同时改一个变量,把内容给改乱了。

使用Thread对象的Lock和Rlock可以实现简单的线程同步,这两个对象都有acquire方法和release方法,对于那些需要每次只允许一个线程操作的数据,可以将其操作放到acquire和release方法之间。

import threading
import queue
import timeexitFlag = 0class myThread(threading.Thread):def __init__(self, que):threading.Thread.__init__(self)self.que = quedef run(self):print("Starting " + threading.currentThread().name)process_data(self.que)print("Ending " + threading.currentThread().name)def process_data(que):while not exitFlag:queueLock.acquire()if not workqueue.empty():data = que.get()queueLock.release()print('Current Thread Name %s, data: %s ' % (threading.currentThread().name, data))else:queueLock.release()time.sleep(0.1)if __name__ == '__main__':start_time = time.time()queueLock = threading.Lock()workqueue = queue.Queue()threads = []# 填充队列queueLock.acquire()for i in range(1,10):workqueue.put(i)queueLock.release()for i in range(4):thread = myThread(workqueue)thread.start()threads.append(thread)#等待队列清空while not workqueue.empty():pass#通知线程退出exitFlag = 1for t in threads:t.join()print("Exiting Main Thread")end_time = time.time()print('耗时{}s'.format((end_time - start_time)))

输出结果:

Starting Thread-1
Current Thread Name Thread-1, data: 1, time: Sat May 11 10:47:25 2019
Starting Thread-2
Current Thread Name Thread-2, data: 2, time: Sat May 11 10:47:25 2019
Starting Thread-3
Current Thread Name Thread-3, data: 3, time: Sat May 11 10:47:25 2019
Starting Thread-4
Current Thread Name Thread-4, data: 4, time: Sat May 11 10:47:25 2019
Current Thread Name Thread-1, data: 5, time: Sat May 11 10:47:25 2019
Current Thread Name Thread-2, data: 6, time: Sat May 11 10:47:25 2019
Current Thread Name Thread-3, data: 7, time: Sat May 11 10:47:25 2019
Current Thread Name Thread-4, data: 8, time: Sat May 11 10:47:25 2019
Current Thread Name Thread-2, data: 9, time: Sat May 11 10:47:25 2019
Ending Thread-4
Ending Thread-1
Ending Thread-2
Ending Thread-3
Exiting Main Thread
耗时0.31415677070617676s

线程同步——Queue

Python 的 Queue 模块中提供了同步的、线程安全的队列类,包括 FIFO(先入先出)队列 Queue,LIFO(后入先出)队列LifoQueue,和优先级队列 PriorityQueue。这些队列都实现了锁原语,能够在多线程中直接使用。可以使用队列来实现线程间的同步。

import threading
import time
import queuedef get_num(workQueue):print("Starting " + threading.currentThread().name)while True:if workQueue.empty():breaknum = workQueue.get_nowait()print('Current Thread Name %s, Url: %s ' % (threading.currentThread().name, num))time.sleep(0.3)# try:#     num = workQueue.get_nowait()#     if not num:#         break#     print('Current Thread Name %s, Url: %s ' % (threading.currentThread().name, num))# except:#     break# time.sleep(0.3)print("Ending " + threading.currentThread().name)if __name__ == '__main__':start_time = time.time()workQueue = queue.Queue()for i in range(1,10):workQueue.put(i)threads = []thread_num = 4  #线程数for i in range(thread_num):t = threading.Thread(target=get_num, args=(workQueue,))t.start()threads.append(t)for t in threads:t.join()end_time = time.time()print('耗时{}s'.format((end_time - start_time)))

输出结果:

Starting Thread-1
Current Thread Name Thread-1, data: 1, time: Sat May 11 10:48:27 2019
Starting Thread-2
Current Thread Name Thread-2, data: 2, time: Sat May 11 10:48:27 2019
Starting Thread-3
Current Thread Name Thread-3, data: 3, time: Sat May 11 10:48:27 2019
Starting Thread-4
Current Thread Name Thread-4, data: 4, time: Sat May 11 10:48:27 2019
Current Thread Name Thread-2, data: 5, time: Sat May 11 10:48:27 2019
Current Thread Name Thread-1, data: 6, time: Sat May 11 10:48:27 2019
Current Thread Name Thread-4, data: 7, time: Sat May 11 10:48:27 2019
Current Thread Name Thread-3, data: 8, time: Sat May 11 10:48:27 2019
Current Thread Name Thread-2, data: 9, time: Sat May 11 10:48:28 2019
Ending Thread-1
Ending Thread-4
Ending Thread-3
Ending Thread-2
耗时0.9028356075286865s

因为考虑到爬虫方面的实际应用,所以在程序中使用了 Python 的 Queue 模块,使用队列来实现线程间的同步(关于线程同步在后面会讲到)。在 get_num() 方法中注释的内容是另一种判断队列为空,结束子线程任务的代码实现。

在实际运行中,通过调节线程数可以看到,执行时间会随着线程数的增加而缩短。

Python 多线程学习相关推荐

  1. Python多线程学习(上)

    最近在学习python多线程,写一下随笔就当复习了.另外强烈推荐大家看一下<Python核心编程>这本书,这本书里面可以帮你学习python进阶. 一.基本概念: 1.线程: 线程又称为轻 ...

  2. Python多线程学习

    一.Python中的线程使用: Python中使用线程有两种方式:函数或者用类来包装线程对象. 1.  函数式:调用thread模块中的start_new_thread()函数来产生新线程.如下例: ...

  3. python多线程学习-多线程下载图片

    目录 开发工具 知识点 代码 总结 开发工具 python版本: python-3.8.1-amd64 python开发工具: JetBrains PyCharm 2018.3.6 x64 知识点 多 ...

  4. Python多线程学习(下)

    今天介绍用Queue进行线程间的信息共享,具体就是创建一个类似队列,将数据存进去,供不同的线程之间进行利用数据.例:消费者和生产者问题,生产的产品存入队列,有消费者进行对产品消费,生产者向队列放入产品 ...

  5. Python多线程学习教程

    首先我们来解释一下多线程:多线程我们可以理解为多个进程/多个程序同时运行,多线程最大的好处就是帮助我们提高效率,平常我们1小时完成的任务,通过多线程10分钟就可以完成,甚至更短,这个就取决于你的线程数 ...

  6. python多线程执行_python多线程实现同时执行两个while循环

    如果想同时执行两个while True循环,可以使用多线程threading来实现. 完整代码 #coding=gbk from time import sleep, ctime import thr ...

  7. 【Python爬虫学习实践】多线程爬取Bing每日壁纸

    在本节实践中,我们将借助Python多线程编程并采用生产者消费者模式来编写爬取Bing每日壁纸的爬虫.在正式编程前,我们还是一样地先来分析一下我们的需求及大体实现的过程. 总体设计预览 首先,我们先来 ...

  8. 【Python基础学习】—多线程

    前言 我们知道,每个独立的进程有一个程序运行的入口.顺序执行序列和程序的出口.进程里面的任务由线程执行,线程必须依存在应用程序中,多个线程执行能够提高应用程序的执行效率,多个线程之间共用进程的寄存器数 ...

  9. python多线程并发学习

    1.python多线程适用于什么场景? 举个��:当我们想从网页上下载信息,或者从ftp服务器上下载版本时,若是版本太大,那么顺序的执行下载N个(N>1)版本就会耗费许多时间,若是可以并发地下载 ...

最新文章

  1. 造完家怎么拆东西_我今天把老家的宅基地拆了!
  2. python搜索列表内_使用Python在另一个List中搜索列表的值
  3. 成功解决ValueError: Parameter values for parameter (max_depth) need to be a sequence.
  4. 一文搞定C#关于NPOI类库的使用读写Excel以及io流文件的写出
  5. 浅谈javascript递归(白话版)
  6. c# 添加防火墙例外端口_C#添加删除防火墙例外(程序、端口)
  7. 字母三角形c语言ABBBCCCCC,C语言输出ABBBCCCCCDDDDDDDCCCCCBBBA
  8. 社群数据分析:你运营的社群是好社群吗?
  9. 算法与数据结构(一)-导学
  10. python之路_mysql数据操作1
  11. hcfax2e伺服驱动器说明书_SD伺服驱动器说明书
  12. lacp静态和动态区别_LACP协议原理
  13. SSDP协议内容解析
  14. 51单片机SG90舵机控制原理
  15. word上下的横线怎么去掉_word文档上面有一条横线怎么去掉
  16. Maven知识补充(项目模型变量,Maven属性,依赖项的范围,查找公共存储库的依赖项等)
  17. 模拟城市服务器连接中断 正试着,【模拟城市5】确认DRM在线 中断不会被踢
  18. JavaScript面向对象学习深拷贝、浅拷贝(三)
  19. 标准中文电码 API数据接口
  20. gcc/g++ 如何支持c11 / c++11标准编译

热门文章

  1. python中文注释与单行注释_Python单行注释方法
  2. mysql 命令行 h_mysql-命令行
  3. Docker容器与容器云
  4. ipv6地址概述——了解ipv6与ipv4不同
  5. 计算机专业研究生职业生涯规划
  6. UNIX和Linux Shell正则表达式语法介绍
  7. window.showModalDialog 简介
  8. svn服务器端下载linux,Svn linux服务端安装及配置
  9. 无效的列类型: 1111
  10. 52、疏散楼梯的设计要求