人人都能学会的 Python 多线程指南!

人人都能学会的 Python 多线程指南~ 大家好鸭!有没有想我~(https://jq.qq.com/?_wv=1027&k=rX9CWKg4) 在 Python 中,多线程最常见的一个场景就是爬虫,例如这样一个需求,有多个结构一样的页面需要爬取,例如下方的URL(豆瓣阿凡达影评,以10个为例) url_list = [ ‘https://movie.douban.com/subject/1652587/reviews?sort=time&start=0’, ‘https://movie.douban.com/subject/16 阅读详情

在 Python 中,多线程最常见的一个场景就是爬虫,例如这样一个需求,有多个结构一样的页面需要爬取,例如下方的URL(豆瓣阿凡达影评,以10个为例)

  url_list = [  
      'https://movie.douban.com/subject/1652587/reviews?sort=time&start=0',  
      'https://movie.douban.com/subject/1652587/reviews?sort=time&start=20',  
      'https://movie.douban.com/subject/1652587/reviews?sort=time&start=40',  
      'https://movie.douban.com/subject/1652587/reviews?sort=time&start=60',  
      'https://movie.douban.com/subject/1652587/reviews?sort=time&start=80',  
      'https://movie.douban.com/subject/1652587/reviews?sort=time&start=100',  
      'https://movie.douban.com/subject/1652587/reviews?sort=time&start=120',  
      'https://movie.douban.com/subject/1652587/reviews?sort=time&start=140',  
      'https://movie.douban.com/subject/1652587/reviews?sort=time&start=160',  
      'https://movie.douban.com/subject/1652587/reviews?sort=time&start=180']

如果依次爬取,请求第一个页面——得到返回数据——解析数据——提取、存储数据——请求第二个页面,按照这样的思路,那么大量时间都会浪费在请求、返回数据上,如果在等待第一个页面返回数据时去请求第二个页面,就能有效的提高效率,多线程就可以实现这样的功能。

在Python中实现多线程的方法也很多,我将基于 threading 模块一点一点介绍,注意本文不会太注重于多线程背后的技术概念(面试常问),仅希望用最少的话教会大家如何实现。当然会在最后介绍如何使用threading模块来解决上面的爬虫问题。

threading基本使用

让我们先从一个简单的例子开始,定义do_something函数,执行该函数需要消耗1秒

import time  
  
start = time.perf_counter()  
  
def do_something():  
    print("-> 线程启动")  
    time.sleep(1)  
    print("-> 线程结束")  
  
do_something()  
  
finish = time.perf_counter()  
  
print(f"全部任务执行完成,耗时 {round(finish - start,2)} 秒")  
  

上面的代码不难理解,执行do_something并计算耗时,结果很明显应该是1s

-> 线程启动  
-> 线程结束  
全部任务执行完成,耗时 1.01 秒  

现在如果需要执行两次do_something,按照最基本的思路

import time  
  
start = time.perf_counter()  
  
  
def do_something():  
    print("-> 线程启动")  
    time.sleep(1)  
    print("-> 线程结束")  
  
  
do_something()  
do_something()  
  
finish = time.perf_counter()  
  
print(f"全部任务执行完成,耗时 {round(finish - start,2)} 秒")  

执行上面代码结果也很容易猜到是2秒

-> 线程启动  
-> 线程结束  
-> 线程启动  
-> 线程结束  
全部任务执行完成,耗时 2.01 秒  

这就是最常规的 同步 思路,在CPU执行第一个函数,也就是等待1s的时间内,什么也不干,等第一个函数执行完毕后再执行第二个函数

很明显,这样让CPU干等着啥也不干并不是一个很好的选择,而多线程就是解决这一问题的方法之一,让CPU在等待某个任务完成时去执行更多的操作,将整个过程简化为下图流程,这样就能充分节省时间

现在使用threading来通过多线程的方式实现上面的过程,非常简单,定义两个线程并依次启动即可👇

import time  
import threading  
  
start = time.perf_counter()  
  
  
def do_something():  
    print("-> 线程启动")  
    time.sleep(1)  
    print("-> 线程结束")  
  
  
thread1 = threading.Thread(target=do_something)  
thread2 = threading.Thread(target=do_something)  
  
thread1.start()  
thread2.start()  
  
finish = time.perf_counter()  
  
print(f"全部任务执行完成,耗时 {round(finish - start,2)} 秒")  

执行上面的代码,结果如下

-> 线程启动  
-> 线程启动  
全部任务执行完成,耗时 0.0 秒  
-> 线程结束  
-> 线程结束  

可以看到,两个子线程确实同时启动,但是主线程并未等待两个子线程执行完毕就直接结束。

为了解决这个问题,我们可以使用threading.join()方法,意思是在子线程完成运行之前,这个子线程的父线程将一直被阻塞

换成人话就是让主线程挂起,等待所有子线程结束再执行,体现到代码上也很简单,只需要添加两行即可

import time  
import threading  
  
start = time.perf_counter()  
  
  
def do_something():  
    print("-> 线程启动")  
    time.sleep(1)  
    print("-> 线程结束")  
  
  
thread1 = threading.Thread(target=do_something)  
thread2 = threading.Thread(target=do_something)  
  
thread1.start()   
thread2.start()  
  
thread1.join()  
thread2.join()  
  
finish = time.perf_counter()  
  
print(f"全部任务执行完成,耗时 {round(finish - start,2)} 秒")  

运行结果如下,全部代码在1秒内运行完毕

-> 线程启动  
-> 线程启动  
-> 线程结束  
-> 线程结束  
全部任务执行完成,耗时 1.01 秒  

至此,我们就得到了第一个有效的多线程代码,相信你也能大致明白threading的基本使用流程。

传递参数

现在来看看如何在多线程之间传递参数,让我们升级代码:do_something函数来接受一个参数,控制他睡眠等待的时间

def do_something(num):  
    print(f"-> 线程{num} 启动,睡眠 {num} 秒")  
    time.sleep(num)  
    print(f"-> 线程{num} 结束")  

在 threading 中,创建线程时可以使用 args 来传递参数,例如现在接收一个参数,则上一小节的代码可以如下修改

import time  
import threading  
  
start = time.perf_counter()  
  
def do_something(num):  
    print(f"-> 线程{num} 启动,睡眠 {num} 秒")  
    time.sleep(num)  
    print(f"-> 线程{num} 结束")  
  
thread1 = threading.Thread(target=do_something,args = [1])  
thread2 = threading.Thread(target=do_something,args = [2])  
  
thread1.start()  
thread2.start()  
  
thread1.join()  
thread2.join()  
  
finish = time.perf_counter()  
  
print(f"全部任务执行完成,耗时 {round(finish - start,2)} 秒")  

这段代码中,我分别让两个线程等待1、2秒,运行结果显然应该是2秒

-> 线程1 启动,睡眠 1 秒  
-> 线程2 启动,睡眠 2 秒  
-> 线程1 结束  
-> 线程2 结束  
全部任务执行完成,耗时 2.01 秒  

如果你的线程函数需要更多的参数,只需要依次向args中追加即可。

简化代码

上面的案例中,我们仅开启了两个线程,如果是更多个线程的话,再依次重复定义、启动就会显得十分繁琐,此时我们可以使用循环来处理。

例如开启10个线程,依次睡眠1-10秒,可以先创建一个 list 用于存储每个线程,接着利用循环依次创建线程,启动后追加到刚刚创建的 list 中,之后再依次等待每个线程执行完毕,代码如下

import time  
import threading  
  
start = time.perf_counter()  
  
def do_something(num):  
    print(f"-> 线程{num} 启动,睡眠 {num} 秒")  
    time.sleep(num)  
    print(f"-> 线程{num} 结束")  
  
thread_list = []  
  
for i in range(1,11):  
  
    thread = threading.Thread(target=do_something, args=[i])  
    thread.start()  
    thread_list.append(thread)  
  
for t in thread_list:  
  
    t.join()  
  
finish = time.perf_counter()  
  
print(f"全部任务执行完成,耗时 {round(finish - start,2)} 秒")  

结果是显然的,虽然我们执行了十次do_something,每次用时1-10秒,但总耗时应该为10秒

-> 线程1 启动,睡眠 1 秒  
-> 线程2 启动,睡眠 2 秒  
-> 线程3 启动,睡眠 3 秒  
-> 线程4 启动,睡眠 4 秒  
-> 线程5 启动,睡眠 5 秒  
-> 线程6 启动,睡眠 6 秒  
-> 线程7 启动,睡眠 7 秒  
-> 线程8 启动,睡眠 8 秒  
-> 线程9 启动,睡眠 9 秒  
-> 线程10 启动,睡眠 10 秒  
-> 线程1 结束  
-> 线程2 结束  
-> 线程3 结束  
-> 线程4 结束  
-> 线程5 结束  
-> 线程6 结束  
-> 线程7 结束  
-> 线程8 结束  
-> 线程9 结束  
-> 线程10 结束  
全部任务执行完成,耗时 10.01 秒  

共享变量锁的问题

现在,你应该已经了解 threading 最基本的用法,只需要将 do_somthing 函数进行修改即可,但是如果你深入使用,还会有其他的问题出现,例如共享变量的问题,让我们继续探讨。

多线程很常见的一个应用就是爬虫,回到开头的爬虫问题,如果我们希望爬取10个网页的评论,可能会先定一个空dataframe,然后使用多线程都往这个dataframe中写入数据,但由于多个线程同时操作这一个变量,可能会导致评论并不是按照顺序写入的。

例如第一个页面有10条评论,第一个线程写入了2条后,第二个线程将第二个页面的前两条写入,最终导致十个页面的评论是乱序存储!

让我们把这个问题抽象出来,还是之前的代码,稍微修改一下

我们先定义了一个空list,线程函数会将传入的数字添加到该list中,在未加锁的情况下,由于线程竞争,虽然我们线程是按照顺序开启,但是最终数字并不是按照顺序写入。

有没有办法解决呢?当然有,很自然的想法就是当第一个线程操作该变量时,其他线程等着,写完了再释放,这就是锁!

先看代码

在上面的代码中,我们使用 threding.Lock 创建了一个线程锁,之后在线程函数操作 result 前,首先使用 lock.acquire() 加上锁,之后操作 results ,在修改完后使用 lock.relese() 释放,此时其他线程若想操作 results 则会阻塞,等该线程释放后才能拿走操作中,这样我们就保证了线程是“安全的”!

最基本的线程锁用法就如上面代码所示,定义锁 --> 上锁 --> 解锁,但是一定要注意,lock.acquire() 和 lock.relese(),如果加了锁但是没有释放,后面的线程将会全部阻塞!

限制线程数量

最后还有一个常见的问题,上面我们需要执行几次线程函数就开了几个线程,但是如果需要爬成千上万个网页,开这么多线程cpu一定不同意,代码也会在开启的线程达到一定数量后报错。

所以如何让程序只启动我们指定的线程数量,例如一次开五个线程,结束一个再添加一个,直到全部任务完成?

还是锁!在 threading 模块中有一个 BoundedSemaphore(信号量)类,我们可以给他一个初始的信号量(最大线程数),之后每次有线程获得信号量的时候(即 acquire() )计数器-1,释放信号量时候(release())计数器+1,计数器为0的时候其它线程就被阻塞无法获得信号量。当计数器为设定好的上限的时候 BoundedSemaphore 就无法进行 release() 操作了。

体现到代码上则比较简单,还是基于上面的例子修改

总共需要运行十次,我们定义最大线程数为3,并在线程启动前调用acquire方法增加一个计数,在线程最后释放。

此时程序一次只能启动三个线程,如图中所示,首先启动123,之后完成123,启动456,当第四个线程结束启动第七个线程······直到全部线程结束。

这里我们同时使用了上一节说的线程锁来保护变量,用 BoundedSemaphore 锁来控制最大线程数,在实际写代码时就需要小心检查锁是否正确释放,否则就会报错!

一个真实的多线程爬虫案例

至此,threading 模块最常见的用法就介绍完毕,现在让我们回到本文一开始的问题,有多个(以十个为例)URL需要爬取,既然每个页面需要执行的操作一样,如果等待一个页面爬取完毕再爬第二页面就太浪费时间了。这时就可以仿照上面的思路去使用多线程加速。

我们只需要将上面的do_something函数修改为对也面的爬取操作,之后的创建启动线程操作不变即可,代码如下

import time  
import threading  
import requests  
import pandas as pd  
from faker import Faker  
from bs4 import BeautifulSoup  
  
  
def craw_url(url):  
    global df  
    fake = Faker()  
    headers = {'User-Agent': fake.user_agent()}  
    r = requests.get(url, headers=headers)  
    soup = BeautifulSoup(r.content, 'html.parser')  
    review_list = soup.find_all(class_="main review-item")  
  
    for i in range(len(review_list)):  
  
        rank = review_list[i].select('span')[0].get('title')  
        time1 = review_list[i].select('span')[1].get('content')  
        title = review_list[i].select('h2>a')[0].text  
        df = df.append({'时间': time1,  
                        '评分': rank,  
                        '标题': title, }, ignore_index=True)  
  
    print("-> 爬取完成")  
  
  
if __name__ == '__main__':  
  
    start = time.perf_counter()  
    df = pd.DataFrame(columns=['时间', '评分', '标题'])  
  
    url_list = [  
        'https://movie.douban.com/subject/1652587/reviews?sort=time&start=0',  
        'https://movie.douban.com/subject/1652587/reviews?sort=time&start=20',  
        'https://movie.douban.com/subject/1652587/reviews?sort=time&start=40',  
        'https://movie.douban.com/subject/1652587/reviews?sort=time&start=60',  
        'https://movie.douban.com/subject/1652587/reviews?sort=time&start=80',  
        'https://movie.douban.com/subject/1652587/reviews?sort=time&start=100',  
        'https://movie.douban.com/subject/1652587/reviews?sort=time&start=120',  
        'https://movie.douban.com/subject/1652587/reviews?sort=time&start=140',  
        'https://movie.douban.com/subject/1652587/reviews?sort=time&start=160',  
        'https://movie.douban.com/subject/1652587/reviews?sort=time&start=180']  
    thread_list = []  
    for i in url_list:  
  
        thread = threading.Thread(target=craw_url, args=[i])  
        thread.start()  
        thread_list.append(thread)  
  
    for t in thread_list:  
  
        t.join()  
  
    finish = time.perf_counter()  
      
    print(f"全部任务执行完成,耗时 {round(finish - start,2)} 秒")  

执行这段代码,差不多仅用了1秒就将全部内容爬取并存储到 dataframe 中,比同步的代码块了近十倍!如果感兴趣的话可以自己尝试一下。

至此,有关 Python 多线程模块 threading 的基本用法与需要注意的几点就介绍完毕,如果全部认真看完的话,我相信你一定能照猫画虎写出第一个多线程爬虫脚本。

当然有关 Python 多线程还有很多饱受诟病的争议(GIL),多线程的实现方法也远不止 threading 模块,例如更常见的写法是通过 concurrent.futures 模块以及多进程、协程,这些都留在本系列后续文章中再进一步讨论!


读者福利

如果你是0基础,想要学习Python的话,可以试试我整理的这份Python全套学习资料,0基础入门+进阶实战,学完就可以自己在网上帮人写写脚本,接单赚米了~

需要的可以自行扫描下方SCDN官方认证二维码免费领取

😝朋友们如果有需要的话,可以V扫描下方二维码免费领取🆓

学好 Python 不论是就业还是做副业赚钱都不错,但要学会 Python 还是要有一个学习规划。最后大家分享一份全套的 Python 学习资料,给那些想学习 Python 的小伙伴们一点帮助!

#### **一、Python学习路线**

image-20230619144606466

python学习路线图1

二、Python基础学习
1. 开发工具

2. 学习笔记

在这里插入图片描述

3. 学习视频

在这里插入图片描述

三、Python小白必备手册

图片

四、数据分析全套资源

在这里插入图片描述

五、Python面试集锦
1. 面试资料

在这里插入图片描述

在这里插入图片描述

2. 简历模板

在这里插入图片描述

** 因篇幅有限,仅展示部分资料,添加上方即可获取**
人人都能学会的 Python 多线程指南 大家好,我是早起。 在 Python 中,多线程最常见的一个场景就是爬虫,例如这样一个需求,有多个结构一样的页面需要爬取,例如下方的URL(豆瓣阿凡达影评,以10个为例) url_list=[ 'https://movie.douban.com/subject/1652587/reviews?sort=time&start=0', 'https://movie.douban.com/subject/1652587/reviews?sort=time&st... 阅读详情

相关推荐

奇迹MU单机版开服全流程:从零搭建到分辨率调整(附GM工具使用技巧)

本文详细介绍了奇迹MU单机版从零搭建到分辨率调整的完整流程,包括环境配置、服务端启动、GM工具使用技巧等关键步骤。特别针对现代高分辨率显示器提供了优化方案,帮助玩家轻松搭建个性化游戏环境,重温经典MMORPG的独特魅力。

b3n4m5q6w7的博客 676

Python多线程详解

关于Python多线程的详细解读

ifhuke的博客 2万+

CANoe LIN一致性测试之干扰测试Test moudle_LIN Disturbance Block

注意:相邻两个显隐性不同的bit不允许干扰,例如我同时选择bit6和bit7,相邻两个同显隐性的bit允许干扰,例如当我选择bit3和bit4,项,这里我选择ID:为0x2F(PID:0x6F)的frame。需按照具体测试规范实施干扰项,帧头和数据段都可以实现干扰。最近发现一个干扰测试模块,下面是一些具体使用方法。选择干扰项:PID的奇偶校验位(一致性测试干扰测试之手动测试。一个待测试frameD。

yuanyikangkang的博客 3410

Python学习系列之多线程

一、多线程定义       进程是由若干线程组成的,一个进程至少有一个线程,叫主线程。 多线程类似于同时执行多个不同程序,多线程运行有如下优点: 使用线程可以把占据长时间的程序中的任务放到后台去处理,不会出现界面卡顿的情况。 用户界面更加友好,这样比如用户点击了一个按钮去触发某些事件的处理,可以弹出一个进度条来显示处理的进度。 程序的运行速度可能加快。 在一些等待的任务实现上如用户输入、...

answer3lin的博客 1万+

医学图像分割评价指标详解:从Dice到MSD的实战解析

本文深入解析医学图像分割的核心评价指标,从最常用的Dice相似系数到衡量边界精度的MSD(最大对称表面距离),通过实战案例与代码演示,系统阐述各指标的计算原理、应用场景及临床意义,帮助读者全面评估模型性能。

weixin_29070721的博客 593

Unity地形数据高效转换:从Terrain到3DMAX的OBJ导出实战

本文详细解析了Unity地形数据无法直接导入3DMAX的原因,并提供了高效转换的实战方案。通过编写自定义编辑器脚本,将Unity Terrain的高度图数据“烘焙”为静态网格模型,并以OBJ格式导出。文章重点讲解了核心代码逻辑、分辨率选择对模型精度的影响,以及在3DMAX中导入和优化模型的实用技巧,旨在打通Unity与3DMAX之间的数据协作壁垒。

sky77的博客 447

Python教程:一文掌握Python多线程(很详细)

多线程是指在同一进程内同时运行多个线程,每个线程执行不同的任务,实现并发执行。每个线程都有自己的执行路径,可以独立运行和调度,共享进程的资源。

旦莫的博客 3万+

从“破解”到应用:三菱数控系统M80协议逆向分析与工业数据采集网关搭建指南

本文深入解析三菱数控系统M80协议的逆向分析方法与工业数据采集网关搭建技术。通过TCP协议实现跨平台通信,详细介绍了M80协议的数据包结构、多语言实现方案(C#、Java、C/C++)以及Linux数据采集网关的架构设计与容器化部署。为工业自动化领域的开发者提供从协议分析到企业级数据采集落地的完整解决方案。

weixin_29228781的博客 1211

一文吃透python多线程(全面总结)

python多线程,包含创建、线程锁、线程通信、线程池等讲解

qq_43745578的博客 2万+

RH850系列芯片深度剖析 1.5-链接(ld)文件解析

基础编译过程分为四个阶段:预处理(Preprocessing)、编译(Compilation)、汇编(Assembly)、链接(Linking)。执行这四个阶段的程序(预处理器、编译器、汇编器、和链接器)一起构成了编译系统。CONSTANTS接下来的内存空间定义部分会使用这些常量来定义内存空间的范围,理论上来说不定义常量也没有影响,直接用魔鬼数字代替,定义常量的主要目的就是方便理解以及方便修改;从示例中可以看出,常量的值可以为16进制或者10十进制,而且10进制支持数量级单位(k级,M级);

本博客专注于嵌入式软件领域技术分享。 1427

7.1华为OD机试真题 新系统 - 仓库盘点 (Java/Py/C/C++/Js/Go)

7.1华为OD机试真题 新系统 - 仓库盘点 (Java/Py/C/C++/Js/Go)

算法大师! 505

如何入门 Python 爬虫?人人都能学会

文章目录前言一、网络爬虫的概念1.1 网络爬虫与浏览器的区别:1.2 网络爬虫的定义及作用二、爬虫的基本流程2.1 发起请求2.2 获取响应内容2.3 解析内容2.4 保存数据三、Python爬虫学习的10个步骤四、180分钟学会爬虫入门(含实例) 前言 从爬虫基础概念入手开始讲,再说爬虫学习的步骤及知识点,最后送大家180分钟的爬虫实战教程。 初学Python爬虫要搞清楚的问题: 网络哦按差的定义及其作用 爬虫的基本流程有哪些 Python爬虫学习的几个步骤 一、网络爬虫的概念 1.1 网络爬虫.

黑马程序员官方博客 1975

OpenStack详解、排错、经验总结

第1章 Openstack简介 1.1 计算(Compute):Nova。 一套控制器,用于为单个用户或使用群组管理虚拟机实例的整个生命周期,根据用户需求来提供虚拟服务。负责虚拟机创建、开机、关机、挂起、暂停、调整、迁移、重启、销毁等操作,配置CPU、内存等信息规格。自Austin版本集成到项目中。 1.2 对象存储(Object Storage):Swift。 一套用于在大规模可扩展系统中通过内置冗余及高容错机制实现对象存储的系统,允许进行存储或者检索文件。可为Glance提供镜像存储,为Cinder提供

weixin_40548182的博客 1974

想让技术快速变现,学会Python爬虫才是王道!

相信看到这篇文章的朋友们,都是奔着赚钱来的。当初我也是冲着爬虫接单可以赚钱才学的Python爬虫,确实可以接单赚钱。

DEVELOPERAA的博客 403

VB摄像头监控

该VB模块可对摄像头捕获的画面进行录像等,可在创建的窗口进行控制

TMS_WebGMaps_1.9.8.0_Full_Source

TMS_WebGMaps_1.9.8.0_Full_Source 全源码 delphi cbuilder

你评论,我赠书~【哈士奇赠书 - 14期】-〖人人都离不开的算法-图解算法应用〗参与评论,即可有机获得

无论是否愿意,我们的生活已被算法包围。为了帮助大家全面认知我们当前所身处的世界,消弭技术发展过快带来的困扰与隐忧,《人人都离不开的算法——图解算法应用》一方面从人工智能算法的五大核心应用领域—公共、商业、医疗、工业、金融的典型场景出发,以通俗化、故事化和漫画化的具体事例,深入解读算法是如何在各行各业具体发挥作用和对日常生活的影响;另一方面,将从算法的责任监管和立法治理等角度,阐述算法开发与应用者们应该如何守好伦理底线,让科技向善而行。

易编橙 · 终身成长社群,相遇已是上上签! 3429

精雕软件(JDpaint) 5.5 绿色版

精雕软件(JDpaint) 5.5 绿色版精雕软件(JDpaint) 5.5 绿色版精雕软件(JDpaint) 5.5 绿色版

上一篇: Python大作业:用Python写了一个水果忍者小游戏
下一篇: 【Python爬虫框架】这5个Python爬虫框架你用过几个?最后一个秒杀全部
大模型应用场景
博客等级 码龄5年 7629粉丝 1350原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值