python3之进程、线程、协程
相关概念说明
进程
进程就是一个程序在一个数据集上的一次动态执行过程。 进程一般由程序、数据集、进程控制块三部分组成。程序用来描述进程要完成哪些功能以及如何完成;数据集则是程序在执行过程中所需要使用的资源;进程控制块用来记录进程的外部特征,描述进程的执行变化过程,系统可以利用它来控制和管理进程,它是系统感知进程存在的唯一标志。
线程
线程是属于进程的,线程运行在进程空间内,同一进程所产生的线程共享同一内存空间。当进程退出时该进程所产生的线程都会被强制退出并清除。
线程可与属于同一进程的其它线程共享进程所拥有的全部资源。
线程指的是进程中一个单一顺序的控制流。一个进程中可以并发多个线程,每条线程并行执行不同的任务。
由于同一进程中的多个线程共享数据,在程序执行中线程切换可能出现数据异常的情况,因此引入了线程锁。
多任务
要实现多任务,通常会设计 Master-Worker 模式,Master 负责分配任务,Worker 负责执行任务,因此,多任务环境下,通常是一个 Master,多个 Worker。
如果用多进程实现 Master-Worker,主进程就是 Master,其他进程就是 Worker。
如果用多线程实现 Master-Worker,主线程就是 Master,其他线程就是 Worker。
多进程模式最大的优点就是稳定性高,因为一个子进程崩溃了,不会影响主进程和其他子进程。当然主进程挂了所有进程就全挂了,但是通常 Master 进程只负责分配任务,挂掉的概率低。
多进程模式的缺点是创建进程的代价大,尤其是在 Windows 下。另外,操作系统能同时运行的进程数也是有限的。
多线程模式通常比多进程稍快一点,但是由于所有线程共享进程的内存,多线程模式任何一个线程挂掉都可能直接造成整个进程崩溃,导致多线程模式稳定性问题。
在 Windows 下,多线程的效率比多进程要高,但由于多线程存在稳定性的问题,又有多进程+多线程的混合模式。
进程、线程切换
操作系统在切换进程或者线程时需要先保存当前执行的现场环境(CPU 寄存器状态、内存页等),然后把新任务的执行环境准备好(恢复上次的寄存器状态,切换内存页等),才能开始执行。这个切换过程虽然很快,但是也需要耗费时间。如果有几千个任务同时进行,操作系统可能就主要忙着切换任务,根本没有多少时间去执行任务了。
所以,多任务一旦多到一个限度,就会消耗掉系统所有的资源,结果效率急剧下降,所有任务都做不好。
计算密集型 vs. IO 密集型
计算密集型任务的特点是要进行大量的计算,消耗 CPU 资源,比如计算圆周率、对视频进行高清解码等等,全靠 CPU 的运算能力。这种计算密集型任务虽然也可以用多任务完成,但是任务越多,花在任务切换的时间就越多,CPU 执行任务的效率就越低,所以,要最高效地利用 CPU,计算密集型任务同时进行的数量应当等于 CPU 的核心数。
计算密集型任务由于主要消耗 CPU 资源,因此,代码运行效率至关重要。Python 这样的脚本语言运行效率很低,完全不适合计算密集型任务。对于计算密集型任务,最好用 C 语言编写。
涉及到网络、磁盘 IO 的任务都是 IO 密集型任务,这类任务的特点是 CPU 消耗很少,任务的大部分时间都在等待 IO 操作完成(IO 的速度远远低于 CPU 和内存的速度)。对于 IO 密集型任务,任务越多,CPU 效率越高,但也有一个限度。常见的大部分任务都是 IO 密集型任务,比如 Web 应用。
IO 密集型任务执行期间,99%的时间都花在 IO 上,花在 CPU 上的时间很少,因此,用运行速度极快的 C 语言替换用 Python 这样运行速度极低的脚本语言,完全无法提升运行效率。对于 IO 密集型任务,最合适的语言就是开发效率最高(代码量最少)的语言,脚本语言是首选,C 语言最差。
异步 IO
考虑到 CPU 和 IO 之间巨大的速度差异,一个任务在执行的过程中大部分时间都在等待 IO 操作,单进程单线程模型会导致别的任务无法并行执行,因此,我们才需要多进程模型或者多线程模型来支持多任务并发执行。
现代操作系统对 IO 操作已经做了巨大的改进,最大的特点就是支持异步 IO。如果充分利用操作系统提供的异步 IO 支持,就可以用单进程单线程模型来执行多任务,这种全新的模型称为事件驱动模型。
Nginx 就是支持异步 IO 的 Web 服务器,它在单核 CPU 上采用单进程模型就可以高效地支持多任务。在多核 CPU 上,可以运行多个进程(数量与 CPU 核心数相同),充分利用多核 CPU。
协程
协程 Coroutine,又称微线程,用户线程
子程序,或者称为函数,在所有语言中都是层级调用,比如 A 调用 B,B 在执行过程中又调用了 C,C 执行完毕返回,B 执行完毕返回,最后是 A 执行完毕。
所以子程序调用是通过栈实现的,一个线程就是执行一个子程序。
子程序调用总是一个入口,一次返回,调用顺序是明确的。而协程的调用和子程序不同。
协程看上去也是子程序,但执行过程中,在子程序内部可中断,然后转而执行别的子程序,在适当的时候再返回来接着执行。
协程的特点在于是一个线程执行,那和多线程比,协程有何优势?
最大的优势就是协程极高的执行效率。因为子程序切换不是线程切换,而是由程序自身控制,因此没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显。
第二大优势就是不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多。
协程是一个线程执行,那怎么利用多核 CPU 呢?最简单的方法是多进程+协程,既充分利用多核,又充分发挥协程的高效率,可获得极高的性能。
多进程
os模块的fork()
多进程(multiprocessing):Unix/Linux操作系统提供了一个fork()函数,fork()函数调用时,操作系统自动把当前进程(父进程)复制了一份(子进程),然后分别在父进程和子进程内返回。
子进程永远返回0,而父进程返回子进程的ID。
import os
print("Parent Process ({}) start".format(os.getpid()))
# 下面的5行代码只在Unix/Linux/Mac系统生效,windows系统os没有fork
try:
pid = os.fork()
if pid == 0:
print("child process ({}) ,parent process ({})".format(os.getpid(), os.getppid()))
else:
print("parent process ({}) fork a child process ({}).".format(os.getpid(), pid))
except AttributeError as err:
print(err)
windows环境输出
Parent Process (4612) start
module 'os' has no attribute 'fork'
multiprocessing模块
windows 环境中多进程可以使用multiprocessing模块
创建子进程时,只需要传入一个执行函数和函数的参数,创建一个Process实例,用start()方法启动,join()方法可以等待子进程结束后再继续往下运行,通常用于进程间的同步。
from multiprocessing import Process
# 子进程要执行的代码
def run_proc(name):
print("Run child process {} ({})...".format(name, os.getpid()))
time.sleep(random.random())
print("child process {} ({}) done".format(name, os.getpid()))
# 创建子进程时,只需要传入一个执行函数和函数的参数,创建一个Process实例,
# 用start()方法启动,join()方法可以等待子进程结束后再继续往下运行,通常用于进程间的同步。
if __name__ == "__main__":
print("Parent Process ({}) start...".format(os.getpid()))
p = Process(target=run_proc, args=("test",))
print("Child process will start.")
p.start()
p.join()
print("Child process end.")
输出
Parent Process (6304) start...
Child process will start.
Run child process test (20676)...
child process test (20676) done
Child process end.
进程池
import time, random
from multiprocessing import Process, Pool
def run_proc(name):
print("Run child process {} ({})...".format(name, os.getpid()))
time.sleep(random.random())
print("child process {} ({}) done".format(name, os.getpid()))
# 进程池
# Pool的默认大小是CPU的核数
# 对Pool对象调用join()方法会等待所有子进程执行完毕,
# 调用join()之前必须先调用close(),调用close()之后就不能继续添加新的Process了。
if __name__ == "__main__":
print("Parent Process ({}) start...".format(os.getpid()))
p = Pool(4)
for i in range(5):
p.apply_async(run_proc, args=(i,))
print("Waiting for all subprocesses done...")
p.close()
p.join()
print("All subprocesses done.")
输出
Parent Process (836) start...
Waiting for all subprocesses done...
Run child process 0 (19948)...
Run child process 1 (5084)...
Run child process 2 (14552)...
Run child process 3 (7928)...
child process 0 (19948) done
child process 2 (14552) done
Run child process 4 (19948)...
child process 1 (5084) done
child process 3 (7928) done
child process 4 (19948) done
All subprocesses done.
外部子进程
# 子进程-外部进程
import subprocess
r = subprocess.call(["python", "-V"])
print("Exit code:", r)
输出
Python 3.10.8
Exit code: 0
进程间通信-队列
# 进程间通信
# multiprocessing模块提供了Queue、Pipes等多种方式来交换数据来实现进程间的通信。
from multiprocessing import Queue, Process
import random, time, os
# 写数据进程
def write(q):
print("Process to write:{}".format(os.getpid()))
for value in ["A", "B", "C"]:
print("Put {} to queue ".format(value))
q.put(value)
time.sleep(random.random())
def read(q):
print("Process to read: {}".format(os.getpid()))
while True:
value = q.get(True)
print("Get {} from queue".format(value))
if __name__ == "__main__":
# 父进程创建Queue,并传给各个子进程:
q = Queue()
pw = Process(target=write, args=(q,))
pr = Process(target=read, args=(q,))
# 启动子进程pw,写入:
pw.start()
# 启动子进程pr,读取:
pr.start()
# 等待pw结束:
pw.join()
# pr进程里是死循环,无法等待其结束,只能强行终止:
pr.terminate()
输出
Process to write:10704
Put A to queue
Process to read: 21360
Get A from queue
Put B to queue
Get B from queue
Put C to queue
Get C from queue
多线程
import time, threading
# Thread方法说明
# t.start() : 激活线程,
# t.getName() : 获取线程的名称
# t.setName() : 设置线程的名称
# t.name : 获取或设置线程的名称
# t.is_alive() : 判断线程是否为激活状态
# t.isAlive() :判断线程是否为激活状态
# t.setDaemon() 设置为后台线程或前台线程(默认:False);
# 通过一个布尔值设置线程是否为守护线程,必须在执行start()方法之后才可以使用。
# 如果是后台线程,主线程执行过程中,后台线程也在进行,主线程执行完毕后,后台线程不论成功与否,均停止;
# 如果是前台线程,主线程执行过程中,前台线程也在进行,主线程执行完毕后,等待前台线程也执行完成后,程序停止
# t.isDaemon() : 判断是否为守护线程
# t.ident :获取线程的标识符。线程标识符是一个非零整数,只有在调用了start()方法之后该属性才有效,否则它只返回None。
# t.join() :逐个执行每个线程,执行完毕后继续往下执行,该方法使得多线程变得无意义
# t.run() :线程被cpu调度后自动执行线程对象的run方法
# 新线程执行的代码:
def loop():
print("thread {} is running".format(threading.current_thread().name))
n = 0
while n < 5:
n = n + 1
print(threading.current_thread().name, n)
time.sleep(1)
print("thread {} end".format(threading.current_thread().name))
print("thread {} is running".format(threading.current_thread().name))
t = threading.Thread(target=loop, name="LoopThread")
t.start()
t.join()
print("thread {} end".format(threading.current_thread().name))
输出
thread MainThread is running
thread LoopThread is running
LoopThread 1
LoopThread 2
LoopThread 3
LoopThread 4
LoopThread 5
thread LoopThread end
thread MainThread end
线程锁
多线程和多进程最大的不同在于:
多进程中,同一个变量,各自有一份拷贝存在于每个进程中,互不影响,
而多线程中,所有变量都由所有线程共享,所以,任何一个变量都可以被任何一个线程修改,因此引入了线程锁。
import threading
import time
globals_num = 0
def func():
lock.acquire() # 获得锁
global globals_num
globals_num += 1
time.sleep(1)
print(globals_num)
lock.release() # 释放锁
# 由于线程之间是进行随机调度,并且每个线程可能只执行n条执行之后,CPU接着执行其他线程。为了保证数据的准确性,引入了锁的概念。
# RLock允许在同一线程中被多次acquire。而Lock却不允许这种情况。 如果使用RLock,那么acquire和release必须成对出现,即调用了n次acquire,必须调用n次的release才能真正释放所占用的琐。
lock = threading.Lock()
# lock = threading.RLock()
for i in range(5):
t = threading.Thread(target=func)
t.start()
输出
1
2
3
4
5
线程事件
import threading
def func(event):
print("{} start".format(threading.current_thread().name))
event.wait()
print("{} execute".format(threading.current_thread().name))
evt = threading.Event()
for i in range(5):
t = threading.Thread(target=func, args=(evt,))
t.start()
# python线程的事件用于主线程控制其他线程的执行,事件主要提供了三个方法 set、wait、clear。
# 事件处理的机制:全局定义了一个'Flag',如果'Flag'值为 False,那么当程序执行 event.wait 方法时就会阻塞,如果'Flag'值为True,那么event.wait 方法时便不再阻塞。
# clear:将'Flag'设置为False
# set:将'Flag'设置为True
# Event.isSet() :判断标识位是否为Ture。
evt.clear()
input_str = ""
while True:
input_str = input("input:") + "\n"
if input_str.strip() == "true":
evt.set()
break
输出
Thread-1 (func) start
Thread-2 (func) start
Thread-3 (func) start
Thread-4 (func) start
Thread-5 (func) start
input:false
input:1
input:true
Thread-1 (func) execute
Thread-2 (func) execute
Thread-5 (func) execute
Thread-4 (func) execute
Thread-3 (func) execute
threading.Condition
import threading
import time
# Condition实现了一个conditon变量。 这个conditiaon变量允许一个或多个线程等待,直到他们被另一个线程通知。
# 如果有等待的线程,notify()方法会唤醒一个在等待conditon变量的线程。notify_all() 则会唤醒所有在等待conditon变量的线程。
def read(cond):
with cond:
print("{} wait".format(threading.current_thread().name))
cond.wait()
print("{} execute".format(threading.current_thread().name))
def write(cond):
with cond:
print("{} before notify all".format(threading.current_thread().name))
cond.notify_all()
print("{} after notify all".format(threading.current_thread().name))
condition = threading.Condition()
for i in range(2):
tr = threading.Thread(target=read, args=(condition,))
tr.start()
time.sleep(2)
tw = threading.Thread(target=write, args=(condition,))
tw.start()
输出
Thread-1 (read) wait
Thread-2 (read) wait
Thread-3 (write) before notify all
Thread-3 (write) after notify all
Thread-1 (read) execute
Thread-2 (read) execute
队列
import queue, threading
# 队列,线程安全
# q = queue.Queue(maxsize=0) # 构造一个先进先出队列,maxsize指定队列长度,为0 时,表示队列长度无限制。
# q.join() # 等到队列为空的时候,在执行别的操作
# q.qsize() # 返回队列的大小 (不可靠)
# q.empty() # 当队列为空的时候,返回True 否则返回False (不可靠)
# q.full() # 当队列满的时候,返回True,否则返回False (不可靠)
# q.put(item, block=True, timeout=None) # 将item放入Queue尾部
# 可选参数block默认为True,表示当队列满时,会等待队列给出可用位置,block为False时为非阻塞,此时如果队列已满,会引发queue.Full 异常。
# 可选参数timeout,表示设置阻塞时间,超时后如果队列无法给出放入item的位置,则引发 queue.Full 异常
# q.get(block=True, timeout=None) # 移除并返回队列头部的一个值
# 可选参数block默认为True,表示获取值的时候,如果队列为空,则阻塞,为False时,不阻塞,若此时队列为空,则引发 queue.Empty异常。
# 可选参数timeout,表示设置阻塞时间,超时后如果队列为空,则引发Empty异常。
# q.put_nowait(item) # 等效于 put(item,block=False)
# q.get_nowait() # 等效于 get(item,block=False)
q = queue.Queue(5)
def write(i):
q.put(i)
print("{} put {}".format(threading.current_thread().name, i))
def read(i):
msg = q.get()
print("{} get {}".format(threading.current_thread().name, msg))
for i in range(7):
t = threading.Thread(target=write, args=(i,))
t.start()
for i in range(6):
t = threading.Thread(target=read, args=(i,))
t.start()
输出
Thread-1 (write) put 0
Thread-2 (write) put 1
Thread-3 (write) put 2
Thread-4 (write) put 3
Thread-5 (write) put 4
Thread-8 (read) get 0
Thread-6 (write) put 5
Thread-9 (read) get 1
Thread-10 (read) get 2
Thread-7 (write) put 6
Thread-11 (read) get 3
Thread-12 (read) get 4
Thread-13 (read) get 5
threading.local
# 在多线程环境下,一个线程使用自己的局部变量比使用全局变量好,因为局部变量只有线程自己能看见,不会影响其他线程,而全局变量的修改必须加锁。
# 一个ThreadLocal变量虽然是全局变量,但每个线程都只能读写自己线程的独立副本,互不干扰。
# ThreadLocal解决了参数在一个线程中各个函数之间互相传递的问题。
import threading
local = threading.local()
def func2():
print(threading.current_thread().name, local.params)
def func(num):
local.params = num
func2()
t1 = threading.Thread(target=func, args=(2,))
t2 = threading.Thread(target=func, args=(3,))
t1.start()
t2.start()
输出
Thread-1 (func) 2
Thread-2 (func) 3
协程
通过generator实现的协程
def consumer():
r = ""
while True:
n = yield r
if not n:
return
print("[CONSUMER] Consuming %s..." % n)
r = "200 OK"
def producer(c):
c.send(None)
n = 0
while n < 5:
n = n + 1
print("[PRODUCER] Producing %s..." % n)
r = c.send(n)
print("[PRODUCER] Consumer return: %s" % r)
c.close()
c = consumer()
producer(c)
# consumer函数是一个generator,把一个consumer传入produce后:
# 首先调用c.send(None)启动生成器;
# 然后,一旦生产了东西,通过c.send(n)切换到consumer执行;
# consumer通过yield拿到消息,处理,又通过yield把结果传回;
# produce拿到consumer处理的结果,继续生产下一条消息;
# produce决定不生产了,通过c.close()关闭consumer,整个过程结束。
# 整个流程无锁,由一个线程执行,produce和consumer协作完成任务,所以称为“协程”
使用async和await实现的协程
# 在Python中,可以通过使用async关键字定义协程函数,使用await关键字挂起协程的执行,等待耗时操作完成后再恢复执行。
# 由async修饰的普通函数变成了异步函数,异步函数不同于普通函数不可能被直接调用
# await的作用是挂起自身的协程,直到await修饰的协程完成并返回结果
# await只能使用在有async修饰的函数中不然会报错
import time, random
# 使用async和await实现的协程
async def info(name):
print("coroutine info , name:{}".format(name))
res = await getRemote(name)
print("coroutine info , res:{}".format(res))
return res
async def getRemote(name):
print("coroutine getRemote , name:{}".format(name))
time.sleep(2)
return "{}-{}".format(name, random.randint(10, 99))
# 调用协程
# info("lily").send(None) # 报错信息:StopIteration lily-70
def run(func, num):
try:
func(num).send(None)
except StopIteration as r:
return r.value
# 用try解决上面的报错问题,运行协程函数
res = run(info, "lily")
print(res)
# coroutine info , name:lily
# coroutine getRemote , name:lily
# coroutine info , res:lily-68
# lily-68
res = run(getRemote, "rose")
print(res)
# coroutine getRemote , name:rose
# rose-23
异步IO
# 异步编程是一种编写能够在单线程中同时处理多个任务的编程方式。
# 与传统的同步编程相比,异步编程的主要优势在于能够提高程序的并发性和响应性,尤其适用于IO密集型任务,如网络通信、数据库访问等。
# asyncio是Python 3.4版本引入的标准库,用于实现异步编程。它基于事件循环(Event Loop)模型,通过协程(Coroutines)来实现任务的切换和调度。
# 在asyncio中可以使用async关键字定义协程函数,并使用await关键字挂起协程的执行。
# 异步编程的核心思想是避免阻塞,即当一个任务在等待某个操作完成时,可以让出CPU执行权,让其他任务继续执行。这样可以充分利用CPU的时间片,提高程序的整体效率。
# 事件循环
# 事件循环是异步编程的核心机制,它负责协调和调度协程的执行,以及处理IO操作和定时器等事件。
# 它会循环监听事件的发生,并根据事件的类型选择适当的协程进行调度。
import asyncio
async def hello(name):
print("coroutine hello start")
await asyncio.sleep(1)
print("hello {}".format(name))
return name
# 用asyncio.run直接运行协程,参数为协程函数及其参数
res = asyncio.run(hello("lily"))
print(res)
# coroutine hello start
# hello lily
# lily
# 创建task
async def main():
print("main start")
tasks = []
for l in ["lily", "rose"]:
tasks.append(asyncio.create_task(hello(l)))
for task in tasks:
await task
print("main end")
# asyncio.run(main())
# main start
# coroutine hello start
# coroutine hello start
# hello lily
# hello rose
# main end
# 创建task,并收集返回值
async def main_g():
print("main_g start")
tasks = []
for l in ["lily", "rose"]:
tasks.append(asyncio.create_task(hello(l)))
response = await asyncio.gather(tasks[0], tasks[1]) # 将task作为参数传入gather,等异步任务都结束后返回结果列表
print(response)
print("main_g end")
# asyncio.run(main_g())
# main_g start
# coroutine hello start
# coroutine hello start
# hello lily
# hello rose
# ['lily', 'rose']
# main_g end
本文内容参考总结自:
https://liaoxuefeng.com/books/python/process-thread/process/index.html
https://www.cnblogs.com/aylin/p/5601969.html
https://www.cnblogs.com/Red-Sun/p/16934843.html
https://www.cnblogs.com/aylin/p/5601969.html
https://www.cnblogs.com/Red-Sun/p/16934843.html
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 【自荐】一款简洁、开源的在线白板工具 Drawnix