python3之进程、线程、协程

本文内容参考

多进程

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.
测试过程中发现了一个问题:
from multiprocessing import Process


print("Parent Process ({}) start".format(os.getpid()))


def run_proc(name):
    print("Run child process {} ({})...".format(name, os.getpid()))
    time.sleep(random.random())
    print("child process {} ({}) done".format(name, os.getpid()))


if __name__ == "__main__":
    p = Process(target=run_proc, args=("test",))
    print("Child process will start.")
    p.start()
    p.join()
    print("Child process end.")

输出内容让我觉得有些迷糊:

Parent Process (22880) start
Child process will start.
Parent Process (16272) start
Run child process test (16272)...
child process test (16272) done
Child process end.

还有

from multiprocessing import Process


print("Parent Process ({}) start".format(os.getpid()))


def run_proc(name):
    print("Run child process {} ({})...".format(name, os.getpid()))
    time.sleep(random.random())
    print("child process {} ({}) done".format(name, os.getpid()))


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 (18116) start
Parent Process (18116) start...
Child process will start.
Parent Process (11684) start
Run child process test (11684)...
child process test (11684) 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()))
    
# 进程池
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("{} before wait".format(threading.current_thread().name))
        cond.wait()
        print("{}  after wait".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) before wait
Thread-2 (read) before wait
Thread-3 (write) before notify all
Thread-3 (write) after notify all
Thread-1 (read)  after wait
Thread-2 (read)  after wait

队列

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

 协程

# 协程存在的意义:对于多线程应用,CPU通过切片的方式来切换线程间的执行,线程切换时需要耗时(保存状态,下次继续)。
# 协程,则只使用一个线程,在一个线程中规定某个代码块执行顺序。
# 协程的适用场景:当程序中存在大量不需要CPU的操作时(IO),适用于协程;

# 由async修饰的普通函数变成了异步函数,异步函数不同于普通函数不可能被直接调用

# await的作用是挂起自身的协程,直到await修饰的协程完成并返回结果
# await只能使用在有async修饰的函数中不然会报错

import asyncio, time


async def cor1(num):
    print("COR1 start")
    res = await cor2(num)
    print("COR1 end")
    return res


async def cor2(num):
    print("COR2", num, time.time() - now_time)
    return num


async def main():
    print("main start")

    tasks = []
    for num in range(1, 4):
        tasks.append(asyncio.create_task(cor1(num)))

    # for task in tasks:
    #     await task

    # response = await asyncio.gather(tasks[0], tasks[1], tasks[2])  # 将task作为参数传入gather,等异步任务都结束后返回结果列表
    # print(response)

    print("main end")


def run(func, num):  # 用try解决报错问题,运行协程函数
    try:
        func(num).send(None)
    except StopIteration as r:
        return r.value


now_time = time.time()
asyncio.run(cor1(1))  ## 用asyncio.run直接运行协程参数为协程函数及其参数
print("执行时间:{}".format(time.time() - now_time))
print("----------------------")

now_time = time.time()
asyncio.run(main())
print("执行时间:{}".format(time.time() - now_time))
print("----------------------")

# 调用协程
# cor2(2).send(None) # StopIteration 2
res = run(cor2, 2)
print(res)  # 2
res = run(cor1, 1)
print(res)  # 1
输出
COR1 start
COR2 1 0.002824068069458008    
COR1 end
执行时间:0.0037648677825927734
----------------------
main start
main end
COR1 start
COR2 1 0.0
COR1 end
COR1 start
COR2 2 0.0
COR1 end
COR1 start
COR2 3 0.0
COR1 end
执行时间:0.0014872550964355469
----------------------
COR2 2 0.0014872550964355469
2
COR1 start
COR2 1 0.0014872550964355469
COR1 end
1

 

当main()中await修饰的相关语句取消注释时,输出如下:
----------------------        
main start
COR1 start
COR2 1 0.0014913082122802734  
COR1 end
COR1 start
COR2 2 0.0014913082122802734  
COR1 end
COR1 start
COR2 3 0.0014913082122802734  
COR1 end
[1, 2, 3]
main end
执行时间:0.0025000572204589844
----------------------

相关概念说明 

 # 进程

进程就是一个程序在一个数据集上的一次动态执行过程。 进程一般由程序、数据集、进程控制块三部分组成。程序用来描述进程要完成哪些功能以及如何完成;数据集则是程序在执行过程中所需要使用的资源;进程控制块用来记录进程的外部特征,描述进程的执行变化过程,系统可以利用它来控制和管理进程,它是系统感知进程存在的唯一标志。

# 线程

线程是属于进程的,线程运行在进程空间内,同一进程所产生的线程共享同一内存空间。当进程退出时该进程所产生的线程都会被强制退出并清除。
线程可与属于同一进程的其它线程共享进程所拥有的全部资源。
线程指的是进程中一个单一顺序的控制流。一个进程中可以并发多个线程,每条线程并行执行不同的任务。

由于同一进程中的多个线程共享数据,在程序执行中线程切换可能出现数据异常的情况,因此引入了线程锁。

# 多任务

要实现多任务,通常会设计 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 呢?最简单的方法是多进程+协程,既充分利用多核,又充分发挥协程的高效率,可获得极高的性能。

 
posted @ 2024-01-17 13:47  carol2014  阅读(14)  评论(0编辑  收藏  举报