python多进程与多线程

本文详细介绍了Python中的多进程和多线程,包括进程、线程的概念,同步与异步的区别,以及threading和multiprocessing模块的使用,如线程锁、条件变量、信号量和事件等实现线程同步的机制,并探讨了GIL对多线程的影响。此外,还提到了concurrent.futures模块用于启动并行任务,以及进程间的通信方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

进程、线程、协程

进程:进程是一个“执行中的程序”。程序是一个没有生命的实体,只有在处理器赋予程序生命时,它才能成为一个活动的实体,我们称其为进程。每一个进都有自己的地址空间,一般情况下,包括文本区域(text region)、数据区域(data region)和堆栈(stack region)。

进程是操作系统进行资源分配和调度的基本单位。

线程:对于操作系统来说,一个任务就是一个进程;在一个进程内部,要同时干多件事,这需要同时运行多个“子任务”,我们把进程内的这些子任务称之为线程。

线程是程序执行的最小单位,实际上进程只负责分配资源,而利用这些资源执行程序的是线程,所以一个进程中最少有一个线程来负责执行程序。同一个进程内的线程共享进程的全部资源。

要利用多核计算机的计算性能,推荐使用 multiprocessing 模块;

想同时运行多个I/O绑定任务,推荐使用 threading 模块。

协程:单线程+异步I/O的编程模型称为协程,有了协程的支持,就可以基于事件驱动编写高效的多任务程序。协程最大的优势就是极高的执行效率因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销。协程的第二个优势就是不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不用加锁,只需要判断状态就好了,所以执行效率比多线程高很多。如果想要充分利用CPU的多核特性,最简单的方法是多进程+协程,既充分利用多核,又充分发挥协程的高效率,可获得极高的性能。

同步和异步

同步:是按照任务的顺序执行任务,前一个任务没有执行结束,下一个任务不会执行,要等待上一个任务执行结束。

异步:是同一时间内可以做多件事。(这往往伴随着多线程)

同步和异步的优缺点:

  • 同步的执行效率会比较低,耗费时间,但有利于我们对流程进行控制,避免很多不可掌控的意外情况;
  • 异步的执行效率高,节省时间,但是会占用更多的资源,也不利于我们对进程进行控制。

threading - 线程模块

  • GIL : global interpreter lock全局解释器锁

    CPython 解释器所采用的一种机制,它确保同一时刻只有一个线程在执行 Python bytecode。给整个解释器加锁使得解释器多线程运行更方便,其代价则是牺牲了在多处理器上的并行性。
    作用:限制多线程同时执行,保证同一个时刻只有一个线程执行。

原因:线程并非独立,在一个进程中多个线程共享变量的,多个线程执行会导致数据被污染造成数据混乱,这就是线程的不安全性,为此引入了互斥锁。

互斥锁:即确保某段关键代码的数据只能又一个线程从头到尾完整执行,保证了这段代码数据的安全性,但是这样就会导致死锁。

死锁:多个子线程在等待对方解除占用状态,但是都不先解锁,互相等待,这就是死锁。

基于GIL的存在,在遇到大量的IO操作(文件读写,网络等待)代码时,使用多线程效率更高。

Thread Objects

class threading.Thread(group=None, target=None, name=None, args=(), kwargs={}, *, daemon=None)

# 参数
group 应该为 None;为了日后扩展 ThreadGroup 类实现而保留。
target 是用于 run() 方法调用的可调用对象。默认是 None,表示不需要调用任何方法。
name 是线程名称。默认情况下,由 "Thread-N" 格式构成一个唯一的名称,其中 N 是小的十进制数。
args 是用于调用目标函数的参数元组。默认是 ()。
kwargs 是用于调用目标函数的关键字参数字典。默认是 {}。
daemon 参数将设置该线程是否为守护模式。当 daemon=True 时,主进程结束是不管子进程有没有结束都会退出程序

# 可调用的方法
start()
run()
join(timeout=None) 等待子进程执行完后再执行主进程
is_alive()
daemon 值继承于创建线程;主线程不是守护线程,因此主线程创建的所有线程默认都是 daemon = False。

关于join():

# 假设已经创建了两个线程 t1,t2

t1.start()
t1.join()
t2.start()
t2.join()
#  先执行线程1然后等待线程1执行完毕,然后执行线程2等待线程2执行完毕。

t1.start()
t2.start()
t1.join()
t2.join()
# 等到线程1与线程2执行完毕后再执行主线程。

要创建一个线程对象,除了通过 Thread 类直接创建,还可以构建自定义的线程类

from threading import Thread

class MyThread(Thread):

    # 重写构造函数
    def __init__(self,name):
        super().__init__()
        self._name = name
        
    # 重写run方法
    def run(self):
        print('Hello,',self._name)

if __name__ == '__main__':
    t = MyThread('Alice')
    t.start()

Lock Objects

原始锁处于 “锁定” 或者 “非锁定” 两种状态之一。它被创建时为非锁定状态。它有两个基本方法, acquire()release() 。当状态为非锁定时, acquire() 将状态改为 锁定 并立即返回。当状态是锁定时, acquire() 将阻塞其它线程,直到另一个线程调用 release() 将其改为非锁定状态。 release() 只在锁定状态下调用; 它将状态改为非锁定并立即返回。如果尝试释放一个非锁定的锁,则会引发 RuntimeError 异常。

锁同样支持 上下文管理协议。

class threading.Lock
实现原始锁对象的类。一旦一个线程获得一个锁,会阻塞随后尝试获得锁的线程,直到它被释放;任何线程都可以释放它。

    acquire(blocking=True, timeout=-1) 可以阻塞或非阻塞地获得锁。
    release() 释放一个锁。这个方法可以在任何线程中调用,不单指获得锁的线程。
# 递归锁
class threading.RLock

RLock允许在同一线程中被多次acquire(比如你一个函数上了锁,这个函数调用另一个函数,另一个函数也上了锁 )。
而Lock却不允许这种情况。否则会出现死循环,程序不知道解哪一把锁。
注意:如果使用RLock,那么acquire和release必须成对出现.

线程间的通信

在一个进程中,数据变量是共享的,即多个子线程可以对同一个全局变量进行操作修改,那么线程之间的通信可以通过

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值