建二手车网站,wordpress支持中文,wordpress移动下方的菜单,电子邮箱注册前言:
在了解本章之前#xff0c;我们先来了解下什么是线程和进程: 在计算机科学中#xff0c;进程和线程是执行程序的基本单元#xff0c;它们在操作系统的管理下运作#xff0c;但它们之间有着本质的区别。理解进程和线程的概念对于进行有效的程序设计和系统管理非常重要…前言:
在了解本章之前我们先来了解下什么是线程和进程: 在计算机科学中进程和线程是执行程序的基本单元它们在操作系统的管理下运作但它们之间有着本质的区别。理解进程和线程的概念对于进行有效的程序设计和系统管理非常重要。 进程(Process) 进程可以被理解为一个运行中的程序的实例。它是系统资源分配和执行的基本单位。每个进程都有自己独立的内存空间包括代码段、数据段和堆栈等进程间的通信IPCInter-process communication需要特定的机制如管道、消息队列、共享内存等来实现。 操作系统管理的任务比如执行一个应用程序通常都是在一个独立的进程中完成的。进程具有以下特性 独立性每个进程都有自己独立的地址空间和系统资源。 并发性多个进程可以同时运行在多核或单核CPU系统上。 隔离性一个进程崩溃通常不会影响到其他进程。 线程(Thread) 线程有时被称为轻量级进程是进程的执行单元。一个进程中可以包含一个或多个线程所有线程共享该进程的地址空间和资源但每个线程有自己的执行路径和状态比如程序计数器、寄存器和栈。线程之间的通信和数据共享更为容易因为它们共享相同的进程空间但这也意味着需要额外的注意来避免资源竞争和同步问题。 线程具有以下特性 轻量级创建和销毁线程比进程更快线程间的切换开销也更小。 共享数据线程之间可以直接访问相同的数据和资源这使得数据共享和通信更方便。 多线程一个单独的进程可以并发执行多个线程实现程序的并行处理。 进程与线程的对比 资源分配进程是资源分配的基本单位有独立的地址空间线程是CPU调度的基本单位是进程中的一个实体是比进程更小的能独立运行的基本单位线程自身基本上不拥有系统资源除了必要的少量资源外它与进程内的其他线程共享进程所拥有的全部资源。 通信方面由于线程共享相同的地址空间线程之间的通信相对简单但需要处理同步和互斥进程间通信则需要通过IPC机制。 独立性进程之间相互独立一个进程的崩溃不会影响到其他进程而线程之间共享进程资源一个线程的错误可能影响到整个进程的其他线程。 效率线程的创建、销毁和切换的开销小于进程。 概括来说进程和线程都是操作系统中的并发执行的单位但线程是进程的一部分二者在资源管理、通信机制、开销和设计上都有差异。理解这些差异对于设计高效、稳定的并发程序至关重要。
1. 多线程 (Threading)
多线程是操作系统能够在同一进程中并行处理多个任务的能力。在纯Python代码中由于全局解释器锁Global Interpreter Lock, GIL的存在同一时刻只允许一个线程执行Python字节码。因此在CPU密集型任务中Python的多线程并不会带来太大的性能提升但在I/O密集型任务中多线程可以在一个线程等待外部响应时允许其他线程执行这样可以提高程序的整体效率。
1.1 多线程例子
import threading
import timedef print_numbers():for i in range(1, 6):time.sleep(1)print(i)def print_letters():for letter in abcde:time.sleep(1.5)print(letter)# 创建线程
t1 threading.Thread(targetprint_numbers)
t2 threading.Thread(targetprint_letters)# 启动线程
t1.start()
t2.start()# 等待线程完成
t1.join()
t2.join()
start() 方法: 用于启动一个Thread线程实例。在创建Thread实例后线程并不会立即执行直到调用了它的start()方法该线程才真正被操作系统调度开始运行对应的target函数。
join() 方法: 用来等待线程结束的。当在某个线程A中调用另外一个线程B的join()方法时线程A将被阻塞直到线程B完成执行后才继续执行线程A之后的操作。join()方法对于控制程序流程和确保线程按期望执行完成非常有用。
1.2 多线程中的Queue:
在多线程编程中queue模块中的Queue类是一个线程安全的队列实现它提供了一种安全的方式来交换信息或数据使得在不同线程间通信变得简单而且不易出错。Queue的作用主要有以下几点 线程间的数据交换 Queue允许多个线程放入元素和取出元素这些操作内部是自动加锁的因此线程在这些操作中不会彼此干扰也不会造成数据结构的损坏。 任务调度 Queue常被用来分发任务其中一个线程通常称作生产者负责将任务放到队列中然后多个处理线程通常称作消费者可以同时从队列中取出任务并执行。 同步与顺序控制 Queue可以用来协调线程的执行例如可以用它来确保任务按照放入队列的顺序来处理或在pipeline中Queue可以用作各阶段中的缓存同步不同阶段的处理速度。 缓解生产者和消费者速度不匹配问题 如果生产者线程的生产速度快于消费者线程的处理速度那么Queue可以作为缓冲区暂存任务避免生产者直接阻塞等待。 资源池管理 Queue同时也可以用来管理资源池比如数据库连接池线程可以从队列中获取资源进行操作操作完成后再放回队列供其他线程使用。 Queue提供了多种方法如put(), get(), qsize(), empty(), full()以及join()和task_done()来支持其上述作用 put(item): 将item放入队列中。 get(): 从队列中移除并返回一个元素。若队列为空调用该方法的线程会被阻塞直到有元素可以返回。 qsize(): 返回队列中大致的元素数量由于多线程的原因这个数量可能不准确。 empty(): 检查队列是否为空。 full(): 检查队列是否已满。 join(): 阻塞调用线程直到队列中所有元素都被处理task_done() 被每个元素调用一次。 task_done(): 告诉队列之前排队的一个元素的处理已完成当队列中的所有元素都被处理完成后调用join()的线程才会被解锁继续执行。
1.2.1 多线程中带参数的使用和Queue的应用
import threading
import queue
import time# 线程要执行的函数计算平方并将结果放入队列
def calc_square(numbers, results_queue):for number in numbers:time.sleep(0.5) # 模拟耗时操作square number * numberresults_queue.put(square)print(fSquare of {number} is {square})# 创建一个queue来存放结果
results_queue queue.Queue()# 定义一组数字
numbers [2, 4, 6, 8]# 创建线程带参数
thread threading.Thread(targetcalc_square, args(numbers, results_queue))# 启动线程
thread.start()# 等待线程完成
thread.join()# 从队列中获取计算结果
while not results_queue.empty():square results_queue.get() # 通过queue.get获取计算结果print(fSquare result from queue: {square})输出
# Square of 2 is 4
# Square of 4 is 16
# Square of 6 is 36
# Square of 8 is 64
# Square result from queue: 4
# Square result from queue: 16
# Square result from queue: 36
# Square result from queue: 64在这个例子中我们定义了一个calc_square函数它接收一组数字和一个队列。对每个数字计算平方并将结果放入队列。我们通过args参数给线程传递了需要处理的数字列表和用于存储结果的队列。线程启动后会执行calc_square函数主线程通过join()等待线程完成。最后主线程从队列中取出并打印了平方计算的结果。
2. 多进程multiprocessing
多进程是指操作系统能够运行多个进程为每个进程分配独立的内存空间每个进程中可能有一个或多个线程。多进程可以绕过GIL的限制在Python中实现真正的并行计算尤其适合CPU密集型任务。
2.1 多进程例子
import multiprocessing
import timedef calculate_square(numbers):for n in numbers:time.sleep(0.5)print(Square:, n * n)def calculate_cube(numbers):for n in numbers:time.sleep(0.5)print(Cube:, n * n * n)# 多进程需要在if __name__ __main__ 中调用, 这是因为Windows没有fork()调用因此Python解释器在Windows上需要通过“引导bootstrapping”的方式来启动新的Python进程这意味着它会重新导入主模块
if __name__ __main__:numbers [2, 3, 4, 5]# 创建进程p1 multiprocessing.Process(targetcalculate_square, args(numbers,))p2 multiprocessing.Process(targetcalculate_cube, args(numbers,))# 启动进程p1.start()p2.start()# 等待进程完成p1.join()p2.join()# 输出
# Square: 4
# Cube: 8
# Square: 9
# Cube: 27
# Square: 16
# Cube: 64
# Square: 25
# Cube: 1252.2 多进程中Queue的使用
from multiprocessing import Process, Queue
import timedef worker(task_queue, result_queue):工作进程用于处理任务并将结果放入结果队列while not task_queue.empty():task task_queue.get()print(fProcess {task}...)processed_result task * task # 假设任务为计算数值的平方time.sleep(1) # 模拟工作负载result_queue.put(processed_result)if __name__ __main__:# 创建任务队列和结果队列task_queue Queue()result_queue Queue()# 填充任务队列tasks [2, 3, 5, 7, 11]for t in tasks:task_queue.put(t)# 创建并启动多个工作进程num_processes 3processes [Process(targetworker, args(task_queue, result_queue)) for _ in range(num_processes)]for p in processes:p.start()for p in processes:p.join() # 等待所有进程完成# 收集结果results []while not result_queue.empty():results.append(result_queue.get())print(fResults: {results})# 输出:
# Process 2...
# Process 3...
# Process 5...
# Process 7...
# Process 11...
# Results: [9, 4, 25, 121, 49]在这个示例中我们定义了一个工作进程函数worker它从task_queue中获取任务处理这些任务在这里是计算数值的平方然后将结果放入result_queue。task_queue和result_queue都是通过multiprocessing.Queue创建的队列它们可以在不同的进程间安全地传输Python对象。 我们首先将任务放入task_queue然后创建了几个工作进程并启动每个工作进程都接收task_queue和result_queue作为参数使用args参数传递。工作进程都完成后主进程将从result_queue中收集所有结果并打印出来。
3. 全局解释器锁GIL
全局解释器锁Global Interpreter Lock简称GIL是Python解释器中一个用来保护Python对象防止多个线程同时访问的机制。GIL确保同一时刻只有一个线程可以执行Python字节码即CPython的一部分因此即使在多核处理器上CPython的多线程程序也不能实现真正的并行执行至少不是在执行Python字节码时。
这个锁是Python的内部细节特别是在CPython解释器中因为这是目前最流行的Python实现。GIL并不是Python语言的固有特性而是CPython解释器的设计选择。一些其他的Python解释器比如Jython或IronPython因为它们依赖于Java Virtual MachineJVM或.NET的CLRCommon Language Runtime它们并没有像CPython那样的GIL。 GIL是为了简化CPython中多线程操作的复杂性而引入的因为内存管理并不是线程安全的。因为GIL的存在使得CPython的垃圾收集器尤其是引用计数这一部分不需要额外的同步机制从而在单线程情况下有更好的性能。 然而GIL也有不少缺点 多核处理器的利用率低在多核处理器上运行的多线程程序由于GIL的原因不能完全利用多核的优势来并行执行任务。 性能问题在多线程程序中频繁地获取和释放GIL会引起性能瓶颈这个过程中会产生额外的开销尤其是在线程数量较多时。 编程复杂性开发者要想在CPython中编写真正并行的多线程程序时需要格外考虑GIL并可能采用其他方法例如多进程来规避。
为了绕过GIL的限制Python的开发者通常会使用以下方法 使用多进程而非多线程因为每个进程有自己的Python解释器和内存空间因此GIL不会成为限制。 使用基于C语言的扩展来执行计算密集的任务在C语言的扩展中可以释放GIL。 使用其他实现的Python解释器例如PyPy它也有GIL但是用了一些技术来减少GIL的影响或使用完全没有GIL的Jython或IronPython这些解释器有其他限制。 虽然GIL在是Python社区中常被诟病的话题但Python仍然非常流行这表明GIL并不是对大多数Python程序来说绝对的障碍。对于一些高并发处理的需求通常会有其他语言或架构层面的解决方案。
4. 多进程和多线程的比较 线程在进程下行进单纯的车厢无法运行 一个进程可以包含多个线程一辆火车可以有多个车厢 不同进程间数据很难共享一辆火车上的乘客很难换到另外一辆火车比如站点换乘 同一进程下不同线程间数据很易共享A车厢换到B车厢很容易 进程要比线程消耗更多的计算机资源采用多列火车相比多个车厢更耗资源 进程间不会相互影响一个线程挂掉将导致整个进程挂掉一列火车不会影响到另外一列火车但是如果一列火车上中间的一节车厢着火了将影响到该趟火车的所有车厢 进程可以拓展到多机进程最多适合多核不同火车可以开在多个轨道上同一火车的车厢不能在行进的不同的轨道上 进程使用的内存地址可以上锁即一个线程使用某些共享内存时其他线程必须等它结束才能使用这一块内存。比如火车上的洗手间”互斥锁mutex” 进程使用的内存地址可以限定使用量比如火车上的餐厅最多只允许多少人进入如果满了需要在门口等等有人出来了才能进去“信号量semaphore” (看的某篇博客的总结就抄录在此了)
总结
如果任务是I/O密集型的比如网络操作或磁盘读写多线程能够提高效率。 如果任务是CPU密集型的比如复杂计算可以考虑多进程以并行地利用CPU资源。 在实际应用中适当选择多线程或多进程甚至两者结合使用可以优化程序的性能。然而在使用多线程和多进程时需要考虑线程或进程间的通信、数据共享、同步和死锁等问题。