python爬虫学习(三十四天)

hello,小伙伴们!我是喔的嘛呀。今天我们来学习多线程方面的知识(下篇)。

一、 多线程爬虫的概念

二、Python中的多线程实现

三、线程安全和数据共享

四、多线程与I/O密集型任务

五、队列(Queue)在多线程中的应用

六、多线程与多进程、多协程的对比

七、总结

四、多线程与I/O密集型任务

概念

  1. 多线程:多线程允许操作系统并发地执行多个线程。每个线程都是程序执行流的一个单元,它有自己的执行栈和程序计数器。多线程可以提高程序的吞吐量,特别是在I/O密集型任务中。
  2. I/O密集型任务:I/O密集型任务是指那些大部分时间都在等待I/O操作(如文件读写、网络通信等)完成的任务。由于I/O操作通常比CPU计算要慢得多,因此多线程可以有效地利用CPU等待I/O操作完成的时间来执行其他任务。
  3. 并发与并行:虽然这两个术语经常一起出现,但它们有不同的含义。并发是指多个任务在同一时间段内交替执行,而并行是指多个任务在同一时刻同时执行。在多线程中,我们可以实现并发,但在多核CPU上,也可以实现并行。
  4. 线程同步:当多个线程需要访问共享资源时,我们需要确保一次只有一个线程可以访问该资源,以避免数据不一致或其他问题。这通常通过使用锁、条件变量等同步原语来实现。

代码示例

下面是一个使用Python的threading模块来演示多线程处理I/O密集型任务的简单示例。我们将模拟从多个URL下载数据的场景。

import threading  
import time  
import requests  
  
# 模拟从URL下载数据的函数  
def download_data(url, result_dict):  
    # 模拟下载延迟  
    time.sleep(2)  # 假设每个下载需要2秒  
      
    # 使用requests库模拟下载数据(这里只是返回URL作为示例数据)  
    data = requests.get(url).text  # 注意:实际使用时需要处理异常  
      
    # 将下载的数据存储到字典中,键为URL  
    result_dict[url] = data[:10]  # 只存储前10个字符作为示例  
  
# 主函数  
def main():  
    urls = [  
        '<https://example.com/data1>',  
        '<https://example.com/data2>',  
        '<https://example.com/data3>',  
        # ...更多URL  
    ]  
      
    result_dict = {}  # 存储下载结果的字典  
    threads = []  # 存储所有线程的列表  
      
    # 为每个URL创建一个线程  
    for url in urls:  
        t = threading.Thread(target=download_data, args=(url, result_dict))  
        t.start()  
        threads.append(t)  
      
    # 等待所有线程完成  
    for t in threads:  
        t.join()  
      
    # 输出结果  
    for url, data in result_dict.items():  
        print(f"Downloaded from {url}: {data}")  
  
if __name__ == "__main__":  
    main()

注意事项

  1. 异常处理:在上面的示例中,我们没有处理可能出现的异常(如网络连接问题)。在实际应用中,你应该添加适当的异常处理代码来确保程序的健壮性。
  2. 线程安全:在这个示例中,我们使用了一个字典来存储下载结果。由于字典的读写操作在Python中通常是线程安全的(在CPython实现中,由于全局解释器锁GIL的存在),因此我们不需要额外的同步机制。但是,如果我们在处理更复杂的共享资源时,请务必注意线程安全问题。
  3. 性能考虑:虽然多线程可以提高I/O密集型任务的吞吐量,但在某些情况下,使用异步I/O(如Python的asyncio库)可能会获得更好的性能。异步I/O允许在等待I/O操作完成时释放线程,从而进一步提高CPU的利用率。
  4. 资源限制:创建过多的线程可能会耗尽系统资源(如内存和CPU时间)。因此,在设计多线程程序时,我们需要考虑系统的资源限制,并合理控制线程的数量。

五、队列(Queue)在多线程中的应用

队列(Queue)在多线程编程中扮演着至关重要的角色,特别是在生产者和消费者模式的实现中。队列允许线程安全地存储和检索数据,确保数据在多个线程之间的传递是有序和可靠的。

队列在多线程中的应用场景

  1. 生产者和消费者模式:在多线程应用中,一个或多个生产者线程生成数据,一个或多个消费者线程消费这些数据。队列用于在生产者和消费者之间传递数据。生产者将数据放入队列,消费者从队列中取出数据。
  2. 任务调度:在多线程任务处理系统中,可以使用队列来调度任务。系统可以将待处理的任务放入队列,然后由工作线程从队列中取出任务并执行。
  3. 数据缓冲:在某些情况下,数据的生成速度和消费速度可能不匹配。队列可以作为一个缓冲区,暂时存储生成的数据,直到消费者线程准备好处理它们。

Python中的线程安全队列

Python的queue模块提供了多种线程安全的队列类,如QueueLifoQueue(后进先出队列)和PriorityQueue(优先队列)。这些队列类在内部使用了锁或其他同步机制来确保线程安全。

示例代码:使用队列实现生产者和消费者模式

下面是一个简单的Python示例,演示了如何使用queue.Queue来实现生产者和消费者模式。

import threading  
import queue  
import time  
import random  
  
# 生产者线程  
def producer(queue, event):  
    for i in range(10):  
        time.sleep(random.random())  # 模拟生产数据的延迟  
        item = f"Item {i}"  
        print(f"Producer produced {item}")  
        queue.put(item)  # 将数据放入队列  
    event.set()  # 通知所有消费者数据生产完毕  
  
# 消费者线程  
def consumer(queue, event):  
    while not event.is_set() or not queue.empty():  
        item = queue.get()  # 从队列中取出数据  
        if item is None:  
            continue  # 如果是None,则继续等待下一个循环  
        time.sleep(random.random())  # 模拟消费数据的延迟  
        print(f"Consumer consumed {item}")  
        queue.task_done()  # 通知队列一个任务已经完成  
  
# 主函数  
def main():  
    q = queue.Queue()  
    done = threading.Event()  
  
    # 创建生产者线程  
    for i in range(2):  
        t = threading.Thread(target=producer, args=(q, done))  
        t.start()  
  
    # 创建消费者线程  
    for i in range(3):  
        t = threading.Thread(target=consumer, args=(q, done))  
        t.start()  
  
    # 等待所有生产者线程完成  
    done.wait()  
  
    # 发送一个None信号给消费者线程,表示没有更多的数据了  
    for _ in range(3):  
        q.put(None)  
  
    # 等待所有任务完成  
    q.join()  
  
if __name__ == "__main__":  
    main()

在这个示例中,我们创建了两个生产者线程和三个消费者线程。生产者线程将数据放入队列,消费者线程从队列中取出数据并处理。我们使用threading.Event来通知消费者线程何时停止等待新的数据。最后,我们通过向队列中添加None来通知消费者线程没有更多的数据了,并使用queue.join()来等待所有任务完成。

以下是Python queue模块中提供的几种队列类型及其特点的简要说明:

FIFO队列(Queue)

  • 特点:元素按照它们被添加到队列中的顺序(先进先出,FIFO)进行移除。
  • 应用场景:当需要在多线程之间按照特定的顺序传递数据时,可以使用FIFO队列。例如,一个生产者线程生成数据并将其放入队列,多个消费者线程从队列中取出并处理数据。

LIFO队列(LifoQueue)

  • 特点:与栈类似,后添加的元素会先被移除(后进先出,LIFO)。
  • 应用场景:当需要在多线程环境中实现类似栈的行为时,可以使用LIFO队列。虽然这种情况在多线程编程中相对较少见,但LIFO队列仍然有其特定的应用场景。

优先级队列(PriorityQueue)

  • 特点:元素按照它们的优先级进行移除,而不是按照它们被添加到队列中的顺序。元素的优先级通常在将元素添加到队列时指定。
  • 应用场景:当需要在多线程环境中按照优先级处理任务时,可以使用优先级队列。例如,一个线程可以将不同优先级的任务放入队列,另一个线程可以从队列中取出并处理优先级最高的任务。

六、多线程与多进程、多协程的对比

多线程、多进程和多协程都是并发编程中的概念,它们各自有其优缺点和适用场景。下面是对这三种并发模型的对比:

多线程(Multi-threading)

定义

多线程允许一个进程内存在多个线程,这些线程共享进程的资源(如内存空间、文件句柄等),但每个线程有自己的执行栈和线程局部存储。

优点

  • 线程间切换开销小,因为线程共享进程的资源,无需进行额外的内存分配和回收。
  • 通信方便,可以通过共享内存直接访问进程内的数据。
  • 适用于I/O密集型任务,因为线程在等待I/O操作时可以释放CPU给其他线程使用。

缺点

  • 线程间同步复杂,需要额外的同步机制来避免数据竞争和死锁。
  • 不适用于CPU密集型任务,因为多线程并不能提高整体的计算速度(受限于物理CPU核数)。
  • 全局解释器锁(GIL,在Python中)会限制多线程的并行性。

多进程(Multi-processing)

定义

多进程是指操作系统中同时运行多个进程,每个进程有自己的独立内存空间和系统资源。

优点

  • 进程间相互独立,不存在数据竞争问题,无需同步机制。
  • 适用于CPU密集型任务,因为可以充分利用多核CPU的计算能力。
  • 可以避免全局解释器锁的限制(如Python的GIL)。

缺点

  • 进程间切换开销大,因为需要切换不同的内存空间和系统资源。
  • 进程间通信复杂,通常需要使用IPC(进程间通信)机制,如管道、消息队列、套接字等。
  • 需要更多的系统资源来管理多个进程。

多协程(Multi-coroutine)

定义

协程(Coroutine)是一种用户态的轻量级线程,它的调度完全由用户控制。协程之间可以通过协作的方式共享一个线程的执行权,从而实现并发执行。

优点

  • 极高的并发性能,因为协程的切换开销极小,几乎可以忽略不计。
  • 无需线程上下文切换,减少CPU占用和延迟。
  • 可以轻松地实现非阻塞I/O操作,提高I/O密集型任务的性能。

缺点

  • 协程的调度需要用户自行控制,增加了编程的复杂性。
  • 由于协程是基于单线程的,因此无法充分利用多核CPU的计算能力。
  • 协程的适用场景相对有限,主要适用于I/O密集型任务。

代码示例:

多线程示例

使用Python的threading模块实现多线程:

import threading  
  
def worker(num):  
    """线程工作函数"""  
    print(f"Worker {num} is working...")  
  
# 创建线程  
threads = []  
for i in range(5):  
    t = threading.Thread(target=worker, args=(i,))  
    threads.append(t)  
    t.start()  
  
# 等待所有线程完成  
for t in threads:  
    t.join()  
  
print("All threads have finished.")

多进程示例

使用Python的multiprocessing模块实现多进程:

import multiprocessing  
  
def worker(num):  
    """进程工作函数"""  
    print(f"Worker {num} is working...")  
  
if __name__ == "__main__":  
    # 创建进程  
    processes = []  
    for i in range(5):  
        p = multiprocessing.Process(target=worker, args=(i,))  
        processes.append(p)  
        p.start()  
  
    # 等待所有进程完成  
    for p in processes:  
        p.join()  
  
    print("All processes have finished.")

多协程示例

在Python中,可以使用asyncio模块实现协程(尽管协程与传统的线程和进程有所不同,但它们通常被认为是并发模型的一种)。

import asyncio  
  
async def worker(num):  
    """协程工作函数"""  
    print(f"Worker {num} is working...")  
    await asyncio.sleep(1)  # 模拟I/O操作  
    print(f"Worker {num} has finished.")  
  
async def main():  
    # 创建并运行协程  
    tasks = [asyncio.create_task(worker(i)) for i in range(5)]  
    await asyncio.gather(*tasks)  
  
# 运行主协程  
asyncio.run(main())

在上面的协程示例中,我们使用asyncio.create_task()函数来创建协程任务,并使用asyncio.gather()函数来等待所有协程任务完成。await asyncio.sleep(1)用于模拟一个异步的I/O操作,这是协程的一个常见用途。最后,我们使用asyncio.run()函数来运行主协程。

总结

  • 多线程适用于I/O密集型任务,因为线程间切换开销小且可以通过共享内存直接访问数据。但对于CPU密集型任务,多线程可能无法提供明显的性能提升。
  • 多进程适用于CPU密集型任务,因为可以充分利用多核CPU的计算能力。但进程间切换开销大且通信复杂。
  • 多协程具有极高的并发性能,适用于I/O密集型任务。但由于基于单线程,无法充分利用多核CPU。此外,协程的调度需要用户自行控制,增加了编程的复杂性。

在选择使用哪种并发模型时,需要根据具体的任务类型和性能需求进行权衡。

七、总结

  1. 线程间的通信和同步:线程之间需要有效地进行通信和同步,以避免数据竞争、死锁和其他并发问题。Python中的threading模块提供了多种同步原语,如锁(Lock)、条件变量(Condition)、信号量(Semaphore)和事件(Event),来帮助实现线程间的协调。
  2. 共享资源的访问:多线程爬虫通常会共享一些资源,如URL队列、已爬取URL集合、数据存储等。需要确保这些资源的访问是线程安全的,避免数据不一致或混乱。
  3. 异常处理:在多线程环境中,异常处理变得更加复杂。由于一个线程的异常可能会影响到整个程序,因此需要在每个线程中都进行充分的异常处理,并考虑线程间的异常传播和协调。
  4. 性能调优:多线程虽然可以提高效率,但也会带来额外的开销,如线程的创建和销毁、线程间的切换等。需要根据实际情况调整线程的数量和调度策略,以达到最佳的性能。
  5. 选择合适的多线程策略:不同的应用场景和需求可能需要不同的多线程策略。例如,对于I/O密集型任务,可以使用更多的线程来充分利用CPU的空闲时间;而对于CPU密集型任务,则需要根据CPU的核数来合理设置线程数量。
  6. 遵守网站的爬虫策略:在编写爬虫时,一定要遵守目标网站的爬虫策略(Robots协议),不要对网站造成过大的压力或干扰。
  7. 法律和道德问题:在爬取数据时,需要确保自己的行为符合法律和道德要求,不要侵犯他人的隐私或知识产权。

总之,多线程爬虫技术是一个强大的工具,但也需要谨慎使用。通过合理的线程管理、资源访问控制和异常处理,可以充分发挥多线程的优势,提高数据爬取的效率和速度。

好了今天的学习就到这里啦,我们明天再见啦!拜拜啦!

  • 33
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

喔的嘛呀

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值