多任务爬虫(多线程和多进程)

        在一台计算机中,我们可以同时打开多个软件,例如同时浏览网页、听音乐、打字等,这是再正常不过的事情。但仔细想想,为什么计算机可以同时运行这么多软件呢? 这就涉及计算机中的两个名词:多进程和多线程。

        同样,在编写爬虫程序的时候,为了提高爬取效率,我们可能会同时运行多个爬虫任务,其中同样涉及多进程和多线程。

目录

一、多线程的概念

二、并发和并行

三、多线程的应用场景 

四、Python多线程用途

五、threading模块

六、多线程共享全局变量的问题

(1)锁机制和threading.Lock类

(2)Condition锁

(3)Queue队列 


一、多线程的概念

        说起多线程,就不得不先说什么是线程。说起线程,又不得不先说什么是进程。

        进程可以理解为一个可以独立运行的程序单位,例如打开一个浏览器,就开启了一个浏览器进程;打开一个文本编辑器,就开启了一个文本编辑器进程。在一个进程中,可以同时处理很多事情,例如在浏览器进程中,可以在多个选项卡中打开多个页面,有的页面播放音乐,有的页面播放视频,有的网页播放动画,这些任务可以同时运行,互不干扰。为什么能做到同时运行这么多任务呢? 这便引出了线程的概念,其实一个任务就对应一个线程。

        进程就是线程的集合,进程是由一个或多个线程构成的,线程是操作系统进行运算调度的最小单位,是进程中的最小运行单元。以上面说的浏览器进程为例,其中的播放音乐就是一个线程,播放视频也是一个线程。当然,浏览器进程中还有很多其他线程在同时运行,这些线程并发或并行执行使得整个浏览器可以同时运行多个任务。

        了解了线程的概念,多线程就很容易理解了。多线程就是一个进程中同时执行多个线程,上面的浏览器进程就是典型的多线程。

二、并发和并行

        我们知道,在计算机中运行一个程序,底层是通过处理器运行一条条指令来实现的。

        处理器同一时刻只能执行一条指令,并发(concurrency)是指多个线程对应的多条指令被快速轮换地执行。例如一个处理器,它先执行线程A的指令一段时间,再执行线程B的指令一段时间,然后再切回线程A 执行一段时间。处理器执行指令的速度和切换线程的速度都非常快,人完全感知不到计算机在这个过程中还切换了多个线程的上下文,这使得多个线程从宏观上看起来是同时在运行。从微观上看,处理器连续不断地在多个线程之间切换和执行,每个线程的执行都一定会占用这个处理器的一个时间片段,因此同一时刻其实只有一个线程被执行。

        并行(parallel)指同一时刻有多条指令在多个处理器上同时执行,这意味着并行必须依赖多个处理器。不论是从宏观还是微观上看,多个线程都是在同一时刻一起执行的。

        并行只能存在于多处理器系统中,因此如果计算机处理器只有一个核,就不可能实现并行。而并发在单处理器和多处理器系统中都可以存在,因为仅靠一个核,就可以实现并发。例如,系统处理器需要同时运行多个线程。如果系统处理器只有一个核,那它只能通过并发的方式来运行这些线程。而如果系统处理器有多个核,那么在一个核执行一个线程的同时,另一个核可以执行另一个线程,这样这两个线程就实现了并行执行。当然,其他线程也可能和另外的线程在同一个核上执行,它们之间就是并发执行。具体的执行方式,取决于操作系统如何调度。

三、多线程的应用场景 

        在一个程序的进程中,有一些操作是比较耗时或者需要等待的,例如等待数据库查询结果的返回、等待网页的响应。这时如果使用单线程,处理器必须等这些操作完成之后才能继续执行其他操作,但在这个等待的过程中,处理器明显可以去执行其他操作。如果使用多线程,处理器就可以在某个线程处于等待态的时候,去执行其他线程,从而提高整体的执行效率。

        很多情况和上述场景一样,线程在执行过程中需要等待。网络爬虫就是一个非常典型的例子,爬虫在向服务器发起请求之后,有一段时间必须等待服务器返回响应,这种任务就属于IO密集型任务。对于这种任务,如果我们启用多线程,那么处理器就可以在某个线程等待的时候去处理其他线程,从而提高整体的爬取效率。

        但并不是所有任务都属于IO密集型任务,还有一种任务叫作计算密集型任务,也可以称为CPU密集型任务。顾名思义,就是任务的运行一直需要处理器的参与。假设我们开启了多线程,处理器从一个计算密集型任务切换到另一个计算密集型任务,那么处理器将不会停下来,而是始终忙于计算,这样并不会节省整体的时间,因为需要处理的任务的计算总量是不变的。此时要是线程数目过多,反而还会在线程切换的过程中耗费更多时间,使得整体效率变低。

        综上所述,如果任务不全是计算密集型任务,就可以使用多线程来提高程序整体的执行效率。尤其对于网络爬虫这种 IO密集型任务,使用多线程能够大大提高程序整体的爬取效率。

四、Python多线程用途

        Python自带的解释器是Cpython,并不支持真正意义上的多线程。Cpython提供了多线程包,包含一个叫Global Interpreter Lock(GIL)锁,它能确保你的代码中永远只有一个线程在执行。经过GL的处理,会增加执行的开销。这就意味着如果你先要提高代码执行效率,使用threading不是一个明智的选择,当然如果你的代码是IO密集型,比如爬虫,多线程可以明显提高效率,相反如果你的代码是CPU密集型,比如大量计算类型,这种情况下多线程反而没有优势,建议使用多进程。 

五、threading模块

        threading模块是python中专门提供用来做多线程编程的模块。thrcading模块中最常用的类是Thread。

        1.用thrcading模块直接写一个多线程程序
        2.threading模块下的Thrcad类,继承自这个类,然后实现run方法,线程就会自动运行run方法中的代码 

代码实例:

# 多线程案例1
import threading
import time


def singing(name,delay):
    print(f'{name}开始唱歌')
    time.sleep(delay)
    print('结束唱歌')

def dacning(name,delay):
    print(f'{name}开始跳舞')
    time.sleep(delay)
    print('结束跳舞')

def single_thread():
    singing('学友',2)
    dacning('潘潘',3)

def multi_thread():
    task = []
    th1 = threading.Thread(target=singing,args=('学友',2))
    th1.start()
    for i in range(3):
        th2 = threading.Thread(target=dacning,args=('潘潘',3))
        th2.start()
        task.append(th2)

    task.append(th1)
    for t in task:
        t.join()


if __name__ == '__main__':
    start_time = time.time()
    # single_thread()
    multi_thread()
    print(threading.enumerate())
    end_time = time.time()
    print(f'总共消耗时间:{end_time-start_time}')

结果如下:

学友开始唱歌
潘潘开始跳舞
潘潘开始跳舞
潘潘开始跳舞
结束唱歌
结束跳舞
结束跳舞
结束跳舞
[<_MainThread(MainThread, started 21676)>]
总共消耗时间:3.022017002105713

         这里使用了4个线程,一个线程用于打印唱歌并计时,三个线程用于打印跳舞并计时。最终消耗3秒多钟,因为使用最长时间的线程所花费的时间为3秒钟,其中还存在一些调用等之类的方法所耗费时间,一共加起来为3秒多种的时间。

六、多线程共享全局变量的问题

        问题:多线程都是在同一个进程中运行的。因此在进程中的全局变量所有线程都是可共享的。这就造成了一个问题,因为线程执行的顺序是无序的,有可能会造成数据错误。

解决方法:

(1)锁机制和threading.Lock类

        为了解决共享全局变量的问题。thrcading提供了一个Lock类,这个类可以在某个线程访问某个变量的时候加锁,其他线程此时就不能进来,直到当前线程处理完后,把锁释放了,其他线程才能进来处理。实例如下:

# 多线程共享全局变量
import threading
lock = threading.Lock()
a = 0
def add_value(num):
    global a
    lock.acquire()
    for i in range(num):
        a += 1
    lock.release()
    print(f'A的值是:{a}')

def main():
    for i in range(10):
        th = threading.Thread(target=add_value,args=(1000000,))
        th.start()

if __name__ == '__main__':
    main()

结果如下:

A的值是:1000000
A的值是:2000000
A的值是:3000000
A的值是:4000000
A的值是:5000000
A的值是:6000000
A的值是:7000000
A的值是:8000000
A的值是:9000000
A的值是:10000000

         如果不使用锁机制,则会出现A计算出的值不是按照1000000相加的得到的结果。

生产者和消费者模式

        生产者和消费者模式是多线程开发中经常见到的一种模式。生产者的线程专门用来生产一些数据,然后存放到一个中间的变量中。消费者再从这个中间的变量中取出数据进行消费。通过生产者和消费者模式,程序分工更加明确,线程更加方便管理。 

        生产者和消费者因为要使用中间变量,中间变量经常是一些全局变量,因此需要使用锁来保证数据完整性。下面案例使用threading.Lock来解决生产者和消费者问题:
# 多线程L4-生产者和消费者-Lock版
import threading
import random
import time

lock = threading.Lock()
cycle_time = 10
count = 0
total_money = 0
class Producer(threading.Thread):
    def run(self) -> None:
        global total_money,cycle_time,count
        while True:
            lock.acquire()
            if count > cycle_time:
                print('生产者已经完成工作了')
                lock.release()
                break
            money = random.randint(100,5000)
            total_money += money
            count += 1
            print(f'{threading.current_thread().name}赚了{money}元')
            lock.release()
            time.sleep(0.5)

class Consumer(threading.Thread):
    def run(self) -> None:
        global total_money,count
        while True:
            lock.acquire()
            money = random.randint(100,5000)
            if total_money >= money:
                total_money -= money
                print(f'{threading.current_thread().name}消费了{money}元')
            else:
                if count > cycle_time:
                    print(f'{threading.current_thread().name}想消费{money}元,但是余额不足,并且生产者不再生产了')
                    lock.release()
                    break
                print(f'{threading.current_thread().name}想消费{money}元,但是余额不足,只有{total_money}')
            lock.release()
            time.sleep(0.5)

def main():
    for i in range(5):
        th1 = Producer(name=f'生产者{i}号')
        th1.start()

    for t in range(5):
        th2 = Consumer(name=f'消费者{t}号')
        th2.start()

if __name__ == '__main__':
    main()

结果如下:

生产者0号赚了2243元
生产者1号赚了1043元
生产者2号赚了1706元
生产者3号赚了3515元
生产者4号赚了1278元
消费者0号消费了4066元
消费者1号消费了4953元
消费者2号想消费3077元,但是余额不足,只有766
消费者3号想消费1452元,但是余额不足,只有766
消费者4号想消费4889元,但是余额不足,只有766
生产者2号赚了2290元
生产者1号赚了460元
生产者4号赚了2376元
生产者3号赚了3128元
生产者0号赚了4567元
消费者4号消费了1347元
消费者3号消费了4373元
消费者2号消费了3644元
消费者0号消费了2307元
消费者1号想消费3447元,但是余额不足,只有1916
生产者2号赚了1046元
生产者已经完成工作了
生产者已经完成工作了
生产者已经完成工作了
生产者已经完成工作了
消费者1号消费了2568元
消费者0号想消费2265元,但是余额不足,并且生产者不再生产了
消费者2号消费了235元
消费者3号想消费4350元,但是余额不足,并且生产者不再生产了
消费者4号想消费1287元,但是余额不足,并且生产者不再生产了
生产者已经完成工作了
消费者2号想消费3451元,但是余额不足,并且生产者不再生产了
消费者1号想消费3857元,但是余额不足,并且生产者不再生产了

(2)Condition锁

         threading.Condition可以在没有数据的时候处于阻塞等待状态。 一旦有合适的数据了,还可

以使用notify相关的函数来通知其他处于等待状态的线程。这样就可以不用做一些无用的上

锁和解锁的操作。可以提高程序的性能。

常用函数如下:

  • acquire:   上锁。
  • release:   解锁。
  • wait:  将当前线程处于等待状态,并且会释放锁。可以被其他线程使用notify和notify_all函数

        唤醒。被唤醒后会继续等待上锁,上锁后继续执行下面的代码。

  • notify:   通知某个正在等待的线程,默认是第1个等待的线程。
  • notify_all:   通知所有正在等待的线程。notify 和notfy_all 不会释放锁。并且需要在release之前

        调用。 

下面案例使用  threading.Condition来解决生产者和消费者问题:

# 多线程L5-生产者和消费者-Condition版

import threading
import random
import time

lock = threading.Condition()
cycle_time = 10
count = 0
total_money = 0


class Producer(threading.Thread):
    def run(self) -> None:
        global total_money,cycle_time,count
        while True:
            lock.acquire()
            if count > cycle_time:
                print('生产者已经完成工作了')
                lock.release()
                break
            money = random.randint(100,5000)
            total_money += money
            lock.notify_all()
            count += 1
            print(f'{threading.current_thread().name}赚了{money}元')
            lock.release()
            time.sleep(0.5)


class Consumer(threading.Thread):
    def run(self) -> None:
        global total_money,count
        while True:
            lock.acquire()
            money = random.randint(100,5000)
            while total_money < money:
                if count > cycle_time:
                    print(f'{threading.current_thread().name}想消费{money}元,但是余额不足,并且生产者不再生产了')
                    lock.release()
                    return
                
                lock.wait()
            total_money -= money
            print(f'{threading.current_thread().name}消费了{money}元')

            lock.release()
            time.sleep(0.5)


def main():
    for i in range(5):
        th1 = Producer(name=f'生产者{i}号')
        th1.start()

    for t in range(5):
        th2 = Consumer(name=f'消费者{t}号')
        th2.start()


if __name__ == '__main__':
    main()

结果如下: 

生产者0号赚了1782元
生产者1号赚了4000元
生产者2号赚了174元
生产者3号赚了4387元
生产者4号赚了4438元
消费者0号消费了4504元
消费者1号消费了4909元
消费者2号消费了3572元
生产者3号赚了2507元
生产者0号赚了391元
消费者4号消费了3385元
生产者2号赚了3058元
生产者4号赚了3922元
消费者2号消费了3263元
消费者0号消费了4646元
生产者1号赚了4092元
消费者1号消费了549元
消费者3号消费了2709元
生产者3号赚了1972元
生产者已经完成工作了
消费者2号消费了1771元
生产者已经完成工作了
消费者4号想消费3128元,但是余额不足,并且生产者不再生产了
生产者已经完成工作了
消费者0号想消费3515元,但是余额不足,并且生产者不再生产了
消费者3号想消费2024元,但是余额不足,并且生产者不再生产了
消费者1号消费了863元
生产者已经完成工作了
生产者已经完成工作了
消费者2号想消费1832元,但是余额不足,并且生产者不再生产了
消费者1号想消费2619元,但是余额不足,并且生产者不再生产了

(3)Queue队列 

         Queue是python标准库中的线程安全的队列(FIFO)实现,提供了一个适用于多线程编程的先进先出的数据结构。队列可以完美解决线程间的数据交换,保证线程间数据的安全性和一致性。
初始化Queue(maxsize):创建一个先进先出的队列。

  • qsize():返回队列的大小。
  • empty():判断队列是否为空。
  • full():判断队列是否满了。
  • get():从队列中取最后一个数据。
  • put():将一个数据放到队列中。

        下面这篇帖子就是采取队列的方式来防止乱序问题的出现:

Python高级进阶--多线程爬取下载小说(基于笔趣阁的爬虫程序)_python多线程爬取小说-CSDN博客

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/405256.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

QT_day4

1.思维导图 2. 输入闹钟时间格式是小时:分钟 widget.cpp #include "widget.h" #include "ui_widget.h"Widget::Widget(QWidget *parent): QWidget(parent), ui(new Ui::Widget) {ui->setupUi(this);id startTimer(1000);flag1;speecher new QTextT…

如何搭建Facebook直播网络?

在当今数字化时代&#xff0c;Facebook直播已经成为了一种极具吸引力的社交形式&#xff0c;为个人和企业提供了与观众直接互动的机会&#xff0c;成为推广产品、分享经验、建立品牌形象的重要途径。然而&#xff0c;对于许多人来说&#xff0c;搭建一个稳定、高质量的 Faceboo…

前端(vue)数据存储方案

引言 本需求文档旨在明确前端项目中的数据存储需求&#xff0c;包括数据类型、数据结构、数据交互方式等。它定义了前端项目中需要存储和处理的数据&#xff0c;以及对这些数据进行访问和操作的要求。 功能需求 数据存储按数据类型分为 持久存储、内存存储&#xff08;响应式…

【网络安全 | 网络协议】一文讲清HTTP协议

HTTP概念简述 HTTP&#xff08;Hypertext Transfer Protocol&#xff09;协议&#xff0c;又称超文本传输协议&#xff0c;用于传输文本、图像、音频、视频以及其他多媒体文件。它是Web应用程序通信的基础&#xff0c;通过HTTP协议&#xff0c;Web浏览器可以向Web服务器发起请…

RabbitMQ监控方法以及核心指标

RabbitMQ监控方法以及核心指标 1. 监控指标采集2. 使用rabbimq插件采集指标2.1 3.8.0之前版本&#xff0c;使用外部插件暴露2.2 3.8.0之后版本&#xff0c;使用内置插件暴露 3. 使用rabbitmq_exporter采集指标3.1 部署rabbitmq_exporter3.2 prometheus采集rabbitmq_exporter的暴…

二、基本语法

一、变量声明 1、语法 <变量名称>: <变量类型> <变量值> 2、变量类型 字符串&#xff1a;string 数值&#xff0c;整数、浮点数都可以&#xff1a;number 布尔&#xff1a;boolean 任意类型&#xff1a;any 联合类型&#xff0c;指定的多个类型中的…

springmvc+mybatis+springboot航空飞机订票售票系统_f48cp

互联网发展的越来越快了&#xff0c;在当下社会节点&#xff0c;人们也开始越来越依赖互联网。通过互联网信息和数据&#xff0c;极大地满足用户要求[5]。飞机订票系统使用了B/S模式&#xff0c;并且不需要安装第三方插件&#xff0c;他们甚至能直接在电脑上随机随地实现飞机订…

JavaScript运算符

文章目录 运算符介绍算术运算符递增和递减运算符比较运算符逻辑运算符短路运算逻辑与 逻辑或 赋值运算符运算符优先级 运算符介绍 算术运算符 %取余运算符的主要用途&#xff1a; 判断某个数是否能被某个数整除。 浮点数的精度问题&#xff1a; 所以&#xff1a;不要直接判断…

《一本书讲透 Elasticsearch》荣登当当人工智能新书榜

年前&#xff0c;《一本书讲透 Elasticsearch》荣登京东编程语言与程序设计榜前5名&#xff0c;今天又上榜当当人工智能新书榜第7名。 先看评价&#xff0c;看看大家阅后反馈 来自百度公司员工评价 来自Elastic原厂资深架构师评价 来自IBM资深架构师周钰老师的评价 来自2位阿里…

【Java程序员面试专栏 数据结构】一 高频面试算法题:数组

一轮的算法训练完成后,对相关的题目有了一个初步理解了,接下来进行专题训练,以下这些题目就是汇总的高频题目,本篇主要聊聊数组,包括数组合并,滑动窗口解决最长无重复子数组问题,图形法解下一个排列问题,以及一些常见的二维矩阵问题,所以放到一篇Blog中集中练习 题目…

鸿蒙开发市场憧憬如何?是否值得一冲~

自从华为放话&#xff1a;鸿蒙NEXT 不再支持安卓操作系统。换句话说&#xff0c;华为就是在向全世界宣布&#xff0c;华为官宣于2024年&#xff0c;不再支持安卓的AOSP。 大家也应该知道&#xff0c;谷歌暂停与华为的合作。为了生存&#xff0c;华为被迫突出了自研的鸿蒙操作系…

物麒平台自定义事件代码修改流程

是否需要申请加入数字音频系统研究开发交流答疑群(课题组)&#xff1f;可加我微信hezkz17, 本群提供音频技术答疑服务&#xff0c;群赠送蓝牙音频&#xff0c;DSP音频项目核心开发资料, 1 配置工具对应关系 2 事件处理 3 事件定义 4

滚雪球学Java(69):深入浅出Java中高效的ConcurrentLinkedQueue队列底层实现与源码分析

咦咦咦&#xff0c;各位小可爱&#xff0c;我是你们的好伙伴——bug菌&#xff0c;今天又来给大家普及Java SE相关知识点了&#xff0c;别躲起来啊&#xff0c;听我讲干货还不快点赞&#xff0c;赞多了我就有动力讲得更嗨啦&#xff01;所以呀&#xff0c;养成先点赞后阅读的好…

数据库面试题汇总,助你轻松应对面试!

考虑到最近有些小伙伴准备跳槽&#xff0c;所以更新一些数据库相关的面试题&#xff0c;希望能帮到大家&#xff01; 一 请写出创建表的基本语法结构&#xff1f; 创建表的基本语法结构如下&#xff1a; CREATE TABLE IF NOT EXISTS 表名(字段名1 字段类型,字段名2 字段类型 …

day08_面向对象-继承-课后练习 - 参考答案

文章目录 day08_课后练习代码阅读分析题第1题第2题第3题第4题第05题 代码编程题## 第1题第2题第3题第4题 day08_课后练习 代码阅读分析题 第1题 考核知识点&#xff1a;权限修饰符 如下代码是否可以编译通过&#xff0c;如果能&#xff0c;结果是什么&#xff0c;如果不能&…

计算机网络-网络互联与互联网(一)

1.常用网络互联设备&#xff1a; 1层物理层&#xff1a;中继器、集线器2层链路层&#xff1a;网桥、交换机3层网络层&#xff1a;路由器、三层交换机4层以上高层&#xff1a;网关 2.网络互联设备&#xff1a; 中继器Repeater、集线器Hub&#xff08;又叫多端口中继器&#xf…

基于PID控制器的直流电机位置控制系统simulink建模与仿真

目录 1.课题概述 2.系统仿真结果 3.核心程序与模型 4.系统原理简介 1. PID控制器原理 2. 位置控制环 5.完整工程文件 1.课题概述 基于PID控制器的直流电机位置控制系统。直流电机位置控制系统是工业自动化领域中的一个重要应用。为了实现精确的位置控制&#xff0c;常采…

2024 GMS(昆明)跨境电商交易博览会

2024 GMS&#xff08;昆明&#xff09;跨境电商交易博览会 招 展 手 册 立足大湄公河次区域&#xff0c;以云南为中心&#xff0c; 辐射南亚、东南亚的区域性国际跨境电商展会 展会背景&#xff1a; “十三五”以来&#xff0c;我国跨境电商行业在政策的推动下迎来了前所未…

通过platform总线驱动框架编写LED灯的驱动

通过platform总线驱动框架编写LED灯的驱动&#xff0c;编写应用程序测试 pdrv.c #include <linux/init.h> #include <linux/module.h> #include <linux/of_gpio.h> #include <linux/gpio.h> #include <linux/platform_device.h> #include <l…

【MATLAB源码-第143期】基于matlab的蝴蝶优化算法(BOA)机器人栅格路径规划,输出做短路径图和适应度曲线。

操作环境&#xff1a; MATLAB 2022a 1、算法描述 蝴蝶优化算法&#xff08;Butterfly Optimization Algorithm, BOA&#xff09;是基于蝴蝶觅食行为的一种新颖的群体智能算法。它通过模拟蝴蝶个体在寻找食物过程中的嗅觉导向行为以及随机飞行行为&#xff0c;来探索解空间&a…