多任务 同步 异步

1. 阻塞 非阻塞

阻塞:阻塞状态指程序未得到所需计算资源时被挂起的状态(无法继续往下)。或者程序在等待某个操作完成期间,自身无法继续干别的事情,则称该程序在该操作上是阻塞的。

常见的阻塞形式有:网络 I/O 阻塞、磁盘 I/O 阻塞、用户输入阻塞等。阻塞是无处不在的,包括 CPU 切换上下文时,所有的进程都无法真正干事情,它们也会被阻塞。如果是多核 CPU 则正在执行上下文切换操作的核不可被利用。

非阻塞:程序在等待某操作过程中,自身不被阻塞,可以继续运行干别的事情,则称该程序在该操作上是非阻塞的。

2.同步 异步

2.1概念

同步和异步关注的是消息通信机制

同步就是调用方必须等待调用结果返回才能继续执行后续操作。
异步就是调用方不必等待结果返回,而是通过状态、通知或回调函数来处理调用结果。

同步和异步也可以指两个线程的运行方式。同步是指两个线程的运行是相关的,其中一个线程要阻塞等待另外一个线程的运行。异步是指两个线程毫无相关,自己运行自己的。

举个例子,如果你去商城买手机,你看上了一款手机,跟店家说你要这款手机,他就去仓库拿货,你得在店里等着,不能离开,这叫做同步。如果你在京东下单,下单完成后你就可以做其他事情(追剧、打王者、lol)等货到了去签收就ok了.这就叫异步¹。

总结
同步:执行一个操作后,必须等待结果,才能继续执行后续操作。
异步:执行一个操作后,可以去执行其他操作,然后等待通知再回来执行刚才没执行完的操作。

同步调用和异步调用
同步调用:函数和被调函数无法同时运行。(有阻塞产生)
异步调用: 函数和被调函数可以同时运行。(无阻塞产生)

2.同步异步优缺点

同步和异步的优缺点可以从不同的角度来分析。根据网上的一些资料,我可以给你这样的总结:

  • 从执行效率和时间上看,同步的执行效率会比较低,耗费时间,但有利于我们对流程进行控制,避免很多不可掌控的意外情况;异步的执行效率高,节省时间,但是会占用更多的资源,也不利于我们对进程进行控制¹²³⁴。
  • 从结果处理和异常捕获上看,同步流程对结果处理通常更为简单,可以就近处理,并且可以很容易捕获、处理异常;异步流程对结果的处理始终和前文保持在一个上下文内,并且需要通过状态、通知或回调函数来处理结果和异常²⁴。
  • 从使用场景上看,同步适合于不涉及共享资源,或对共享资源有写操作,或有时序上的严格关系,或需要原子操作的情况;异步适合于涉及IO操作等耗时操作,或对共享资源只读,或没有时序上的严格关系,或可以通过其他方式控制原子性的情况²⁴。

源:
(1) 同步和异步的区别及优缺点 - CSDN博客. 同步和异步的区别及优缺点_异步采样和同步采样的优缺点-CSDN博客.
(2) 同步和异步的区别和优缺点 - CSDN博客. 同步和异步的区别和优缺点_jnpf流程图里的子流程 同步和异步的区别-CSDN博客.
(3) 同步和异步的区别及优缺点 通俗理解 - CSDN博客. 同步和异步的区别及优缺点 通俗理解_处理器核心同步和异步的性能差距-CSDN博客.
(4) 同步和异步的区别及优缺点 - 大木瓜 - 博客园. 同步和异步的区别及优缺点 - 大木瓜 - 博客园.
(1) 同步(Synchronous)和异步(Asynchronous) - 小cai一碟 - 博客园. 同步(Synchronous)和异步(Asynchronous) - 小cai一碟 - 博客园.
(2) 同步与异步的区别(一看则懂) - CSDN博客. 同步与异步的区别(一看则懂)_同步和异步的区别-CSDN博客.
(3) android开发中的同步和异步区别的理解 - CSDN博客. android开发中的同步和异步区别的理解_android同步线程和异步线程-CSDN博客.


1.多任务编程

1.1 多任务

一种同时执行多个任务或处理多个工作的能力
面对的问题:
例如并发访问共享资源可能引发竞态条件和数据一致性问题,需要采取合适的同步机制来解决。
此外,调度算法的设计和任务切换的开销也需要考虑。

1.2 多任务编程

指在编程中同时执行多个任务或线程。它可以提高程序的效率和响应能力,同时也可以利用多个处理器或核心的能力。
在实际开发中,Python 多任务编程可以通过以下三种形式实现
1.进程: 可以在操作系统级别同时运行多个独立的进程。
特点:每个进程拥有独立的内存空间和资源,可以实现更高的隔离性。

2.线程: 它可以在同一个程序内同时运行多个线程,每个线程负责执行不同的任务。
问题:多线程编程需要注意线程安全问题,比如访问共享资源时需要使用锁来保证数据的一致性

3.协程: 一种轻量级的多任务编程技术,它可以在同一个线程中实现多个任务的切换和调度。
协程通过yield语句和生成器函数实现任务的暂停和恢复,避免了线程切换的开销并减少了锁的使用。
应用场景:协程常用于异步编程场景,比如网络编程和IO密集型任务。

1.2.1多任务【进程】编程

进程: 一个正在运行的 程序或者软件 就是一个进程,它是 操作系统进行资源分配的基本单位,也就是说每启动一个进程,操作系统都会给其分配一定的运行资源(内存资源)保证进程的运行。

特点:

  • 多个进程交替运行
  • 因为进程是程序执行的最小资源分配单位,当一个子进程被创建时,子进程会复制父进程的资源,形成一个独立的空间,所以 多个进程之间的数据是独立不共享的 ——进程间不共享全局变量。

python中使用multiprocessing实现多任务进程编程:
import multiprocessing

(1)创建进程

multiprocessing 模块使用 Process 类创建进程实例对象,实现进程任务的创建。

#  task1是方法名,不带括号哦
multiprocessing.Process(target=task1, name="myprocess1")

说明: target的参数是函数名,不带括号的

(2)启动进程

import multiprocessing
import time

# 跳舞任务
def task1():
    for i in range(5):
        print("跳舞中...")
        time.sleep(0.2)

if __name__ == '__main__':
    # 创建进程
    p1 = multiprocessing.Process(target=task1, name="myprocess1")
    # 启动进程
    p1.start()

(3)获取当前进程

multiprocessing.current_process() 可以获取当前进程

import multiprocessing
def task1():
    print(multiprocessing.current_process())
def task2():
    print(multiprocessing.current_process())

if __name__ == '__main__':
    p1 = multiprocessing.Process(target=task1, name="myprocess-1")
    p2 = multiprocessing.Process(target=task2)
    p1.start()
    p2.start()
---------------------------输出结果-------------------------------------------------------------------
<Process name='myprocess-1' parent=1472 started>
<Process name='Process-2' parent=1472 started> #此处name是默认的

说明:parent主程序main函数

(4)获取进程名

# 进程对象的 name 属性可以获取进程的名称
import multiprocessing

def task1():
    print(multiprocessing.current_process().name)

if __name__ == '__main__':
    print(multiprocessing.current_process().name)# MainProcess
    p1 = multiprocessing.Process(target=task1, name="myprocess-1")
    p1.start()
----------------------------------输出结果-----------------------------------------------
MainProcess
myprocess-1

(5)获取进程ID

每一个进程产生时,操作系统都分为进程分配一个ID编号,可以通过 os 模块中的方法获取进程的ID。

  • os.getpid() 获取当前进程ID;
  • os.getppid() 获取当前进程的父进程的ID

(6)进程任务函数传参

在创建进程对象的时候,为进程任务函数传递参数,可以使用两种方式为任务函数传参。

  • args: 使用可变位置参数形式传参–传递元组;
  • kwargs: 使用可变关键字参数形式传参–传递字典;
import multiprocessing
import time
def task(n, msg):
    for i in range(n):
        print(multiprocessing.current_process().name, f"打印第 {i+1} 次 {msg}")
        time.sleep(0.2)

if __name__ == '__main__':
    # 使用可变位置参数传参
    p1 = multiprocessing.Process(target=task, args=(10, "Python"))
    # 使用可变关键字参数传参
    p2 = multiprocessing.Process(target=task, kwargs={"n": 10, "msg": "Hogwarts"})
    p1.start()
    p2.start()

(7)进程 同步

join() 方法用来将子进程添加到当前进程之前执行,直到子进程执行结束后,当前进程才会继续向下执行。

原理: 多个进程间的代码在运行时是交替执行的,如果使用 join() 方法后,当前进程会进入到阻塞状态,等待子进程结束后,解除阻塞状态,继续执行当前进程。——即为现在是任务同步执行(非异步)

=》使用 join() 方法后,可使多进程的异步执行变成同步执行, 过多使用会使程序效率变低。

异步与同步执行是指在计算机编程中,程序或函数的执行方式。它们之间的主要区别在于程序等待某个操作完成的方式。

  • 同步执行:同步执行是指程序按照顺序执行,每个操作都必须在下一个操作开始之前完成。这意味着程序在等待某个操作完成时会停止执行其他任务。
  • 异步执行:异步执行是指程序在执行某个操作时,不会停下来等待该操作完成,而是继续执行其他任务。当异步操作完成时,程序会通过回调函数或其他方式通知结果。
import multiprocessing
import time
def task(n, msg):
    for i in range(n):
        print(multiprocessing.current_process().name, f"打印第 {i+1} 次 {msg}")
        time.sleep(0.2)

if __name__ == '__main__':
    # 使用可变位置参数传参
    p1 = multiprocessing.Process(target=task, args=(2, "Python"))
    # 使用可变关键字参数传参
    p2 = multiprocessing.Process(target=task, kwargs={"n": 2, "msg": "Hogwarts"})
    print('main run..1')
    p1.start()
    p1.join()
    p2.start()
    print('main run..2')
----------------------------------输出结果-----------------------------------------------
main run..1
Process-1 打印第1次python
Process-1 打印第2次python
main run..2
Process-2 打印第1次hogwarts
Process-2 打印第2次hogwarts

说明:如果没有join方法,正常main函数里(除了子程序p1、p2相关语句)语句都是在子程序之前运行的
——这种理解不对,实际上是主程序和子程序p1、p2是同时运行的(交替运行的),加了join后会阻塞主程序

import multiprocessing
import time
def task(n, msg):
    for i in range(n):
        print(multiprocessing.current_process().name, f"打印第 {i+1} 次 {msg}")
        time.sleep(0.2)

if __name__ == '__main__':
    # 使用可变位置参数传参
    p1 = multiprocessing.Process(target=task, args=(2, "Python"))
    # 使用可变关键字参数传参
    p2 = multiprocessing.Process(target=task, kwargs={"n": 2, "msg": "Hogwarts"})
    print('main run..1')
    p1.start()
    p2.start()
    print('main run..2')
----------------------------------输出结果-----------------------------------------------
main run..1
main run..2
Process-1 打印第1次python
Process-1 打印第2次python
Process-2 打印第1次hogwarts
Process-2 打印第2次hogwarts

说明: 看着输出像是主进程优先运行结束了,实际上是因为主进程语句没耗费多久,子进程里边有循环,耗时比主进程时间长,如果在主进程中加入sleep语句就可以看出效果(交替运行)。

(8)守护进程

比较:

  • 正常流程:多进程在执行时,父进程会等待子进程执行结束才会结束。
  • 进程同步-join:父进程里执行到子进程,父进程阻塞,子进程执行,子进程执行完后父进程才继续执行。
  • 守护进程:多进程在执行时,父进程结束时候强制子进程结束。

守护进程: 如果需要子进程在父进程执行结束后就结束执行,无论子进程是否执行完毕,可以将子进程设置为守护进程。
比如:只有开启企业微信后,才可以使用企业微信的会议功能,当企业微信退出时,会议也会随之退出。

设置守护进程方式有两种:

  • 使用 子进程对象.daemon = True 在子进程启动前将子进程设置为守护进程。
  • 使用 子进程对象.terminate() 在主进程退出前手动将子进程结束。设置子进程为守护进程

a.设置子进程为守护进程

import multiprocessing
import time
def task(n, msg):
    for i in range(n):
        print(multiprocessing.current_process().name, f"打印第 {i+1} 次 {msg}")
        time.sleep(0.2)

if __name__ == '__main__':

    p1 = multiprocessing.Process(target=task, args=(10, "Python"))
    p2 = multiprocessing.Process(target=task, kwargs={"n": 10, "msg": "Hogwarts"})
    # 在子进程启动前将子进程设置为守护进程,当主进程结束后,无论子进程是否执行完都要结束
    p1.daemon = True
    p1.start()
    p2.start()
    # 主进程睡1秒。。。
    time.sleep(1)
    print("main")
-----------------------------输出结果----------------------------------------------------
Process-1 打印第 1 次 Python
Process-2 打印第 1 次 Hogwarts
Process-1 打印第 2 次 Python
Process-2 打印第 2 次 Hogwarts
Process-1 打印第 3 次 Python
Process-2 打印第 3 次 Hogwarts
Process-1 打印第 4 次 Python
Process-2 打印第 4 次 Hogwarts
Process-1 打印第 5 次 Python
Process-2 打印第 5 次 Hogwarts
main
Process-2 打印第 6 次 Hogwarts
Process-2 打印第 7 次 Hogwarts
Process-2 打印第 8 次 Hogwarts
Process-2 打印第 9 次 Hogwarts
Process-2 打印第 10 次 Hogwarts

Process finished with exit code 0

b.手动杀死子进程

import multiprocessing
import time
def task(n, msg):
    for i in range(n):
        print(multiprocessing.current_process().name, f"打印第 {i+1} 次 {msg}")
        time.sleep(0.2)

if __name__ == '__main__':

    p1 = multiprocessing.Process(target=task, args=(10, "Python"))
    p2 = multiprocessing.Process(target=task, kwargs={"n": 10, "msg": "Hogwarts"})
    p1.start()
    p2.start()
    time.sleep(1)
    # 手动杀死子进程2
    p2.terminate()
    print("main")
-----------------------------输出结果-------------------------------------
Process-1 打印第 1 次 Python
Process-2 打印第 1 次 Hogwarts
Process-1 打印第 2 次 Python
Process-2 打印第 2 次 Hogwarts
Process-1 打印第 3 次 Python
Process-2 打印第 3 次 Hogwarts
Process-1 打印第 4 次 Python
Process-2 打印第 4 次 Hogwarts
Process-1 打印第 5 次 Python
Process-2 打印第 5 次 Hogwarts
main
Process-1 打印第 6 次 Python
Process-1 打印第 7 次 Python
Process-1 打印第 8 次 Python
Process-1 打印第 9 次 Python
Process-1 打印第 10 次 Python

1.2.2多任务【线程】编程

线程: 在一个程序中执行一段指令流。
特点:

  • 在操作系统中,线程是调度执行的最小单位。可以独立运行,并共享线程的资源,比如内存空间、文件句柄。
  • 轻量级:相对于进程来说,线程的创建、切换和销毁的开销较小。
  • 共享资源:线程可以共享线程的资源,包括内存空间、文件句柄等。这使得线程之间可以方便地进行数据共享和通信。
  • 并发执行:线程可以并发执行,即多个线程可以在同一时间内执行不同的任务。线程的并发执行可以提高程序的性能和响应性。
  • 线程安全:线程安全是指多个线程同时访问共享数据时,不会出现数据不一致或异常的情况。在多线程编程中,需要采取一些措施(如锁、互斥量等)来保证线程安全性。
  • 线程必须依附于进程中,线程不能单独存在

python中使用threasing模块实现线程多任务编程。
import threading

(1)创建线程

threading 模块 使用 Thread创建线程实例对象,实现线程任务的创建。

Thread([group [, target [, name [, args [, kwargs [, daemon]]]]]])

参数说明:

  • group:指定线程组,目前只能使用 None
  • target:执行的目标任务名
  • name:线程名字
  • args:以元组方式给执行任务传参
  • kwargs:以字典方式给执行任务传参
  • daemon:设置线程对象为守护线程

(2)启动线程

import threading
import time

# 跳舞任务
def task1():
    for i in range(5):
        print("跳舞中...")
        time.sleep(0.2)

# 唱歌任务
def task2():
    for i in range(5):
        print("唱歌中...")
        time.sleep(0.2)

if __name__ == '__main__':
    t1 = threading.Thread(target=task1, name="mythread-1")
    t2 = threading.Thread(target=task2)
   #    启动线程
    t1.start()
    t2.start()

(3)获取当前线程

threading.current_thread() 可以获取当前线程。

(4)获取线程名

线程对象的 name 属性可以获取线程的名称。
threading.current_thread().name

(5)线程无序性

import threading
import time
def task():
    time.sleep(1)
    print("当前线程:", threading.current_thread().name)

if __name__ == '__main__':
   #  Python中对于无需关注其实际含义的变量可以用_代替,这就和for i in range(5)一样,因为这里我们对i并不关心,只是要5次而已,所以用_代替仅获取值而已。
    for _ in range(5,10):
        t = threading.Thread(target=task)
        t.start()
    time.sleep(1)
    print(threading.current_thread().name)
---------------------------------------------------------------
当前线程: Thread-3
当前线程: Thread-4
当前线程: Thread-1
MainThread
当前线程: Thread-2
当前线程: Thread-5

(6)线程任务函数传参

在创建线程对象的时候,为线程任务函数传递参数,可以使用两种方式为任务函数传参。

  • args: 使用可变位置参数形式传参
  • kwargs: 使用可变关键字参数形式传参
import threading
import time
def task(n, msg):
    for i in range(n):
        print(threading.current_thread().name, f"打印第 {i+1} 次 {msg}")
        time.sleep(0.2)

if __name__ == '__main__':
    # 使用可变位置参数传参
    t1 = threading.Thread(target=task, args=(5, "Python"))
    # 使用可变关键字参数传参
    t2 = threading.Thread(target=task, kwargs={"n": 5, "msg": "Hogwarts"})
    t1.start()
    t2.start()

(7)线程同步

针对子线程

  • join() 方法用来将子线程添加到当前线程之前执行,直到子线程执行结束后,当前线程(主线程)才会继续向下执行。
  • 多个线程间的代码在运行时是交替执行的,如果使用 join() 方法后,当前线程会进入到阻塞状态,等待子线程结束后,解除阻塞状态,继续执行当前线程。
  • 使用 join() 方法后,可使多线程的异步执行变成同步执行, 过多使用会使程序效率变低。

(8)守护线程

针对子线程

  • 多线程在执行时,父线程会等待子线程执行结束才会结束
  • 如果需要子线程在父线程执行结束后就结束执行,无论子线程是否执行完毕,可以将子线程设置为守护线程。
  • 比如:在使用下载软件进行下载多个视频时,每个下载任务都是一个线程,如果下载软件退出,则下载任务也会停止并退出。
  • 设置守护线程方式有两种:——
    • 使用 daemon = True 参数在子线程对象创建时将子线程设置为守护线程。
    • 使用 子线程对象.daemon = True 属性在子线程对象启动前将子线程对象设置为守护线程。
    ...
   # 方式1:
    t1 = threading.Thread(target=task, args=(5, "Python"),daemon=True)
    t2 = threading.Thread(target=task, kwargs={"n": 5, "msg": "Hogwarts"}):
    t1.start()
   # 方式2:
    t2.daemon = True
    time.sleep(1)
    t2.start()

(9)获取线程ID

每一个线程产生时,操作系统都分为进程分配一个ID编号,可以通过 os 模块中的方法获取线程的ID。

  • os.getpid() 获取当前进程ID;
  • os.getppid() 获取当前进程的父进程的ID;

(10)线程共享全局变量

因为线程是程序执行的最小执行单位,当一个子线程被创建时,子线程会使用父线程的资源,所以多个线程之间的数据是共享的。

(11)锁

线程安全问题

  • 线程间可以访问全局变量,在多个线程间进行数据传递时非常方便,但是随之也会产生很大的问题。
  • 当多个线程同时对共享的全局变量进行操作时,可能会出现脏数据的问题。

注意:

  • Python 3.9 版本解释器之前,线程安全问题非常明显
  • Python 3.10 版本后,引入了新的 GIL2.0 版本的锁,有效的提升了线程安全问题,但某些时刻还需要使用互斥锁保证线程安全。

a.互斥锁Mutex
在 Python 中,可以使用互斥锁(Mutex)来保护 共享资源,(目的)避免多个线程同时对共享资源进行写操作,从而避免 竞争条件数据不一致 的问题。

  • 使用 threading.Lock() 获取互斥锁对象。 lock = threading.Lock()
  • 互斥锁操作:
    • 加锁操作: lock.acquire()
    • 解锁操作: lock.release()
import time
import threading

# 定义全局变量
sum = 0
# 定义互斥锁
lock = threading.Lock()

def add_one():
    global sum
    # 加锁
    lock.acquire()
    for i in range(1000000):
        sum += 1
    # 解锁
    lock.release()
    print(threading.current_thread().name , " : ", sum)

if __name__ == '__main__':
    # 创建3个线程
    t1 = threading.Thread(target=add_one)
    t2 = threading.Thread(target=add_one)
    t3 = threading.Thread(target=add_one)
    # 启动线程
    t1.start()
    t2.start()
    t3.start()
    time.sleep(3)
    print(threading.current_thread().name , " : ", sum)
-----------------------------------------------------------------------
Thread-1 (add_one)  :  1000000
Thread-2 (add_one)  :  2000000
Thread-3 (add_one)  :  3000000
MainThread  :  3000000

b.死锁现象
虽然使用互斥锁可以解决线程间数据安全问题,但是,如果互斥锁使用不当,会出现死锁现象。
死锁: 是指一个线程获取锁权限后,并未释放锁,导致其它线程无法获取互斥锁的使用权,持续进行等待的过程。

import threading
import time

# 创建互斥锁
lock = threading.Lock()

numbers = [3, 6, 8, 1, 9]

# 根据下标去取值, 保证同一时刻只能有一个线程去取值
def get_value(index):
    # 上锁
    lock.acquire()
    # 判断下标释放越界
    if index >= len(numbers):
        print(threading.current_thread().name, f"下标 {index} 越界")
        # 如果角标越界就return不会往下执行,那么上的锁就没有释放,出现死锁,程序一直等待,知道电脑崩溃
        return
    value = numbers[index]
    print(threading.current_thread().name, "取值为: ", value)

    time.sleep(0.2)
    # 释放锁
    lock.release()

if __name__ == '__main__':
    # 模拟大量线程去执行取值操作
    for i in range(10):
        sub_thread = threading.Thread(target=get_value, args=(i,))
        sub_thread.start()
-----------------------------------------------------------------------------
Thread-1 (get_value) 取值为:  3
Thread-2 (get_value) 取值为:  6
Thread-3 (get_value) 取值为:  8
Thread-4 (get_value) 取值为:  1
Thread-5 (get_value) 取值为:  9
Thread-6 (get_value) 下标 5 越界

ps:如果角标越界就return不会往下执行,那么上的锁就没有释放,出现死锁,程序一直等待,直到电脑崩溃
说明:退出函数的地方应该加上锁释放,否则会被死锁。

c.避免死锁
可以在程序的合适位置,将锁释放掉,让其它线程对象有能获取到互斥锁的机会。


# 根据下标去取值, 保证同一时刻只能有一个线程去取值
def get_value(index):
    # 上锁
    lock.acquire()
    # 判断下标释放越界
    if index >= len(numbers):
        print(threading.current_thread().name, f"下标 {index} 越界")
        # 如果角标越界就return不会往下执行,那么上的锁就没有释放,出现死锁,程序一直等待,直到电脑崩溃
        # 所以当越界的线程来到这里需要把锁给释放掉,让其他线程可以拿到资源继续运行
        lock.release()
        return
    value = numbers[index]
    print(threading.current_thread().name, "取值为: ", value)

    time.sleep(0.2)
    # 释放锁
    lock.release()

1.2.3多任务【协程】编程

协程: 又称微线程,纤程,Coroutine。
可以在同一个线程中实现多任务的切换和调度
协程优势:

  • 协程通过任务的暂停和恢复,避免了线程切换的开销并减少了锁的使用。协程常用于 异步编程 场景,比如网络编程和IO密集型任务。
  • 最大的优势就是协程 极高的执行效率 。因为函数切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销,和多线程比,协程数量越多,协程的性能优势就越明显。
  • 不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多。

比如:一个人在打印资料的等待过程中,又去接听了客户的电话,在接听电话的等待过程中,又整理了桌面。

使用第三方模块 gevent 实现协程多任务编程

# pip install gevent
import gevent

(1)创建协程

gevent 模块使用 spawn 类创建协程实例对象,实现协程任务的创建。

spawn(run [, args [, kwargs]])

参数说明:

  • run:执行的目标任务名
  • args:以元组方式给执行任务传参
  • kwargs:以字典方式给执行任务传参

(2)启动进程

协程对象创建成功后,需要使用 join() 方法启动协程才会开始执行。——该方法的作用是对当前线程进行阻塞,直到协程执行结束后,继续执行当前线程。

import gevent
import threading
import os

def task():
    print(f"{threading.current_thread().name}_id:{os.getpid()}")
    print(f"{threading.current_thread().name}_pid:{os.getppid()}")
    for i in range(1,3):
        print(i)

g1 = gevent.spawn(task)
g2 = gevent.spawn(task)
g3 = gevent.spawn(task)
g1.join()
g2.join()
g3.join()
print("main")

(3)获取当前协程对象

gevent.getcurrent() 可以获取当前协程对象

import gevent
import threading
import os

def task():
    print(f"{threading.current_thread().name}_id:{os.getpid()}")
    print(f"{threading.current_thread().name}_pid:{os.getppid()}")
    for i in range(1,3):
        print(gevent.getcurrent(),i)

(4)协程组

在创建多个协程对象后,可以将多个协程对象放入一个元组或列表中,然后使用 gevent.joinall() 方法同时启动协程对象。

import gevent

def task():
    for i in range(1,3):
        print(gevent.getcurrent(), i)

# 使用列表推导式,生成一个有5个协程对象的列表
gs = [gevent.spawn(task) for i in range(5)]
gevent.joinall(gs)

(5)协程切换

可以通过在代码中添加 gevent.sleep() 方法模拟耗时操作,实现协程任务的切换。

import gevent

def task():
    for i in range(1,3):
        print(gevent.getcurrent(), i)
        gevent.sleep(0.001)

# 使用列表推导式,生成一个有5个协程对象的列表
gs = [gevent.spawn(task) for i in range(5)]
gevent.joinall(gs)

(6)协程任务函数传参

协程的任务函数传参与进程和线程不同,协程可以和直接使用函数一样,在 spawn 方法中为任务函数传参。
两种方式为任务函数传参:

import gevent

def task(n, msg):
    for i in range(1,n+1):
        print(gevent.getcurrent(), f"第 {i} 次输出 {msg}")
        gevent.sleep(0.001)

g1 = gevent.spawn(task,5, "Python")
g2 = gevent.spawn(task, msg="Hogwarts", n=5)
g1.join()
g2.join()

(7)协程异步

在 Python 中,Gevent 的 monkey patch 是指使用 Gevent 的模块 gevent.monkey 中的 patch_all() 等方法,来替换标准库中的一些阻塞式 I/O 操作,以实现非阻塞式的协程 I/O。
说明:在 Python 3.10 版本中,Gevent 的 monkey patch 功能在某些情况下可能无效。可以考虑使用 Python 3.10 引入的 asyncio 模块来进行异步编程。

from gevent import monkey
# 该方法一般写在第一行
monkey.patch_all()
import gevent
import random
import asyncio

def task(n, msg):
    for i in range(1,n+1):
        print(gevent.getcurrent(), f"第 {i} 次输出 {msg}")
        gevent.sleep(random.random())

g1 = gevent.spawn(task,5, "Python")
g2 = gevent.spawn(task, msg="Hogwarts", n=5)
g3 = gevent.spawn(task, n=5, msg="Hello")
gevent.joinall((g1,g2,g3))