python进程、线程、协程?

进程、线程、协程

一、基础概念

1. 什么是进程和线程?

进程

  • 进程其实就是运行中的程序,当运行一个应用程序时,操作系统创建一个进程,为其分配独立的地址空间,内存以及堆栈等资源,是资源分配的最小单位。

  • 一个CPU,在同一个时刻只能运行一个进程,几个进程在同一个CPU上交替执行,称之为并发执行。

线程

  • 线程是进程中的子任务,共享进程中的数据,他们使用相同的地址空间,共享全局变量,静态变量等数据,是程序运行的最小单位。
  • 一个进程可以有多个线程,他们共享进程的资源,一个进程是一个任务,则子线程对应的是子任务。
  • 并发与并行

2. 并发和并行

  • 并发(在一段时间内交替去执行多个任务:任务数量大于CPU核心数<切换速度很快>)。
  • 并行(在一段时间内真正同时一起执行多个任务:任务数量小于或等于CPU核心数)。

3. 多进程和多线程的使用场景

多进程适用于CPU密集型任务

  • CPU密集

    • CPU执行时间占程序运行的大部分时间,小部分时间进行磁盘读写操作
  • 每个进程都有自己的独立内存空间和系统资源,进程间通信需要使用特定的机制,例如管道、消息队列、共享内存等方式,这些机制的开销较大

    • 如矩阵计算、图像处理等

多线程适用于I/O密集型任务

  • I/O密集型
    • I/O读写操作慢,如果程序I/O读写占用时间过长,会造成CPU性能的浪费
  • 线程共享进程的资源,包括内存、文件句柄、网络连接等,因此线程间通信可以通过共享内存来实现,这样的开销较小
    • 如网络通信、文件读写等

4. 进程同步的方式

管道(Pipe)

  • 管道是一种单向通信机制,通信的两端分别为一个读端和一个写端
  • 局限
    • 只能在具有公共祖先的进程之间使用
  • 适用场景
    • 主要适用于父子进程或者兄弟进程之间的通信,例如shell管道命令中的使用

命名管道(Named Pipe)

  • 命名管道是一种特殊的管道,它可以在不具有公共祖先的进程之间进行通信
  • 适用场景
    • 主要适用于无血缘关系的进程之间的通信,例如客户端和服务器之间的通信

信号(Signal)

  • 信号是进程间异步通信的一种方式,可以用于通知某个进程发生了某个事件。信号的发送和接收都是异步的,因此需要特殊处理。
  • 适用场景:
    • 主要适用于进程之间通知或者中断,例如Ctrl+C中断一个进程

信号量(Semaphore)

  • 信号量是一种计数器,用于控制多个进程对共享资源的访问。可以通过对信号量进行操作来实现进程间的同步和互斥。
  • 适用场景:
    • 主要适用于进程之间对共享资源的访问控制,例如控制并发访问数据库

消息队列(Message Queue)

  • 消息队列是一种可存放在内核中的消息列表,可以用于进程间通信。
  • 消息队列具有先进先出的特点,可以实现进程间的异步通信。
  • 适用场景:
    • 主要适用于进程之间通信,并且需要存储大量的消息,例如进程之间传递数据。

共享内存(Shared Memory)

  • 共享内存可以让多个进程共享同一块物理内存,进而实现数据共享。由于共享内存不需要在进程间拷贝数据,因此具有较高的效率。
  • 适用场景:
    • 主要适用于进程之间需要频繁共享大量数据的场景,例如图像处理

套接字(Socket)

  • 套接字是一种可用于不同主机间进程通信的机制,具有广泛的应用场景,例如网络通信、分布式计算等。
  • 适用场景:
    • 主要适用于不同主机或者不同进程之间的通信,例如客户端和服务器之间的通信。

5. 线程同步方式

锁(Lock)

  • 锁是一种用于多线程同步的机制,可以防止多个线程同时访问同一共享资源。通过对锁的加锁和解锁操作,可以实现对共享资源的访问控制。常见的锁类型包括互斥锁、读写锁、条件变量等
  • 适用场景
    • 可以用于共享资源的互斥访问,例如多个线程访问同一文件时可以使用文件锁

条件变量(Condition Variable)

  • 条件变量是一种用于线程间通信的机制,可以在某个条件满足时唤醒等待的线程。条件变量通常与锁一起使用,以实现多线程之间的同步和协作

  • 适用场景

    • 例如多个线程等待某个条件满足时可以使用条件变量唤醒等待的线程。

信号量(Semaphore)

  • 信号量是一种计数器,用于控制多个线程对共享资源的访问。可以通过对信号量进行操作来实现线程间的同步和互斥

  • 适用场景

    • 信号量可以用于控制多个线程对共享资源的访问,例如实现对数据库访问的并发控制

队列(Queue)

  • 队列是一种数据结构,可以用于多线程之间的数据传递。通过队列的生产者和消费者模式,可以实现线程间的异步通信

读写锁(Read-Write Lock)

  • 读写锁是一种特殊的锁,用于控制多个线程对共享资源的读写操作
  • 读写锁允许多个线程同时读取共享资源,但只允许一个线程进行写入操作。这样可以提高读取共享资源的并发性。

原子操作(Atomic Operation)

  • 原子操作是指不可被中断的操作。在多线程环境下,可以利用原子操作来实现对共享变量的安全读写

6. 协程

概念

  • 协程是一种轻量级的线程,又称微线程,它可以在单线程内实现并发编程,通过一个线程实现代码块相互切换执行
  • 可以看作是一种用户空间的线程,它是由程序员自己控制的,不需要操作系统的参与

为什么需要协程?

  • 在计算密集型的程序中,利用协程来回切换执行,没有任何意义,来回切换并保存状态 反倒会降低性能。
  • 在IO密集型程序中(大部分时间都在等待IO操作),如果使用多线程进行并发,CPU大部分时间都将用于切换线程上限文(内核态完成),需要频繁浪费CPU时间片去分配内存
  • 协程的栈空间较少,且在用户态进行协程上下文切换,多个协程在一个线程上并发运行。利用协程在IO等待时间就去切换执行其他任务,当IO操作结束后再自动回调,那么就会大大节省资源并提供性能,从而实现异步编程(不等待任务结束就可以去执行其他代码)

二、 Python 多进程

  • 学习文章

  • 多线程的局限性

    • Python的线程是操作系统线程,全局解释器锁(GIL)是Python解释器实现的一种线程锁机制,它能够保证Python解释器的线程安全性,即在同一时刻只有一个线程可以执行同一份Python字节码,即使CPU有多个核心,也不能实现多线程的并行执行
  • 多进程优势

    • 在多进程中,每个进程都有自己的解释器进程,并且可以同时使用多个 CPU 核心,因此在处理计算密集型任务时比多线程更有效
  • 进程间通信:由于每个进程都有自己的内存空间,因此它们之间不能直接共享数据,我们需要使用 multiprocessing 模块提供的管道、队列等机制来实现进程间通信

  • 内存限制:由于每个进程都有自己的内存空间,因此如果同时创建太多的进程,会占用过多的系统内存,导致程序崩溃

内置库 - multiprocessing

  • Python 3.6 才让 multiprocessing逐渐发展成一个能用的Python内置多进程库,可以进行进程间的通信
  • Python 3.8 在2019年增加了新特性 shared_memory(共享内存)

应用场景

  • 数据处理:当我们需要处理大量的数据时,可以使用多进程技术将数据分成多个部分,并同时处理它们。

  • 网络爬虫:当我们需要爬取大量的网页时,可以使用多进程技术将不同的任务分配给不同的进程,从而并行地执行它们。

  • 图像处理:当我们需要对大量的图像进行处理时,可以使用多进程技术并行执行不同的处理任务。

1. 直接创建子进程

  • 多进程的主进程一定要写在程序入口 if name =='main'
import multiprocessing

def worker(num):
    """打印工作进程的编号"""
    print("Worker %d is running" % num)

if __name__ == "__main__":
    # 创建 5 个进程
    for i in range(5):
        p = multiprocessing.Process(target=worker, args=(i,))
        p.start()
Python多进程可以选择两种创建进程的方式,spawn 与 fork
  • 分支创建:fork会直接复制一份自己给子进程运行,并把自己所有资源的handle 都让子进程继承,因而创建速度很快,但更占用内存资源
  • 分产创建:spawn只会把必要的资源的handle 交给子进程,因此创建速度稍慢,但是节省内存以及进程安全
multiprocessing.set_start_method('spawn')  # default on WinOS or MacOS
multiprocessing.set_start_method('fork')   # default on Linux (UnixOS)

2. 进程池 Pool

  • 进程池Pool 会自动帮我们管理子进程
如何传参
  • 需要传入多个参数,现在把它改成一个参数(tuple, dict等)
创建线程池
import multiprocessing

def worker(num):
   """子进程的工作函数"""
   print(f"Starting worker {num}")
   # 这里可以放一些耗时的任务
   print(f"Finished worker {num}")

if __name__ == '__main__':
   # 创建一个包含 4 个进程的进程池
   with multiprocessing.Pool(processes=4) as pool:
       # 使用 map 函数并行执行 worker 函数
       pool.map(worker, [1, 2, 3, 4])
   print("Parent process finished")
  • 进程池:在使用 Pool 类时,我们需要注意控制并发任务的数量,以免占用过多的系统资源。

map函数与 pool.map

map(function, iterable, ...)
  • python的map函数
    • 功能
      • 不断往同一个函数传不同参数的问题
    • 参数
      • 第一个参数 function 以参数序列中的每一个元素调用 function 函数,
      • 第二个参数时参数序列
    • 返回值
      • 返回包含每次 function 函数返回值所组成的迭代器,可以以list(迭代器)转为列表返回
  • pool.map
    • 与map相似,可进行多进程并行
    • 返回值
      • list

3. 管道 Pipe

默认创建双向的全双工管道
main_conn, child_conn = multiprocessing.Pipe()
  • 返回值为管道的两端Connection,其实两者没有区别,都可以读写,写入管道采用深拷贝
创建单向单双工管道
  • Pipe还有 duplex参数
    • 默认情况下 duplex==True,若不开启双向管道,那么传数据的方向只能 conn1 ← conn2
main_conn, child_conn = multiprocessing.Pipe(False)
读写数据
  • poll() 方法
    • conn2.poll()==True 意味着可以马上使用 conn2.recv() 拿到传过来的数据。conn2.poll(n) 会让它等待n秒钟再进行查询
conn.send(内容)

# 在全双工模式下,可以调用conn1.send发送消息,conn1.recv接收消息。如果没有消息可接收,recv方法会一直阻塞
conn.recv()

# 关闭
conn.close()
示例
import time

def func_pipe1(conn, p_id):

    time.sleep(0.1)
    conn.send(f'{p_id}_msg1')

    time.sleep(0.1)
    rec = conn.recv()
    print(p_id, 'recv: ', rec)

    time.sleep(0.1)
    conn.send(f'{p_id}_msg2')

    time.sleep(0.1)
    rec = conn.recv()
    print(p_id, 'recv: ', rec)


def func_pipe2(conn, p_id):

    time.sleep(0.1)
    rec = conn.recv()
    print(p_id, ' recv: ', rec)

    time.sleep(0.1)
    conn.send(f'{p_id}_msg1')

    time.sleep(0.1)
    rec = conn.recv()
    print(p_id, ' recv: ', rec)

    time.sleep(0.1)
    conn.send(f'{p_id}_msg2')


def run__pipe():
    from multiprocessing import Process, Pipe

    conn1, conn2 = Pipe()

    process = [Process(target=func_pipe1, args=(conn1, 'pipe1')),
               Process(target=func_pipe2, args=(conn2, 'pipe2')),
                ]

    [p.start() for p in process]
    [p.join() for p in process]


if __name__ =='__main__':
    run__pipe()
适用范围
  • Pipe适用于只有两个进程一读一写的单双工情况,也就是说信息是只向一个方向流动

4. 队列 Queue

  • 队列Queue 的功能与前面的管道Pipe非常相似:无论主进程或子进程,都能访问到队列,相对于管道有更多的属性,可进行多进程的通信
示例
import time

def func1(que, name):
    time.sleep(1)
    print(f'{name}: {que.get()}')

def run__queue():
    from multiprocessing import Process, Queue

    queue = Queue(maxsize=4)
    queue.put(True)
    queue.put([0, None, object])
    s = "binge"
    queue.put(s)
    print(queue.qsize())


    process = [Process(target=func1, args=(queue, 'p1')),
               Process(target=func1, args=(queue, 'p2')),
               Process(target=func1, args=(queue, 'p3'))]
    [p.start() for p in process]
    [p.join() for p in process]
    time.sleep(2)
    print(queue.qsize())


if __name__ =='__main__':
    run__queue()

5. 共享内存 Manager

  • 共享内存会由解释器负责维护一块共享内存(而不用深拷贝),这块内存每个进程都能读取到,读写的时候遵守管理

  • 需要再学

三,Python中使用多线程

概念

1. 全局解释器锁(GIL)有什么作用?

  • 全局解释器锁(GIL)是Python解释器实现的一种线程锁机制,它能够保证Python解释器的线程安全性,即在同一时刻只有一个线程可以执行Python字节码。
  • 背景
    • 由于Python解释器是用C语言实现的,而C语言的内存管理不具备自动垃圾回收的功能,因此如果多个线程同时访问Python解释器中的共享资源,就容易出现内存泄漏、内存覆盖等问题,从而导致程序崩溃或者运行不稳定
  • 作用
    • GIL的主要作用是保证Python解释器的线程安全性。在Python解释器中,每个线程都会在执行字节码之前尝试获取GIL。如果某个线程获取到了GIL,那么它就可以执行Python字节码,直到它主动释放GIL或者在执行过程中发生异常等情况。保证了同一时刻只有一个线程可以执行Python字节码,从而避免了多个线程同时修改共享数据带来的竞争条件问题(Race Condition
  • 局限性
    • 由于同一时刻只有一个线程可以执行Python字节码,因此Python中的多线程并发执行不能充分利用多核CPU的优势,从而影响了程序的性能。所以Python常用多进程而非多线程。
    • GIL的存在也使得Python不适合用于高并发的网络编程等场景
  • 使用
    • 大部分情况下,GIL 锁的使用并不需要程序员手动操作,Python 解释器会自动管理 GIL 锁的获取和释放
    • 有些情况下,我们可能需要手动释放 GIL 锁,例如将 CPU 密集型任务委托给 C 扩展模块等

2. 在多线程运行环境中,Python虚拟机执行方式如下:

  1. 设置GIL
  2. 切换进线程
  3. 执行下面操作之一
    1. 运行指定数量的字节码指令
    2. 线程主动让出控制权
  4. 切换出线程(线程处于睡眠状态)
  5. 解锁GIL
  6. 进入1步骤

3. 守护线程

Python线程为什么搞个setDaemon - 知乎 (zhihu.com)

  • 表现
    • 当设置了守护线程后,主线程已经执行完了,设置了守护线程,这时候子线程也一并退出了
  • 用处
    • 提供一个途径,让用户来设置随进程退出的标记, 只要子线程设置了守护线程, 那么主线程一准备退出,全都乖乖地由操作系统销毁回收
  • python3中
    • 当 daemon = False 时,线程不会随主线程退出而退出(默认时,就是 daemon = False)
    • 当 daemon = True 时,当主线程结束,其他子线程就会被强制结束

Threading模块使用多线程

1. threading 模块的类对象

  • Thread 执行线程
  • Timer 在运行前等待一段时间的执行线程
  • Lock 原语锁(互斥锁,简单锁)
  • RLock 重入锁,使单一线程可以(再次)获得已持有的锁
  • Condition 条件变量,线程需要等待另一个线程满足特定条件
  • Event 事件变量,N个线程等待某个事件发生后激活所有线程
  • Semaphore 线程间共享资源的寄存器
  • BoundedSemaphore 与Semaphore 相似,它不允许超过初始值
  • Barrie 执行线程达到一定数量后才可以继续

2. threading 模块的函数

  • activeCount() 获取当前活动中的Thread对象个数
  • currentThread() 获取当前的Thread对象
  • enumerate() 获取当前活动的Thread对象列表
  • settrace(func) 为所有线程设置一个跟踪(trace)函数
  • setprofile(func) 为所有线程设置配置文件(profile)函数
  • stack_size(size=None) 获取新创建线程的栈大小,也可设置线程栈的大小为size。

3. Thread类属性与类方法

  • 类属性
    • name 线程名称
    • ident 线程标识符号
    • daemon 是否为守护线程
  • 类方法
    • ___init__(self, group=None, target=None, name=None, args=(), kwargs=None, *, daemon=None)**
      • group 无用,保留参数
      • target 可调用的目标
      • name 线程的名称
      • args,kwargs
        • 用于接收调用对象的需要用到的参数,args接收tuple,kwargs接收dict
      • daemon 是否为守护线程
    • start()
      • 启动线程的执行
    • join(timeout=None)
      • 一种自旋锁自旋锁,设置阻塞timeout秒,它用来等待线程终止,当线程运行达到超时时间后结束线程
      • 只有在你需要等待线程完成然后在做其他事情的时候才是有用的
    • is_alive () 线程是否存活
    • isDaemon() 是否为守护线程
    • setDaemon(daemonic) 设置为守护线程

4. 可调用对象使用多线程

image-20230131145006388

5. 创建Thread实例

创建Thread实例,传递一个普通函数
import threading
import time

def print_numbers(name):
    for i in range(1, 11):
        print("{name} --- {i}".format(name=name, i=i))
        time.sleep(0.5)

# 创建两个线程,并分别启动
thread1 = threading.Thread(target=print_numbers, args=("线程1", ))
thread2 = threading.Thread(target=print_numbers, args=('线程2', ))
thread1.start()
thread1.join(3)  # 自旋锁,抢占进程
thread2.start()
创建Thread实例使用线程,传递一个 类实例函数
import threading
import time


class Binge:
    def print_numbers(self, name):
        for i in range(1, 11):
            print("{name} --- {i}".format(name=name, i=i))
            time.sleep(0.5)

# 创建两个线程,并分别启动
thread1 = threading.Thread(target=Binge().print_numbers, args=("线程1", ))
thread2 = threading.Thread(target=Binge().print_numbers, args=('线程2', ))
thread1.start()
thread1.join(3)  # 自旋锁,抢占进程
thread2.start()

6. 派生Thread 的子类,并创建子类的实例

from threading import Thread
import time

# 创建 Thread 的子类
class MyThread(Thread):
    def __init__(self, func, args):
        '''
        :param func: 可调用的对象
        :param args: 可调用对象的参数
        '''
        Thread.__init__(self)   # 不要忘记调用Thread的初始化方法
        self.func = func
        self.args = args

    def run(self):
        self.func(*self.args)


def print_numbers(name):
    for i in range(1, 11):
        print("{name} --- {i}".format(name=name, i=i))
        time.sleep(0.5)

def main():
    # 创建 Thread 实例
    t1 = MyThread(print_numbers, ("线程1", ))
    t2 = MyThread(print_numbers, ('线程2', ))
    # 启动线程运行
    t1.start()
    t1.join(3)  # 自旋锁
    t2.start()

    t2.join(3)

if __name__ == '__main__':
    main()

Thread的线程同步

背景

  • 一般在多线程代码中,总会有一些特定的函数或代码块不想被多个线程同时执行,如修改数据库、更新文件等
  • 存在的问题
    • 如果线程的运行顺序不同,有可能产生不同的结果
  • python的同步机制
    • 锁,信号量等

Lock 同步锁(原语锁)

1. 同步锁
  • 例子1
def func():
    global num  # 全局变量
    lock.acquire()  # 获得锁,加锁
    num1 = num
    time.sleep(0.1)
    num = num1 - 1
    lock.release()  # 释放锁,解锁
    time.sleep(2)


num = 100
l = []

for i in range(100):  # 开启100个线程
    t = threading.Thread(target=func, args=())
    t.start()
    l.append(t)

# 等待线程运行结束
for i in l:
    i.join()

print(num)
  • 结果
    • 不使用锁程序运行输出为 99;
    • 使用锁程序运行结果为0
  • 原因
    • time.sleep(0.1)时,当在没有锁的情况下线程将在这里被释放出来,让给下一线程运行,别的线程拿到的num值还是100,100个线程都-1则为0
Lock 与GIL(全局解释器锁)存在区别
  • Lock 锁的目的,它是为了保护共享的数据,同时刻只能有一个线程来修改共享的数据,而保护不同的数据需要使用不同的锁

  • GIL用于限制一个进程中同一时刻只有一个线程被CPU调度,GIL的级别比Lock高,GIL是解释器级别

  • GIL与Lock同时存在,程序执行如下:

    1. 同时存在两个线程:线程A,线程B
    2. 线程A 抢占到GIL,进入CPU执行,并加了Lock,但为执行完毕,线程被释放(GIL锁释放)
    3. 线程B 抢占到GIL,进入CPU执行,执行时发现数据被线程A Lock,于是线程B被阻塞
    4. 线程B的GIL被夺走,有可能线程A拿到GIL,执行完操作、解锁,并释放GIL
    5. 线程B再次拿到GIL,才可以正常执行

死锁

  • 两个或两个以上的线程在执行时,因争夺资源被相互锁住而相互等待

重入锁(递归锁) threading.RLock()

  • 作用
    • 为了支持同一个线程中多次请求同一资源
  • 功能
    • RLock内部维护着一个锁(Lock)和一个计数器(counter)变量,counter 记录了acquire 的次数,从而使得资源可以被多次acquire。直到一个线程所有 acquire都被release(计数器counter变为0),其他的线程才能获得资源
import time
import threading

# 生成一个递归对象
Rlock = threading.RLock()


class MyThread(threading.Thread):
    def __init__(self):
        threading.Thread.__init__(self)

    def run(self) -> None:
        self.fun_A()
        self.fun_B()

    def fun_A(self):
        Rlock.acquire()
        print('A加锁1', end='\t')
        Rlock.acquire()
        print('A加锁2', end='\t')
        time.sleep(0.2)
        Rlock.release()
        print('A释放1', end='\t')
        Rlock.release()
        print('A释放2')

    def fun_B(self):
        Rlock.acquire()
        print('B加锁1', end='\t')
        Rlock.acquire()
        print('B加锁2', end='\t')
        time.sleep(3)
        Rlock.release()
        print('B释放1', end='\t')
        Rlock.release()
        print('B释放2')


if __name__ == '__main__':
    t1 = MyThread()
    t2 = MyThread()
    t1.start()
    t2.start()

信号量(Semaphore)

  • 信号量是一个内部数据,它有一个内置的计数器,它标明当前的共享资源可以有多少线程同时读取

  • 信号量的声明

    • semaphore = threading.Semaphore(5)  # 创建信号量对象,5个线程并发
      
    • 定义一个只能同时执行5个线程的信号量

  • 读取关联信号量的共享资源时获取信号量

    • semaphore.acquire() # 获取共享资源,信号量计数器-1
      
    • 当计数器大于0时,那么可以为线程分配资源权限;当计数器小于0时,未获得权限的线程会被挂起,直到其他线程释放资源

  • 线程不需要共享资源时,需释放信号

    • semaphore.release()  # 释放共享资源,信号量计数器+1
      
信号量实现运行同一个目标函数的多个线程同步
import time
import threading
import random

# 创建信号量对象,信号量设置为3,需要有3个线程才启动
semaphore = threading.Semaphore(3)


def func():

    if semaphore.acquire():  # 获取信号 -1
        print(threading.current_thread().name + '获得信号量')
        time.sleep(random.randint(1, 5))
        semaphore.release()  # 释放信号 +1


for i in range(10):
    t1 = threading.Thread(target=func)
    t1.start()
信号量实现运行不同的目标函数的2个线程同步
import threading
import time
import random

# 同步两个不同线程,信号量被初始化0
semaphore = threading.Semaphore(0)


def consumer():
    print("-----等待producer运行------")
    semaphore.acquire()  # 获取资源,信号量为0被挂起,等待信号量释放
    print("----consumer 结束----- 编号: %s" % item )


def producer():
    global item  # 全局变量
    time.sleep(3)
    item = random.randint(0, 100)  # 随机编号
    print("producer运行编号: %s" % item)
    semaphore.release()


if __name__ == "__main__":
    for i in range(0, 4):
        t1 = threading.Thread(target=producer)
        t2 = threading.Thread(target=consumer)
        t1.start()
        t2.start()
        t1.join()
        t2.join()
    print("程序终止")

Condition 条件变量

  • Condition 条件变量通常与一个锁相关联。需要在多个Condition 条件中共享一个锁时,可以传递一个Lock/RLock实例给构造方法,否则他将自己产生一个RLock实例

  • Condition的用法

    • # 定义条件变量锁实例
      condition = threading.Condition()
      # 获得锁(线程锁)
      condition.acquire() 
      #释放锁
      condition.release() 
      # 挂起线程timeout秒(为None时时间无限),直到收到notify通知或者超时才会被唤醒继续运行。必须在获得Lock下运行。
      wait(timeout) 
      # 通知挂起的线程开始运行,默认通知正在等待该condition的线程,可同时唤醒n个。必须在获得Lock下运行。
      notify(n=1) 
      # 通知所有被挂起的线程开始运行。必须在获得Lock下运行。
      notifyAll() 
      
1. 生产者与消费者
import threading
import time

# 商品
product = None
# 条件变量对象
con = threading.Condition()


# 生产方法
def produce():
    global product  # 全局变量产品
    if con.acquire():
        while True:
            print('---执行,produce--')
            if product is None:
                product = '袜子'
                print('---生产产品:%s---' % product)
                # 通知消费者,商品已经生产
                con.notify()  # 唤醒消费线程
            # 等待通知
            con.wait()
            time.sleep(2)


# 消费方法
def consume():
    global product
    if con.acquire():
        while True:
            print('***执行,consume***')
            if product is not None:
                print('***卖出产品:%s***' % product)
                product = None
                # 通知生产者,商品已经没了
                con.notify()
            # 等待通知
            con.wait()
            time.sleep(2)


if __name__=='__main__':
    t1 = threading.Thread(target=consume)
    t1.start()
    t2 = threading.Thread(target=produce)
    t2.start()

Event 事件锁对象

  • 程序中的其一个线程需要通过判断某个线程的状态来确定自己下一步的操作,就用到了event()对象。event()对象有个状态值,他的默认值为 Flase,即遇到 event() 对象就阻塞线程的执行。

  • Event 的用法

    • # 创建实例
      event = threading.Event()
      # 挂起线程timeout秒(None时间无限),直到超时或收到event()信号开关为True时才唤醒程序。
      wait(timeout=None) 
      # Even状态值设为True
      set()
      # Even状态值设为 False
      clear()
       返回Even对象的状态值。
      isSet()
      
  • 示例

    • import threading
      
      event = threading.Event()
      
      
      def func():
          print('等待服务响应...')
          event.wait()  # 等待事件发生
          print('连接到服务')
      
      
      def connect():
          print('成功启动服务')
          event.set()
      
      
      t1 = threading.Thread(target=func, args=())
      t2 = threading.Thread(target=connect, args=())
      
      t1.start()
      t2.start()
      

Barrie 障碍锁

threading 使用 Queue 保持线程同步

  • Queue 模块可以实现多生产者与多消费者队列,它可以实现多个线程之间的信息安全交换
FIFO队列,先进先出
q = queue.Queue(10) # 指定队列中最多存储项目的个数
LIFO队列,后进先出,如同栈
q = queue.LifoQueue() 
Priority队列,对着中的数据始终保持排序,优先检索最低值
  • 存储的数据必须是同类型的可排序数据
q = queue.PriorityQueue(10)
公共方法
qsize() 队列大大致大小,非准确值

empty() 当前是否为空

full() 当前是否已满

put(item, block=True, timeout=None) 将item放入队列。block=True, timeout=None 在必要时阻塞,直到有空位可用,timeout 为阻止的时间,超时抛出Full异常。
block=False 立即将item放入队列,队列已满引发Full异常。

put_nowait(item) 立即放入队列,同put(item,False)

get(block=True, timeout=None) 从队列中删除并返回一个item。
block=True, timeout=None 在必要时阻塞,直到有可用数据为止,timeout 为阻止的时间,超时抛出Empty异常。
block=False 立即获取队列中的可用数据,否则抛出Empty异常。

get_nowait() 立即获取队列中的数据,同get(False)。

task_done() 向已完成的队列任务发送一个信号。一般是告诉join() 我以完成任务。

join() 阻塞线程,直到队列为空才放行。
生产 消费模式
import threading, time
import queue

# 最多存入10个
q = queue.PriorityQueue(10)


def producer(name):
    ''' 生产者 '''
    count = 1
    while True:
        #  生产袜子
        q.put("袜子 %s" % count)  # 将生产的袜子方法队列
        print(name, "---生产了袜子", count)
        count += 1
        time.sleep(0.2)


def consumer(name):
    ''' 消费者 '''
    while True:
        print("%s ***卖掉了[%s]" % (name, q.get()))  # 消费生产的袜子
        time.sleep(1)
        q.task_done()  # 告知这个任务执行完了


# 生产线程
z = threading.Thread(target=producer, args=("张三",))
# 消费线程
l = threading.Thread(target=consumer, args=("李四",))
w = threading.Thread(target=consumer, args=("王五",))

# 执行线程
z.start()
l.start()
w.start()

四、协程 - Coroutine

python实现协程的方式

  • Python2.x 对协程的支持比较有限,通过 yield 关键字支持的生成器实现了一部分协程的功能但不完全。
  • 第三方库 gevent 对协程有更好的支持。
  • Python3.4 中提供了 asyncio 模块。
  • Python3.5 中引入了 async/await 关键字。
  • Python3.6 中 asyncio 模块更加完善和稳定。
  • Python3.7 中内置了 async/await 关键字

关键字实现协程

1. 简单实现协程

import asyncio


# 定义异步函数
async def func1():
    print(1)
    await asyncio.sleep(2)  # 耗时操作
    print(2)


async def func2():
    print(3)
    await asyncio.sleep(2)  # 耗时操作
    print(4)


async def main():

	# 异步函数实例化对象
    tasks = [
        asyncio.ensure_future(func1()),
        asyncio.ensure_future(func2())
    ]
    await asyncio.gather(*tasks)

asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
asyncio.run(main())

2. 同步编程与基于协程的异步编程

  • 目的
    • 爬虫中, 用代码实现下载 url_list 中的图片
同步编程
"""
下载图片使用第三方模块requests,请提前安装:pip3 install requests
"""
import requests
import time


def download_image(url):
    print("开始下载:",url)
    # 发送网络请求,下载图片
    response = requests.get(url)
    print("下载完成")
    # 图片保存到本地文件
    file_name = url.rsplit('_')[-1]
    with open(file_name, mode='wb') as file_object:
        file_object.write(response.content)


if __name__ == '__main__':
    t1 = time.time()
    url_list = [
        'https://www3.autoimg.cn/newsdfs/g26/M02/35/A9/120x90_0_autohomecar__ChsEe12AXQ6AOOH_AAFocMs8nzU621.jpg',
        'https://www2.autoimg.cn/newsdfs/g30/M01/3C/E2/120x90_0_autohomecar__ChcCSV2BBICAUntfAADjJFd6800429.jpg',
        'https://www3.autoimg.cn/newsdfs/g26/M0B/3C/65/120x90_0_autohomecar__ChcCP12BFCmAIO83AAGq7vK0sGY193.jpg'
    ]
    for item in url_list:
        download_image(item)
    t2 = time.time()
    print(f'cost: {t2-t1}')
  • cost: 0.21187114715576172
异步编程
"""
下载图片使用第三方模块aiohttp,请提前安装:pip3 install aiohttp
"""
#!/usr/bin/env python
# -*- coding:utf-8 -*-
import aiohttp
import asyncio
import time


async def fetch(session, url):
    print("发送请求:", url)
    async with session.get(url, verify_ssl=False) as response:
        content = await response.content.read()
        file_name = url.rsplit('_')[-1]
        with open(file_name, mode='wb') as file_object:
            file_object.write(content)


async def main():
    async with aiohttp.ClientSession() as session:
        url_list = [
            'https://www3.autoimg.cn/newsdfs/g26/M02/35/A9/120x90_0_autohomecar__ChsEe12AXQ6AOOH_AAFocMs8nzU621.jpg',
            'https://www2.autoimg.cn/newsdfs/g30/M01/3C/E2/120x90_0_autohomecar__ChcCSV2BBICAUntfAADjJFd6800429.jpg',
            'https://www3.autoimg.cn/newsdfs/g26/M0B/3C/65/120x90_0_autohomecar__ChcCP12BFCmAIO83AAGq7vK0sGY193.jpg'
        ]
        tasks = [asyncio.create_task(fetch(session, url)) for url in url_list]

        await asyncio.wait(tasks)


if __name__ == '__main__':
    t1 = time.time()
    asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
    asyncio.run(main())
    t2 = time.time()
    print(f'cost: {t2-t1}')
  • cost: 0.09617066383361816

3. python内置的异步编程

  • 异步:在当前任务需要等待IO的执行结果时,CPU先去干其他事情

(1)事件循环

事件循环原理
  • 在事件循环中, 会执行所有任务(即异步函数)
  • 但同一时间, 只有一个任务在执行
  • 当一个任务中执行await后, 此任务被挂起, 事件循环执行下一个任务
创建事件循环
  • 旧版本的采用

    •  loop = asyncio.new_event_loop()
      
  • python 3.7 后采用封装了事件循环的asyncio.run()

    • asyncio.run(func()) # func应该是个协程对象
      

(2) 协程与异步编程

协程函数
  • async def 定义的函数
协程对象
  • 协程对象,调用 协程函数 所返回的对象
    • 调用协程函数时,函数内部代码不会执行,只是会返回一个协程对象
协程对象的执行
  • asyncio.run(func())
    
  • 将协程当做任务添加到 事件循环 的任务列表,然后事件循环检测列表中的协程是否 已准备就绪(默认可理解为就绪状态),如果准备就绪则执行其内部代码

await - 手动切换异步函数
  • await是一个只能在协程函数中使用的关键字,用于遇到IO操作时挂起 当前协程(任务),当前协程(任务)挂起过程中 事件循环可以去执行其他的协程(任务),当前协程IO处理完成时,可以再次切换回来执行await之后的代码
  • await后面必须跟一个协程(future), 就可以阻塞当前协程, 切换到这个新协程里执行
Task对象
  • 在程序想要创建多个任务对象,需要使用Task对象来实现

  • 作用

    • Tasks用于并发调度协程,通过传入协程对象作为参数创建Task对象,这样可以让协程加入事件循环中等待被调度执行
  • python3.7之后

    • asyncio.create_task(协程对象)
      
  • python3.7之前

    • asyncio.ensure_future(协程对象)
      
  • task 与 await

    • import asyncio
      
      
      async def func():
          print(1)
          await asyncio.sleep(2)
          print(2)
          return "返回值"
      
      
      async def main():
          print("main开始")
      
          # 创建协程,将协程封装到一个Task对象中并立即添加到事件循环的任务列表中,等待事件循环去执行(默认是就绪状态)。
          task1 = asyncio.create_task(func())
      
          # 创建协程,将协程封装到一个Task对象中并立即添加到事件循环的任务列表中,等待事件循环去执行(默认是就绪状态)。
          task2 = asyncio.create_task(func())
      
          print("main结束")
      
          # 当执行某协程遇到IO操作时,会自动化切换执行其他任务。
          # 此处的await是等待相对应的协程全都执行完毕并获取结果
          ret1 = await task1
          ret2 = await task2
          print(ret1, ret2)
      
      
      asyncio.run(main())
      
  • asyncio.wait: 对列表中的每个协程执行ensure_future从而封装为Task对象

    • import asyncio
      
      
      async def func():
          print(1)
          await asyncio.sleep(2)
          print(2)
          return "返回值"
      
      
      async def main():
          print("main开始")
      
          # 创建协程,将协程封装到Task对象中并添加到事件循环的任务列表中,等待事件循环去执行(默认是就绪状态)。
          # 在调用
          task_list = [
              asyncio.create_task(func(), name="n1"),
              asyncio.create_task(func(), name="n2")
          ]
      
          print("main结束")
      
          # 当执行某协程遇到IO操作时,会自动化切换执行其他任务。
          # 此处的await是等待所有协程执行完毕,并将所有协程的返回值保存到done
          # 如果设置了timeout值,则意味着此处最多等待的秒,完成的协程返回值写入到done中,未完成则写到pending中。
          done, pending = await asyncio.wait(task_list, timeout=None)
          print(done, pending)
      
      
      asyncio.run(main())
      
asyncio.gather(task...)
  • 告知事件循环有哪些协程
asyncio.gather 和asyncio.wait区别
  • wait()使用一个set保存它创建的Task实例。
    • 因为set是无序的所以这也就是我们的任务不是顺序执行的原因。wait的返回值是一个元组,包括两个集合,分别表示已完成和未完成的任务。
    • wait第二个参数为一个超时值,达到这个超时时间后,未完成的任务状态变为pending
  • gather的区别之处
    • gather任务无法取消。
    • 返回值是一个结果列表
    • 可以按照传入参数的 顺序,顺序输出
asyncio.Future对象
  • asyncio中的Future对象是一个相对更偏向底层的可对象,是 Task的父类
uvloop
  • Python标准库中提供了asyncio模块,用于支持基于协程的异步编程, 是 asyncio 中的事件循环的替代方案,替换后可以使得asyncio性能提高
为何使用 asyncio.sleep,而不是time.sleep?
  • await后面一个要跟一个future(一个异步函数的实例化对象),time.sleep并不是异步函数,不支持协程切换,我发实现并发,只能串行
  • 在asyncio.sleep期间主线程并未等待,而是去执行事件循环中其他可以执行的coroutine

(3)应用案例

异步redis

  • 当通过python去操作redis时,链接、设置值、获取值 这些都涉及网络IO请求,使用asycio异步的方式可以在IO等待时去做一些其他任务,从而提升性能
import asyncio
import aioredis


async def execute(address, password):
    print("开始执行", address)

    # 网络IO操作:先去连接 47.93.4.197:6379,遇到IO则自动切换任务,去连接47.93.4.198:6379
    redis = await aioredis.create_redis_pool(address, password=password)

    # 网络IO操作:遇到IO会自动切换任务
    await redis.hmset_dict('car', key1=1, key2=2, key3=3)

    # 网络IO操作:遇到IO会自动切换任务
    result = await redis.hgetall('car', encoding='utf-8')
    print(result)

    redis.close()
    # 网络IO操作:遇到IO会自动切换任务
    await redis.wait_closed()

    print("结束", address)


task_list = [
    execute('redis://47.93.4.197:6379', "root!2345"),
    execute('redis://47.93.4.198:6379', "root!2345")
]

asyncio.run(asyncio.wait(task_list))

异步mysql

  • 当通过python去操作MySQL时,连接、执行SQL、关闭都涉及网络IO请求,使用asycio异步的方式可以在IO等待时去做一些其他任务,从而提升性能
#!/usr/bin/env python
# -*- coding:utf-8 -*-
import asyncio
import aiomysql


async def execute(host, password):
    print("开始", host)
    # 网络IO操作:先去连接 47.93.40.197,遇到IO则自动切换任务,去连接47.93.40.198:6379
    conn = await aiomysql.connect(host=host, port=3306, user='root', password=password, db='mysql')

    # 网络IO操作:遇到IO会自动切换任务
    cur = await conn.cursor()

    # 网络IO操作:遇到IO会自动切换任务
    await cur.execute("SELECT Host,User FROM user")

    # 网络IO操作:遇到IO会自动切换任务
    result = await cur.fetchall()
    print(result)

    # 网络IO操作:遇到IO会自动切换任务
    await cur.close()
    conn.close()
    print("结束", host)


task_list = [
    execute('47.93.40.197', "root!2345"),
    execute('47.93.40.197', "root!2345")
]

asyncio.run(asyncio.wait(task_list))
posted @   bingekong  阅读(109)  评论(0编辑  收藏  举报
编辑推荐:
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 25岁的心里话
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示