并发编程

引入

必备理论基础

一 操作系统的作用:
    1:隐藏丑陋复杂的硬件接口,提供良好的抽象接口
    2:管理、调度进程,并且将多个进程对硬件的竞争变得有序

二 多道技术:
    1.产生背景:针对单核,实现并发
    ps:
    现在的主机一般是多核,那么每个核都会利用多道技术
    有4个cpu,运行于cpu1的某个程序遇到io阻塞,会等到io结束再重新调度,会被调度到4个
    cpu中的任意一个,具体由操作系统调度算法决定。
    
    2.空间上的复用:如内存中同时有多道程序
    3.时间上的复用:复用一个cpu的时间片
       强调:遇到io切,占用cpu时间过长也切,核心在于切之前将进程的状态保存下来,这样
            才能保证下次切换回来时,能基于上次切走的位置继续运行

什么是进程

程序即数据和代码,进程则可以简单的理解为把程序跑起来。

在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。

只要运行中的程序 都有PID processes ID

每一个进程之间的空间都是独立的

进程的概念

第一,进程是一个实体。每一个进程都有它自己的地址空间,一般情况下,包括文本区域(text region)、数据区域(data region)和堆栈(stack region)。文本区域存储处理器执行的代码;数据区域存储变量和进程执行期间使用的动态分配的内存;堆栈区域存储着活动过程调用的指令和本地变量。

第二,进程是一个“执行中的程序”。程序是一个没有生命的实体,只有处理器赋予程序生命时(操作系统执行之),它才能成为一个活动的实体,我们称其为进程。

第三,进程是操作系统中最基本、重要的概念。是多道程序系统出现后,为了刻画系统内部出现的动态情况,描述系统内部各道程序的活动规律引进的一个概念,所有多道程序设计操作系统都建立在进程的基础上。

操作系统引入进程概念的原因

从理论角度看,是对正在运行的程序过程的抽象;
从实现角度看,是一种数据结构,目的在于清晰地刻画动态系统的内在规律,有效管理和调度进入计算机系统主存储器运行的程序。

进程的特征

动态性:进程的实质是程序在多道程序系统中的一次执行过程,进程是动态产生,动态消亡的。
并发性:任何进程都可以同其他进程一起并发执行
独立性:进程是一个能独立运行的基本单位,同时也是系统分配资源和调度的独立单位;
异步性:由于进程间的相互制约,使进程具有执行的间断性,即进程按各自独立的、不可预知的速度向前推进
结构特征:进程由程序、数据和进程控制块三部分组成。
多个不同的进程可以包含相同的程序:一个程序在不同的数据集里就构成不同的进程,能得到不同的结果;但是执行过程中,程序不能发生改变。

进程与程序的区别

程序是指令和数据的有序集合,其本身没有任何运行的含义,是一个静态的概念。
而进程是程序在处理机上的一次执行过程,它是一个动态的概念。
程序可以作为一种软件资料长期存在,而进程是有一定生命期的。
程序是永久的,进程是暂时的。

注意:同一个程序执行两次,就会在操作系统中出现两个进程,所以我们可以同时运行一个软件,分别做不同的事情也不会混乱。


进程的并行与并发

并行 : 并行是指两者同时执行,比如赛跑,两个人都在不停的往前跑;(资源够用,比如三个线程,四核的CPU )

并发 : 并发是指资源有限的情况下,两者交替轮流使用资源,比如一段路(单核CPU资源)同时只能过一个人,A走一段后,让给B,B用完继续给A ,交替使用,目的是提高效率。

区别:

并行是从微观上,也就是在一个精确的时间片刻,有不同的程序在执行,这就要求必须有多个处理器。
并发是从宏观上,在一个时间段上可以看出是同时执行的,比如一个服务器同时处理多个session。

I/O

和内存直接打交道 不涉及计算的 都是I/O,诸如input() time.sleep() 读写文件操作 socket中的recv()和accept()等

多道

一个小栗子

老教授给CPU执行一个任务,CPU需要耗时间24小时才可以执行完毕

研究生给CPU执行一个任务,CPU耗时3分钟就可以执行完毕

当老教授将耗时长达24小时的任务交给CPU时,CPU开始执行这个任务。然而,研究生们也需要将自己的任务提交给CPU执行,而每个研究生的任务只需要3分钟就能完成。

面对这个情况,CPU需要能够公平地分配时间给每个任务,以便能够高效地处理多个任务。这就引入了分时系统的概念。

分时系统是一种操作系统的调度方式,它将CPU的时间分成小的时间片,并在每个时间片内轮流给每个任务分配CPU时间。在我们的故事中,CPU会按照时间片的方式切换任务,每个任务获得平均的CPU时间。

当第一个研究生将自己的任务提交给CPU时,CPU会分配一部分时间给这个任务。而在这个时间片内,研究生的任务能够迅速完成。然后,CPU会切换到老教授的任务,以确保每个任务都能够得到执行。

当更多的研究生加入进来时,CPU会按照相同的方式分配时间给每个任务。每个研究生的任务都能够在自己的时间片内完成,而CPU会在不同任务之间进行切换,以保证每个任务都得到公平的执行时间。

通过分时系统和多道技术的结合,CPU能够高效地处理多个任务。这种方式不仅能够提高任务的执行效率,还能够合理利用CPU资源,确保每个任务都能够得到公平的执行机会。

这个故事简短地展示了分时系统和多道技术在处理多个任务时的重要性。通过合理调度和切换任务,我们能够充分利用计算机资源,提高任务的执行效率,同时保证公平性和公正性。这也是操作系统和并发编程中的重要概念之一。

同步异步阻塞非阻塞

image-20240116204718685

在了解其他概念之前,我们首先要了解进程的几个状态。在程序运行的过程中,由于被操作系统的调度算法控制,程序会进入几个状态:就绪,运行和阻塞。

  (1)就绪(Ready)状态

  当进程已分配到除CPU以外的所有必要的资源,只要获得处理机便可立即执行,这时的进程状态称为就绪状态。

  (2)执行/运行(Running)状态当进程已获得处理机,其程序正在处理机上执行,此时的进程状态称为执行状态。

  (3)阻塞(Blocked)状态正在执行的进程,由于等待某个事件发生而无法执行时,便放弃处理机而处于阻塞状态。引起进程阻塞的事件可有多种,例如,等待I/O完成、申请缓冲区不能满足、等待信件(信号)等。

img

同步和异步

同步:同一时间只能做一件事情,就是同步
异步:两件事情可以同时进行就是异步

socketserver就是异步

img

所谓同步就是一个任务的完成需要依赖另外一个任务时,只有等待被依赖的任务完成后,依赖的任务才能算完成,这是一种可靠的任务序列。

要么成功都成功,失败都失败,两个任务的状态可以保持一致。

所谓异步是不需要等待被依赖的任务完成,只是通知被依赖的任务要完成什么工作,依赖的任务也立即执行,只要自己完成了整个任务就算完成了。

至于被依赖的任务最终是否真正完成,依赖它的任务无法确定,所以它是不可靠的任务序列。

比如我去银行办理业务,可能会有两种方式:
第一种 :选择排队等候;
第二种 :选择取一个小纸条上面有我的号码,等到排到我这一号时由柜台的人通知我轮到我去办理业务了;

第一种:前者(排队等候)就是同步等待消息通知,也就是我要一直在等待银行办理业务情况;

第二种:后者(等待别人通知)就是异步等待消息通知。在异步消息处理中,等待消息通知者(在这个例子中就是等待办理业务的人)往往注册一个回调机制,在所等待的事件被触发时由触发机制(在这里是柜台的人)通过某种机制(在这里是写在小纸条上的号码,喊号)找到等待该事件的人。

阻塞与非阻塞

阻塞:I/O操作之类

非阻塞:程序不管走到哪儿都不理会阻塞就是非阻塞 不care阻塞

阻塞和非阻塞这两个概念与程序(线程)等待消息通知(无所谓同步或者异步)时的状态有关。也就是说阻塞与非阻塞主要是程序(线程)等待消息通知时的状态角度来说的

继续上面的那个例子,不论是排队还是使用号码等待通知,如果在这个等待的过程中,等待者除了等待消息通知之外不能做其它的事情,那么该机制就是阻塞的,表现在程序中,也就是该程序一直阻塞在该函数调用处不能继续往下执行。
相反,有的人喜欢在银行办理这些业务的时候一边打打电话发发短信一边等待,这样的状态就是非阻塞的,因为他(等待者)没有阻塞在这个消息通知上,而是一边做自己的事情一边等待。

注意:同步非阻塞形式实际上是效率低下的,想象一下你一边打着电话一边还需要抬头看到底队伍排到你了没有。如果把打电话和观察排队的位置看成是程序的两个操作的话,这个程序需要在这两种不同的行为之间来回的切换,效率可想而知是低下的;而异步非阻塞形式却没有这样的问题,因为打电话是你(等待者)的事情,而通知你则是柜台(消息触发机制)的事情,程序没有在两种不同的操作中来回切换。

同步/异步与阻塞/非阻塞

  1. 同步阻塞形式

  效率最低。拿上面的例子来说,就是你专心排队,什么别的事都不做。

  1. 异步阻塞形式

  如果在银行等待办理业务的人采用的是异步的方式去等待消息被触发(通知),也就是领了一张小纸条,假如在这段时间里他不能离开银行做其它的事情,那么很显然,这个人被阻塞在了这个等待的操作上面;

  异步操作是可以被阻塞住的,只不过它不是在处理消息时阻塞,而是在等待消息通知时被阻塞。

  1. 同步非阻塞形式

  实际上是效率低下的。

  想象一下你一边打着电话一边还需要抬头看到底队伍排到你了没有,如果把打电话和观察排队的位置看成是程序的两个操作的话,这个程序需要在这两种不同的行为之间来回的切换,效率可想而知是低下的。

  1. 异步非阻塞形式

  效率更高,

  因为打电话是你(等待者)的事情,而通知你则是柜台(消息触发机制)的事情,程序没有在两种不同的操作中来回切换

  比如说,这个人突然发觉自己烟瘾犯了,需要出去抽根烟,于是他告诉大堂经理说,排到我这个号码的时候麻烦到外面通知我一下,那么他就没有被阻塞在这个等待的操作上面,自然这个就是异步+非阻塞的方式了。

很多人会把同步和阻塞混淆,是因为很多时候同步操作会以阻塞的形式表现出来,同样的,很多人也会把异步和非阻塞混淆,因为异步操作一般都不会在真正的IO操作处被阻塞

总结:异步非阻塞√ 同步阻塞×

刚刚我们已经了解了,运行中的程序就是一个进程。所有的进程都是通过它的父进程来创建的。因此,运行起来的python程序也是一个进程,那么我们也可以在程序中再创建进程。多个进程可以实现并发效果,也就是说,当我们的程序中存在多个进程的时候,在某些时候,就会让程序的执行速度变快。

multiprocessing模块

process模块是一个创建进程的模块,借助这个模块,就可以完成进程的创建。

mulit 多元
processing 进程

multiprocessing 多进程

注意:一旦进程创建起来,它的很多东西就不受我们控制了,是操作系统控制的。

Process([group [, target [, name [, args [, kwargs]]]]]),由该类实例化得到的对象,表示一个子进程中的任务(尚未启动)

强调:
1. 需要使用关键字的方式来指定参数
2. args指定的为传给target函数的位置参数,是一个元组形式,必须有逗号

参数介绍:
1 group参数未使用,值始终为None
2 target表示调用对象,即子进程要执行的任务
3 args表示调用对象的位置参数元组,args=(1,2,'小满',)
4 kwargs表示调用对象的字典,kwargs={'name':'小满','age':3}
5 name为子进程的名称

方法介绍

p.start():启动进程,并调用该子进程中的p.run() 
p.run():进程启动时运行的方法,正是它去调用target指定的函数,我们自定义类的类中一定要实现该方法  
p.terminate():强制终止进程p,不会进行任何清理操作,如果p创建了子进程,该子进程就成了僵尸进程,使用该方法需要特别小心这种情况。如果p还保存了一个锁那么也将不会被释放,进而导致死锁
p.is_alive():如果p仍然运行,返回True
p.join([timeout]):主线程等待p终止(强调:是主线程处于等的状态,而p是处于运行的状态)。timeout是可选的超时时间,需要强调的是,p.join只能join住start开启的进程,而不能join住run开启的进程 

属性介绍

p.daemon:默认值为False,如果设为True,代表p为后台运行的守护进程,当p的父进程终止时,p也随之终止,并且设定为True后,p不能创建自己的新进程,必须在p.start()之前设置
p.name:进程的名称
p.pid:进程的pid
p.exitcode:进程在运行时为None、如果为–N,表示被信号N结束(了解即可)
p.authkey:进程的身份验证键,默认是由os.urandom()随机生成的32字符的字符串。这个键的用途是为涉及网络连接的底层进程间通信提供安全性,这类连接只有在具有相同的身份验证键时才能成功(了解即可)

小案例

import os
import time
from multiprocessing import Process


def func(*args):
    print("*" * 10)
    print(54321)
    print("子进程", os.getpid())
    print("子进程的父进程", os.getppid())
    print("*" * 10)
    time.sleep(3)
    print(12345)


if __name__ == '__main__':  # multiprocessing Windows系统中执行才需要写main入口

    p = Process(target=func)  # 把函数 注册到 进程里面
    # P是一个进程对象,还没有启动进程
    p.start()  # 开启了一个子进程 p.start() 执行了之后才是启动了进程
    print("*" * 10)
    print("父进程", os.getpid())  # os.getpid() 查看当前进程的进程号 PID
    print("父进程的父进程", os.getppid())  # ppid --> parent process id  父进程 谁创建了,谁就是我的父进程
    # 可以理解为 if __name__ == '__main__' 下面的代码都是主进程
    # 主进程的代码 和 func里面的代码是异步执行的,也就是两条马路上的两辆车,没有先后顺序

# 进程的生命周期
# 主进程
# 子进程 --> 等待子进程中的代码运行完毕就结束 比如func 运行完毕就结束了
# 开启了子进程的主进程:
# 主进程自己的代码如果长,等待自己的代码执行结束
# 子进程的执行时间长,主进程会在主进程之后完毕代码执行完毕之后等待子进程执行完毕之后 主进程才结束
# 就是主进程的代码已经全部执行完毕了,但是子进程的代码还没有执行完,主进程等你。
**********
父进程 16084
父进程的父进程 11556
**********
54321
子进程 6284
子进程的父进程 16084
**********
12345

可以通过tasklist | findstr PID号码 去查看一个进程的信息

我的这个python程序是通过pycharm去运行的,所以它的父进程一定是pycharm

image-20240117161448840

join方法

没有使用join之前

import time
from multiprocessing import Process


# join()

def func(arg1: int, arg2: int):
    print("*" * arg1)
    time.sleep(5)
    print("*" * arg2)


if __name__ == '__main__':
    p = Process(target=func, args=(10, 20))
    p.start()
    print("=========> 运行完了")
=========> 运行完了
**********
********************

思考

如何让全部函数func执行完了之后,立马打印=========> 运行完了

part1

import time
from multiprocessing import Process


# join()

def func(arg1: int, arg2: int):
    print("*" * arg1)
    time.sleep(5)
    print("*" * arg2)


if __name__ == '__main__':
    p = Process(target=func, args=(10, 20))
    p.start()
    p.join()  # join()的作用就是感知一个子进程的结束,将异步的程序改为同步
    print("=========> 运行完了")

**********
********************
=========> 运行完了

part2

import time
from multiprocessing import Process


# join()

def func(arg1: int, arg2: int):
    print("*" * arg1)
    time.sleep(5)
    print("*" * arg2)


if __name__ == '__main__':
    p = Process(target=func, args=(10, 20))
    p.start()
    print("一来二去 看你三四五六眼 想你七八九十遍")
    # start()和join()之间的代码与子进程(例如本例中的func)是异步执行的,
    if hero := '小满' != "大乔":
        print('{:-^30}'.format('小满'))
    p.join()  # join()的作用就是感知一个子进程的结束,将异步的程序改为同步
    print("=========> 运行完了")

一来二去 看你三四五六眼 想你七八九十遍
--------------小满--------------
**********
********************
=========> 运行完了

如何开启多个子进程方法1

part1

import time
from multiprocessing import Process


def func(arg1: int, arg2: int):
    print("*" * arg1)
    time.sleep(5)
    print("*" * arg2)


if __name__ == '__main__':
    p = Process(target=func, args=(10, 20))
    p.start()

    p1 = Process(target=func, args=(10, 20))
    p1.start()

    p2 = Process(target=func, args=(10, 20))
    p2.start()

    p3 = Process(target=func, args=(10, 20))
    p3.start()

**********
**********
**********
**********
********************
************************************************************

结果错乱后续可以优化

part2

import time
from multiprocessing import Process


def func(arg1: int, arg2: int):
    print("*" * arg1)
    time.sleep(5)
    print("*" * arg2)


if __name__ == '__main__':
    t1 = time.time()
    for index in range(10):
        # 开启了十个进程 就相当于十条马路 time.sleep(5) 就相当于遇到了红绿灯 大家一起等
        # 大白话异步就是在一个很宽的马路上 有几个进程 就有几条路 大家自己跑自己的
        p = Process(target=func, args=(10 * index, 20 * index))
        p.start()
    p.join()    # 此时的p是最后一个进程的p 最后一个进程执行完了,无法保证前面的进程执行完了
                # 不是按顺序执行的,是按顺序给操作系统发指令,让操作系统启动这些进程,告诉操作系统这件事是顺序来的
                # 但是操作系统不一定按顺序启动  
    print(time.time() - t1)

# 结果如下 注意打印时间的位置
... 省略一部分结果
************************************************************************************************************************
6.277811765670776
****************************************************************************************************

part3

import os
from pathlib import Path
import time
from multiprocessing import Process


def func(filename: str, arg2: int):
    with open(f"{Path.cwd()}{os.sep}{filename}", 'w', encoding='utf-8') as file:
        file.write(arg2 * '*')


if __name__ == '__main__':
    t1 = time.time()
    p_list = []
    for index in range(30):

        p = Process(target=func, args=(f'info {index}.txt', 20 * index))
        p.start()  # 这里的start的意思就是,直接欢快的去启动执行了,不等你后面的进程了
        p_list.append(p)

    [p.join() for p in p_list]  # 之前的所有进程必须在这里执行完才能执行下面的代码
                                # 在某一个节点,我希望子进程结束,我才去写join
    data = [f.name for f in Path.cwd().glob('*.txt')]
    print(data)  
    print(time.time() - t1)  

# 往文件夹里面写入30个文件
# 多进程写入文件
# 首先往文件夹中写入文件
# 然后想用户展示写入文件之后文件夹中所有的文件名
['info 0.txt', 'info 1.txt', 'info 10.txt', 'info 11.txt', 'info 12.txt', 'info 13.txt', 'info 14.txt', 'info 15.txt', 'info 16.txt', 'info 17.txt', 'info 18.txt', 'info 19.txt', 'info 2.txt', 'info 20.txt', 'info 21.txt', 'info 22.txt', 'info 23.txt', 'info 24.txt', 'info 25.txt', 'info 26.txt', 'info 27.txt', 'info 28.txt', 'info 29.txt', 'info 3.txt', 'info 4.txt', 'info 5.txt', 'info 6.txt', 'info 7.txt', 'info 8.txt', 'info 9.txt']
3.957754135131836

如何开启多个子进程方法2

part1

通过继承Process类

import os
from multiprocessing import Process


class MyProcess(Process):
    def run(self) -> None:
        # run里面的代码 就相当于之前func里面的代码
        print("self.pid", self.pid)
        print('self.name', self.name)
        print('子进程PID', os.getpid())
        print('子进程的父进程PID', os.getppid())


if __name__ == '__main__':
    print("主进程PID", os.getpid())

    p1 = MyProcess()
    p1.start()  # 这里调用start()方法,实际上就是调用了类里面的run()方法

    p2 = MyProcess()
    p2.start()

# 1 自定义类 继承Process类
# 2 必须实现一个run方法,run方法中是在子进程中执行代码
主进程PID 16340
self.pid 23832
self.name MyProcess-1
子进程PID 23832
self.pid 8476
self.name MyProcess-2
子进程PID 8476
子进程的父进程PID 16340
子进程的父进程PID 16340

知识点补充:

name属性 就是进程名
pid属性  就是进程号 和os.getpid()一样的

part2

如何给方法传参

定义一个自己的类
继承Process类
如果要传参,重写__init__方法
把参数绑定给self
不要忘记实现父类的__init__方法
重新run方法

用的时候实例化,然后实例化start()即可
import fileinput
import os
import json
import glob
import time
import base64
from dataclasses import dataclass
from pathlib import Path
from multiprocessing import Process, Lock
from typing import Dict
import requests
from fake_useragent import UserAgent


class MyProcess(Process):

    def __init__(self, url, lock, file_dir):
        super().__init__()
        self.url = url
        self.lock = lock
        self.file_dir = file_dir
        self.headers = {'User_Agent': UserAgent().random}

    def run(self) -> None:
        try:
            self.get_content()
            print(f"{self.name.ljust(15)} | 已完成")
        except requests.exceptions.RequestException:
            pass

    def get_content(self):
        self.lock.acquire()
        response = requests.get(self.url, self.headers)
        data: Dict = json.loads(response.text)
        from_who = data.get('from')
        content = data.get('hitokoto')
        self.download(filename=from_who, content=content)
        self.lock.release()

    def download(self, filename, content):
        with open(f'{self.file_dir}{os.sep}{filename}.txt', 'w', encoding='utf-8') as file:
            file.write(content)


@dataclass
class Timer:
    f: callable

    def __call__(self, *args, **kwargs):
        t1 = time.time()
        res = self.f(*args, **kwargs)
        total_time = time.time() - t1
        print(f"已完成,耗时{total_time:.2f}秒")

        return res


def encode_b64():
    secret_url = b'aHR0cHM6Ly92MS5oaXRva290by5jbi8='
    return base64.b64decode(secret_url.decode())


@Timer
def main():
    pathfile = (Path.cwd().parent / '结果').resolve()
    pathfile.mkdir(parents=True, exist_ok=True)

    lock = Lock()
    base_url = encode_b64()
    p_list = []

    for index in range(10):
        p = MyProcess(base_url, lock, pathfile)
        p.start()
        p_list.append(p)

    [p.join() for p in p_list]

    for item in fileinput.input(files=glob.glob(f'{pathfile}{os.sep}*.*'), encoding='utf-8'):
        print(item)


if __name__ == '__main__':
    main()

MyProcess-6     | 已完成
MyProcess-5     | 已完成
MyProcess-3     | 已完成
MyProcess-10    | 已完成
MyProcess-2     | 已完成
MyProcess-1     | 已完成
MyProcess-8     | 已完成
MyProcess-7     | 已完成
MyProcess-9     | 已完成
MyProcess-4     | 已完成
苍天璧络天一络,雅月鸿灵月正灵。
想一个人有多想念,那又是文字失效瞬间。
从今以后,你走你的路,我演我的戏。
井蛙不可以语于海者,拘于虚也;夏虫不可以语于冰者,笃于时也
孤独不是一种脾性,而是一种无奈。
自信人生二百年,会当水击三千里。
金钱和女人,是人生犯错的根源。
一件事物与其自身完全一致
一曲凄音悲鸣,乱了心怀,悲了秋意,最后只剩下无边的萧索。
幸福破灭之时,总是伴随着血腥味。
已完成,耗时4.86秒

当我把进程改成线程的时候,核心逻辑不变,神奇的事情便发生了。

Thread-1        | 已完成
Thread-2        | 已完成
Thread-3        | 已完成
Thread-4        | 已完成
Thread-5        | 已完成
Thread-6        | 已完成
Thread-7        | 已完成
Thread-8        | 已完成
Thread-9        | 已完成
Thread-10       | 已完成
人のエゴで造られた 虚にだけはなりたくはなくて。
须知政权是由枪杆子中取得的。
生命的最终奥义,不过就是活得自在罢了。
欲买桂花同载酒,荒泷天下第一斗。
没人生来杰出。
听闻远方有你,动身跋涉千里;我吹过你吹过的风,这算不算相拥。
实变函数学十遍,泛函学完心泛寒。
借问江潮与海水,何似君情与妾心?
能不能停下来,看看那个满眼泪花奔向你的我。
已完成,耗时1.69秒

当我把线程的Lock 换成信号量,并给它一个值为5的时候,神奇的事情又发生了。

Thread-2        | 已完成
Thread-1        | 已完成
Thread-5        | 已完成
Thread-3        | 已完成
Thread-7        | 已完成
Thread-8        | 已完成
Thread-6        | 已完成
Thread-4        | 已完成
Thread-9        | 已完成
Thread-10       | 已完成
两个人从监狱的窗户往外看,一个看见了土地,一个看见了星星。
形而上者谓之道,形而下者谓之器。
野蛮人之间人吃人,文明人之间人骗人。
生活百般滋味,人生需要笑对。
玲珑骰子安红豆,入骨相思知不知。
知识有两种,一种是你知道的,一种是你知道在哪里能找到的!
苦痛离去没有复苏的平常。
齐人三鼓之后,彼竭我盈之时。
在战略上要藐视敌人,在战术上要重视敌人!
问灵十三载,等一不归人。
已完成,耗时0.34秒

结论:

对于计算密集型任务,使用multiprocessing模块可以实现并行计算,而对于IO密集型任务,使用threading模块可以实现并发执行。

多进程之间数据隔离问题

# 进程 与 进程之间 数据相关性
# 子进程与其它子进程是彼此隔离开的
# 父进程与子进程的数据也是隔离开的

import os
from multiprocessing import Process


def func():
    global n  # 声明了一个全局变量

    n = 0  # 重新定义了一个n
    print(f"子进程pid | {os.getpid()} | {n}")


if __name__ == '__main__':
    n = 100
    p = Process(target=func)
    p.start()
    p.join()
    print(f"主进程pid | {os.getpid()} | {n}")
    
# 如果主进程和子进程之间不是彼此隔离的,那么最后打印的这个n应该是局部的n才对
子进程pid | 18092 | 0
主进程pid | 17556 | 100

使用多进程实现socket服务端的并发效果

个人觉得没有socketserver好用(ε=ε=ε=┏(゜ロ゜;)┛逃)

# config.py

IP = '127.0.0.1'
PORT = 7888
# 服务端

import socket
from multiprocessing import Process
from config import IP, PORT


def my_server(conn, addr):
    conn.send(f"客户端{addr}你好!".encode())
    msg = conn.recv(1024).decode()
    print(f'来自客户端{addr}的消息 | {msg}')
    conn.close()  


def main():
    server = socket.socket()
    server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    server.bind((IP, PORT))
    server.listen()

    try:
        while True:
            conn, addr = server.accept()
            p = Process(target=my_server, args=(conn, addr))
            p.start()
    except (ConnectionResetError, OSError, KeyboardInterrupt) as e:
        print(e)
    finally:
        server.close()


if __name__ == '__main__':
    main()
# 客户端

import socket
from config import IP, PORT

client = socket.socket()
client.connect((IP, PORT))

try:
    msg = client.recv(1024).decode()
    print(msg)

    msg2 = input('>>>').encode()
    client.send(msg2)

except (ConnectionResetError, OSError, KeyboardInterrupt) as e:
    print(e)
finally:
    client.close()

image-20240117220000360

守护进程

part1

# 守护进程
# 子进程 转换成--> 守护进程
import time
from multiprocessing import Process


def func():
    while True:
        time.sleep(0.5)
        print("我还活着...")


if __name__ == '__main__':
    Process(target=func).start()
    i = 0
    while i < 3:  # 假装这个while循环 就是socketserver
        print("我是socketserver")
        time.sleep(1)

        i += 1
    else:
        print("主进程已经结束了。。。")
我是socketserver
我还活着...
我是socketserver
我还活着...
我还活着...
我是socketserver
我还活着...
我还活着...
主进程已经结束了。。。
我还活着...
我还活着...
我还活着...
我还活着...
我还活着...
我还活着...
我还活着...
...无限循环

part2

# 守护进程
# 子进程 转换成--> 守护进程
import time
from multiprocessing import Process


def func():
    while True:
        time.sleep(0.5)
        print("我还活着...")


if __name__ == '__main__':
    p = Process(target=func)
    p.daemon = True  # 设置子进程为守护进程 一定要写在start之前
    p.start()

    i = 0
    while i < 3:  # 假装这个while True 就是socketserver
        print("我是socketserver")
        time.sleep(1)

        i += 1
    else:
        print("主进程已经结束了。。。")

# 守护进程会随着主进程的代码执行而结束
# 主进程结束子进程也跟着结束了
"""
我是socketserver
我是socketserver
我还活着...
我还活着...
我是socketserver
我还活着...
我还活着...
主进程已经结束了。。。
"""

part3

# 守护进程
# 子进程 转换成--> 守护进程
import time
from multiprocessing import Process


def func():
    while True:
        time.sleep(0.5)
        print("我还活着...")


def func2():
    print("in func2 start")
    time.sleep(8)
    print('in func2 finished')


if __name__ == '__main__':
    p = Process(target=func)
    p.daemon = True  # 设置子进程为守护进程 一定要写在start之前
    p.start()

    Process(target=func2).start()

    i = 0
    while i < 3:  # 假装这个while True 就是socketserver
        print("我是socketserver")
        time.sleep(1)

        i += 1
    else:
        print("主进程已经结束了。。。")

# 守护进程会随着主进程的[代码]执行而结束
# 主进程结不结束取决于两点,第一是子进程是否结束,第二是自己的代码是否执行完毕
# 主进程的代码执行完了,守护进程就结束了,不管你子进程有没有执行完
"""
我是socketserver
in func2 start
我是socketserver
我还活着...
我还活着...
我是socketserver
我还活着...
我还活着...
主进程已经结束了。。。
in func2 finished
"""

is_alive() 判断进程对象是否存活

import time
from multiprocessing import Process


def func():
    print("in func start")
    print('in func finished')


if __name__ == '__main__':
    p = Process(target=func)
    p.start()
    p.terminate()  # 在主进程内结束一个子进程 p.terminate()  是先向操作系统发起一个请求,我要结束进程,然后等待操作系统响应
                   # 但是结束并不是马上结束的,所以刚使用terminate结束掉一个
                   # 进程,然后去is_alive()肯定是打印True,可以让它睡一会儿
                   # time.sleep() 比如1秒钟,然后去打印is_alive 就结束了
    print(f"子进程{p.name}还活着吗 {p.is_alive()}")  # is_alivev 检验进程是否还活着,返回布尔值
    time.sleep(1)
    print(f"子进程{p.name}还活着吗 {p.is_alive()}")  # name 就是进程的名称

"""
子进程Process-1还活着吗 True
子进程Process-1还活着吗 False
"""

注意:在多进程中,子进程的代码不能input()

在多进程编程中,子进程无法直接使用终端输入函数,例如input()。这是因为每个进程在操作系统中都有自己独立的输入和输出流。

僵尸进程

僵尸进程是指完成了自己的任务,但父进程没有正确地释放它所占用的系统资源

导致它仍然存在于进程列表中

但已经停止了运行

这些僵尸进程会占据一定的系统内存,并在一定程度上影响系统的性能。

孤儿进程

孤儿进程则是指父进程在子进程终止之前就已经退出了

导致子进程失去了与父进程通信的能力。

这些孤儿进程将被init进程接管

init进程会等待它的状态信息并释放它的系统资源。

from multiprocessing import Process


def func():
    num = input(">>>")
    print(num)
    # 子进程里面不能input()


if __name__ == '__main__':
    Process(target=func).start()

进程同步控制

锁(最重要也最简单)

买火车票,肯定是一个多线程的,大家同一时间有很多人在买,假如现在有8个人都来买票,但是余票只有一张,正常使用多线程的情况下,结果会错误。

# ticket.json的文件内容
{"ticket": 1}
# 锁
# 火车票
import json
import time
from multiprocessing import Process


def show():
    with open('ticket.json') as f:
        data = json.load(f)
    print(f"余票:{data['ticket']}")


def buy_ticket(name):
    with open('ticket.json') as f:
        data = json.load(f)
        time.sleep(0.1)  # 模拟网络延迟
    if data['ticket'] > 0:
        data['ticket'] -= 1
        print("\033[32m{0}买到票了\033[0m".format(name))
    else:
        print("\033[31m{0}没买到票\033[0m".format(name))

    time.sleep(0.1)  # 模拟网络延迟
    with open('ticket.json', 'w', encoding='utf-8') as f:
        json.dump(data, f)


if __name__ == '__main__':
    for index in range(8):
        p = Process(target=show)
        p.start()

    for index in range(8):
        p = Process(target=buy_ticket, args=(index,))
        p.start()

"""
余票:1
余票:1
余票:1
余票:1
1买到票了
3买到票了
7买到票了
0买到票了
6买到票了
余票:1
余票:1
余票:1
余票:0
5没买到票
2没买到票
4没买到票
"""

给某一段代码加一个锁,这一段代码在同一个时刻内,只能被一个进程执行。

锁一般只是多进程/线程才使用

# 锁
# 火车票
import json
import time
from multiprocessing import Process, Lock


def show():
    with open('ticket.json') as f:
        data = json.load(f)
    print(f"余票:{data['ticket']}")


def buy_ticket(name, lock):
    lock.acquire()  # 拿钥匙进门
    with open('ticket.json') as f:
        data = json.load(f)
        time.sleep(0.1)  # 模拟网络延迟
    if data['ticket'] > 0:
        data['ticket'] -= 1
        print("\033[32m{0}买到票了\033[0m".format(name))
    else:
        print("\033[31m{0}没买到票\033[0m".format(name))

    time.sleep(0.1)  # 模拟网络延迟
    with open('ticket.json', 'w') as f:
        json.dump(data, f)
    lock.release()  # 还钥匙


if __name__ == '__main__':
    for index in range(8):
        p = Process(target=show)
        p.start()
    lock = Lock()
    # 加锁会影响速度,但是只要是异步的,就会产生数据安全问题,为了数据安全,只能牺牲效率,
    # 如果只有一张票,卖超了,就会出现问题。
    for index in range(8):
        p = Process(target=buy_ticket, args=(index, lock))
        p.start()
       
"""
余票:1
余票:1
余票:1
余票:1
余票:1
余票:1
1买到票了
余票:1
余票:0
2没买到票
3没买到票
4没买到票
5没买到票
0没买到票
6没买到票
7没买到票
"""

用类的方式再写一次

import json
import time
from multiprocessing import Process, Lock


class TicketManager(Process):
    def __init__(self):
        super().__init__()

    @staticmethod
    def show():
        with open("ticket") as f:
            data = json.load(f)
            print(f"余票 {data['ticket']}")
            time.sleep(0.1)

    @staticmethod
    def buy_ticket(name, lock):

        lock.acquire()  # 拿钥匙开门

        with open('ticket') as f:
            data = json.load(f)
            time.sleep(0.1)

        if data['ticket'] > 0:
            data['ticket'] -= 1
            print(f"\033[32m{name}买到票了\033[0m")
        else:
            print(f"\033[31m{name}没买到票\033[0m")

        time.sleep(0.1)
        with open("ticket", 'w') as f:
            json.dump(data, f)

        lock.release()  # 还钥匙


if __name__ == '__main__':

    lock = Lock()

    for _ in range(8):
        p = Process(target=TicketManager.show)    # 使用了multiprocessing模块中的Process类来创建进程,并通过target参数指定要执行的函数。
        p.start()                                 # 在这种情况下,target参数接受的是函数对象,而不是方法对象,也就是可以不用再函数累不写run方法。

    for index in range(8):
        p = Process(target=TicketManager.buy_ticket, args=(index, lock))
        p.start()

简单总结

多进程代码
from multiprocessing import Process

方法 
	进程对象.start()  开启一个子进程
	进程对象.join()   感知一个子进程的结束
	进程对象.terminate()  结束一个子进程
	进程对象.is_alive()  查看某个子进程是否还在运行
	
属性
	进程对象.name  进程名
	进程对象.pid  进程号
	进程对象.daemon 值为True表示新的子进程是一个守护进程
			- 守护进程 随着主进程代码的结束而结束
			- 一定在start之前设置
lock = Lock() 
lock.acquire()  # 拿钥匙
会操作数据不安全的操作 就写在这部分
lock.release()  # 还钥匙

信号量

part1

假如有20个人要去KTV唱歌,但是这个KTV比较小,每一个房间只能一次性容纳1个人,目前KTV只有4个限制的房间,那怎么办呢?

import time
import random
from multiprocessing import Process


def ktv(index):

    print(f"\033[31m{index}走进ktv\033[0m")
    time.sleep(random.randint(1, 5))  # 模拟唱歌时间
    print(f"\033[32m{index}走出ktv\033[0m")


if __name__ == '__main__':
    for i in range(20):  # 多少个人
        p = Process(target=ktv, args=(i,))
        p.start()
        
# 执行后的效果 20个人全部进去了,这肯定是不符合预期的

解决方案,可以使用信号量Semaphore 【翻译为旗语】

Semaphore内部的计数器可以通过acquire()和release()方法来操作。当调用acquire()时,计数器减少1。如果计数器为0,则调用线程或任务将被阻塞,直到有其他线程或任务调用release()释放资源。当调用release()时,计数器增加1,并且阻塞的线程或任务中的一个将被唤醒。

import random
from multiprocessing import Process
from multiprocessing import Semaphore


def ktv(index, sem):
    sem.acquire()  # 拿到钥匙进入KTV
    print(f"\033[31m{index}走进ktv\033[0m")
    time.sleep(random.randint(1, 5))  # 模拟唱歌时间
    print(f"\033[32m{index}走出ktv\033[0m")
    sem.release()  # 归还钥匙 走出KTV


if __name__ == '__main__':
    sem = Semaphore(4)  # 相当于有几把钥匙,只有拿到钥匙的人才能进去KTV,等唱完了出来归还钥匙,后面的人才可以进去唱歌。
    for i in range(20):  # 多少个人
        p = Process(target=ktv, args=(i, sem))
        p.start()

part2

from multiprocessing import Semaphore

sem = Semaphore(4)
sem.acquire()
print("拿到一把钥匙")
sem.acquire()
print("拿到一把钥匙")
sem.acquire()
print("拿到一把钥匙")
sem.acquire()
print("拿到一把钥匙")
sem.acquire()
print("拿到一把钥匙")

"""
拿到一把钥匙
拿到一把钥匙
拿到一把钥匙
拿到一把钥匙
卡死。。
"""
# 然后因为一共只有四把钥匙,如果没人归还,程序就会一直卡死在这里。。。

事件

冷知识:两个进程之间通信是根据Socket,基于文件的

part1

from multiprocessing import Event

e = Event()  # 创建了一个事件
tag = True if e.is_set() is False else False  # e.is_set()查看一件事件的状态,默认被设置成阻塞即False 它的阻塞依赖于e.wait()
print(f"e被阻塞了吗?", tag)

e.set()  # 将这个事件的状态改为True
tag = True if e.is_set() is False else False
print(f"e被阻塞了吗?", tag)

e.wait()  # 是依据e.is_set()的值来决定是否阻塞的。
          # 如果e.is_set() 是False 就阻塞,是True 就不阻塞了
          # 等待信号的事件被变成True,可以理解为如果为True 忽略这一行wait()就行
          # 如果不执行wait,那么设置事件Event就没有任何意义了
          # 没有遇到wait的时候,wait之前的的代码都是可以正常运行的,即没有遇到第一个wait之前,后面的看状态
		  # 做的就是一件事:等待某一件事中的一个信号变为True
print(12453)

e.clear()  # 将这个事件的状态改为False
tag = True if e.is_set() is False else False
print(f"e被阻塞了吗?", tag)

e.wait()
print("小满会被打印出来吗???")  # 因为阻塞了 所以这句话不会被打印出来
# set 和 clear
# 分别用来修改一个事件的状态 True或者False

# is_set 用来查看一个事件的状态
# wait 是依据事件的状态来决定自己是否被阻塞
#      False阻塞  True不阻塞

# e.is_set() 为False 表示阻塞
# e.is_set() 为False 表示不阻塞
# 别被搞混了这里

"""
e被阻塞了吗? True  
e被阻塞了吗? False  
12453
e被阻塞了吗? True  
"""

# 此时的程序并没有停止,卡住了,因为是阻塞状态

part2

如果进程全部走完了,另外一个进程如果还在执行,记得关闭掉它。

# 通过红绿灯事件 控制全部进程
import random
import time
from colorama import Fore, Style
from multiprocessing import Event, Process


def cars(e, item):
    if not e.is_set():
        print(f"{Fore.BLUE}第{item}辆车正在等红灯{Style.RESET_ALL}")
        e.wait()  # 阻塞 直到得到一个事件状态变成True的信号
    print(f"{Fore.YELLOW}第{item}辆车正已通过红绿灯{Style.RESET_ALL}")


def light(e):
    while True:
        if e.is_set():  # 如过是True
            e.clear()  # 设置为False 等红灯
            print(Fore.RED + "红灯亮了" + Style.RESET_ALL)
        else:
            e.set()  # 事件后面跟一个操作,就是马上执行,所以最终结果看到的就是控制全部进程的状态
            print(Fore.GREEN + "绿灯亮了" + Style.RESET_ALL)
        time.sleep(2)


if __name__ == '__main__':
    e = Event()  # 如果路上没有车, 那么红绿灯就白写了 车通行是根据红绿灯来的
    traffic = Process(target=light, args=(e,))
    traffic.start()

    car_list = []
    for item in range(10):
        car = Process(target=cars, kwargs={'item': item, "e": e})
        car.start()
        car_list.append(car)
        time.sleep(random.random())  # 随机睡几秒

    [car.join() for car in car_list]
    traffic.terminate()
    print("全部车辆已通过, 程序已结束.")

"""
绿灯亮了
第0辆车正已通过红绿灯
第1辆车正已通过红绿灯
第2辆车正已通过红绿灯
第3辆车正已通过红绿灯
红灯亮了
第4辆车正在等红灯
第5辆车正在等红灯
第6辆车正在等红灯
第7辆车正在等红灯
绿灯亮了
第4辆车正已通过红绿灯
第5辆车正已通过红绿灯
第6辆车正已通过红绿灯
第7辆车正已通过红绿灯
第8辆车正已通过红绿灯
第9辆车正已通过红绿灯
全部车辆已通过, 程序已结束.
"""

进程之间的通信

IPC(Inter-Process Communication)

队列概念

创建共享的进程队列,Queue是多进程安全的队列,可以使用Queue实现多进程之间的数据传递。

Queue([maxsize]) 
创建共享的进程队列。
参数 :maxsize是队列中允许的最大项数。如果省略此参数,则无大小限制。
底层队列使用管道和锁定实现。

方法介绍

Queue([maxsize]) 
创建共享的进程队列。maxsize是队列中允许的最大项数。如果省略此参数,则无大小限制。底层队列使用管道和锁定实现。另外,还需要运行支持线程以便队列中的数据传输到底层管道中。 
Queue的实例q具有以下方法:

q.get( [ block [ ,timeout ] ] ) 
返回q中的一个项目。如果q为空,此方法将阻塞,直到队列中有项目可用为止。block用于控制阻塞行为,默认为True. 如果设置为False,将引发Queue.Empty异常(定义在Queue模块中)。timeout是可选超时时间,用在阻塞模式中。如果在制定的时间间隔内没有项目变为可用,将引发Queue.Empty异常。

q.get_nowait( ) 
同q.get(False)方法。

q.put(item [, block [,timeout ] ] ) 
将item放入队列。如果队列已满,此方法将阻塞至有空间可用为止。block控制阻塞行为,默认为True。如果设置为False,将引发Queue.Empty异常(定义在Queue库模块中)。timeout指定在阻塞模式中等待可用空间的时间长短。超时后将引发Queue.Full异常。

q.qsize() 
返回队列中目前项目的正确数量。此函数的结果并不可靠,因为在返回结果和在稍后程序中使用结果之间,队列中可能添加或删除了项目。在某些系统上,此方法可能引发NotImplementedError异常。


q.empty() 
如果调用此方法时 q为空,返回True。如果其他进程或线程正在往队列中添加项目,结果是不可靠的。也就是说,在返回和使用结果之间,队列中可能已经加入新的项目。

q.full() 
如果q已满,返回为True. 由于线程的存在,结果也可能是不可靠的(参考q.empty()方法)。。

其它方法(了解)

q.close() 
关闭队列,防止队列中加入更多数据。调用此方法时,后台线程将继续写入那些已入队列但尚未写入的数据,但将在此方法完成时马上关闭。如果q被垃圾收集,将自动调用此方法。关闭队列不会在队列使用者中生成任何类型的数据结束信号或异常。例如,如果某个使用者正被阻塞在get()操作上,关闭生产者中的队列不会导致get()方法返回错误。

q.cancel_join_thread() 
不会再进程退出时自动连接后台线程。这可以防止join_thread()方法阻塞。

q.join_thread() 
连接队列的后台线程。此方法用于在调用q.close()方法后,等待所有队列项被消耗。默认情况下,此方法由不是q的原始创建者的所有进程调用。调用q.cancel_join_thread()方法可以禁止这种行为。

part1

# 队列 先进先出
# IPC
# import queue 进程不能用这个,进程通信是隔着内存
import time
from multiprocessing import Queue

q = Queue(5)  # 可以传参数,也可以不传,如果传了参数,就是指定了队列的大小,比如此处为5
q.put(1)
q.put(2)
q.put(3)
q.put(4)
q.put(4)  # 如果队列已经满了,程序就会停在这里,等待数据被别人取走,再将数据放入队列。
          # 如果队列中的数据一直不被取走,程序就会永远停在这里。

try:
    q.put_nowait(3)  # 可以使用put_nowait,如果队列满了不会阻塞,但是会因为队列满了而报错。
except:  # 因此我们可以用一个try语句来处理这个错误。这样程序不会一直阻塞下去,但是会丢掉这个消息。
    print('队列已经满了')

print(q.full())  # 因此,我们再放入数据之前,可以先看一下队列的状态,如果已经满了,就不继续put了。
                 # 队列是否满了 返回布尔值

print(q.get())  # 同put方法一样,如果队列已经空了,那么继续取就会出现阻塞。
print(q.get())
print(q.get())
print(q.get())
print(q.get())
print(q.empty())  # 队列是否空了 返回布尔值

# print(q.get())  # 空了 一直阻塞,除非其它进程再防值进去
while True:
    try:
        q.get_nowait()  # 可以使用put_nowait,如果队列满了不会阻塞,但是会因为队列满了而报错。
                        # 因此我们可以用一个try语句来处理这个错误。这样程序不会一直阻塞下去,但是会丢掉这个消息。
    except:
        print("队列空了")
        time.sleep(1)
        break
        # 每一次队列为空的情况下记得睡1秒或者几秒,因为while True的执行非常耗内存,一秒钟能执行几千次
        # 晚1一秒钟得到结果是可以接收的

# full() 和 empty() 在多进程的时候有一些不准

"""
True
1
2
3
4
4
True
队列空了
"""

part2

# 在进程内怎么通过q去通信

import time
from multiprocessing import Queue, Process


def produce(q):
    q.put([time.asctime(), 'Hi', '我是小满'])  # put可以简单的理解为生产数据的地方,生产者


def consume(q):
    data = q.get()
    print("拿到了一个结果", data)  # get可以简单理解为消费数据的地方,消费者


if __name__ == '__main__':
    q = Queue()  # 如果没有设置,表示没有上限,放多少个数据都行,这样就不会在put的时候遇到阻塞的现象了

    p = Process(target=produce, args=(q, ))
    p.start()

    c = Process(target=consume, args=(q, ))
    c.start()

"""
拿到了一个结果 ['Fri Jan 19 19:58:19 2024', 'Hi', '我是小满'] 
"""

生产者消费者模型

在并发编程中使用生产者和消费者模式能够解决绝大多数并发问题。该模式通过平衡生产线程和消费线程的工作能力来提高程序的整体处理数据的速度。

为什么要使用生产者和消费者模式

在线程世界里,生产者就是生产数据的线程,消费者就是消费数据的线程。在多线程开发当中,如果生产者处理速度很快,而消费者处理速度很慢,那么生产者就必须等待消费者处理完,才能继续生产数据。同样的道理,如果消费者的处理能力大于生产者,那么消费者就必须等待生产者。为了解决这个问题于是引入了生产者和消费者模式。

什么是生产者消费者模式

生产者消费者模式是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。

part1

# 买包子
# 生产者消费者模型
# 生产者 进程
# 消费者 进程

import time
import random
from multiprocessing import Process, Queue


def consumer(q, name):
    while True:
        # if q.empty():
        #     break      去判断是否为空是不合理的  有没有可能你此刻的数据已经为空,但是生产者又往里面放了新数据

        # try:
        #     q.get_nowait()  这样捕获异常也是不合理的 有没有可能生产者生产数据比较慢
                            # 数据已经全部取完了,生产者后续的数据就放不进去了,即漏处理部分数据

        if q.get() is None:
            print(f"\033[21m{name}  获取到了一个空\033[0m ")
            break
        food = q.get()
        print(f"\033[21m{name}\033[0m 吃了 {food}")
        time.sleep(random.randint(1, 3))


def produce(name, food, q):  # 谁生产 生产了什么 生产后放哪里
    for index in range(1, 5):  # 每一个生产者都可以包N个包子
        time.sleep(random.randint(1, 3))  # 控制生产包子的时间
        rel_food = f"\033[21m{name}生产的第{index}个{food}\033[0m"
        print(rel_food)
        q.put(rel_food)


if __name__ == '__main__':
    q = Queue(4)

    p = Process(target=produce, kwargs={'name': '小满', 'food': '包子', 'q': q})
    p.start()

    p1 = Process(target=produce, kwargs={'name': '大乔', 'food': '芝麻饼', 'q': q})
    p1.start()

    c1 = Process(target=consumer, args=(q, '小乔'))
    c1.start()

    c2 = Process(target=consumer, args=(q, '阿珂'))
    c2.start()

    p.join()
    p1.join()

    q.put(None)   # 此处如果只加一个None 结果会因为生产者只有一个而两个消费者 引发程序阻塞
                  # 一个队列是进程安全的,就算一个队列里面的数据只能被一个进程取走
    q.put(None)  # 注意:结束信号None,不一定要由生产者发,主进程里同样可以发,但主进程需要等生产者结束后才应该发送该信号

    # 加None结束有什么问题?如果我在加几个消费者是不是又需要添put几个None进去,就会很low,part2会解决

"""
小满生产的第1个包子
小满生产的第2个包子
大乔生产的第1个芝麻饼
小乔 吃了 大乔生产的第1个芝麻饼
大乔生产的第2个芝麻饼
阿珂 吃了 大乔生产的第2个芝麻饼
小满生产的第3个包子
小满生产的第4个包子
大乔生产的第3个芝麻饼
小乔 吃了 大乔生产的第3个芝麻饼
大乔生产的第4个芝麻饼
阿珂 吃了 大乔生产的第4个芝麻饼
小乔  获取到了一个空 
阿珂  获取到了一个空 
"""

part2

# 买包子
import random
import time
from multiprocessing import Process, JoinableQueue


def consumer(q, name):
    while True:
        food = q.get()
        print(f"\033[21m{name}\033[0m 吃了 {food}")
        time.sleep(random.randint(1, 3))
        q.task_done()  # q.task_done() 加入到消费者
                       # 每一次获取到数据之后,需要提交一个回值给这个队列  这个方法就算q.task_done()
                       # 计数器 - 1  count - 1
                       # 每一次task_done 都会被记录下来


def produce(name, food, q, num):  # 谁生产 生产了什么 生产后放哪里
    for index in range(1, num+1):  # 每一个生产者都可以包十个包子
        time.sleep(random.randint(1, 3))  # 控制生产包子的时间
        rel_food = f"\033[21m{name}生产的第{index}个{food}\033[0m"
        print(rel_food)
        q.put(rel_food)  # 计数器 + 1  1 ...总次数
    q.join()  # q.join() 加入到生产者
              # 感知一个队列中的数据  全部处理完毕 即q.task_doen 全部执行完毕,才会触发这个join
              # 阻塞 直到一个队列种所有的数据 全部处理完毕
            	
              # 一个q结束 全部q都结束了
              # 一言以蔽之:我生产了10个包子,不能走,要等客人把包子都吃完,我才结束
    print("\033[43m大家都吃完了,我被执行了。\033[0m")

if __name__ == '__main__':
    num = 4
    q = JoinableQueue(num)

    p = Process(target=produce, kwargs={'name': '小满', 'food': '包子', 'q': q, 'num': num})
    p1 = Process(target=produce, kwargs={'name': '大乔', 'food': '芝麻饼', 'q': q, 'num': num})
    c1 = Process(target=consumer, args=(q, '小乔'))
    c2 = Process(target=consumer, args=(q, '阿珂'))

    p.start()
    p1.start()
    c1.daemon = True  # 守护进程需要给消费者设置
    c2.daemon = True  # 设置为守护进程 主进程中的代码执行完毕之后 子进程自动结束 所以消费者的while循环也自动结束了
    c1.start()
    c2.start()

    p.join()
    p1.join()  # 感知一个进程的结束
               # 生产者什么时候结束,因为生产者内部加了join阻塞,所以需要等

# 做了三件事
	# c1 ,c2 消费者设置守护进程
    # p1, p2 设置join
    # 每一个q get完之后加入一个q.task_done()
        
# 在消费者这一端:
    # 每次获取一个数据
    # 处理一个数据
    # 发送一个记号:标志一个数据被处理成功

# 在生产者这一端
    # 每一次生产一个数据
    # 且每一次生产生产的数据都被放在队列中
    # 在队列中课刻上一个记号
    # 当生产者生产完毕之后
    # join信号:已经停止生产数据了
                # 且要等之前被刻上的记号都被消费完
                # 当数据都被处理完时,join结束

        
# 步骤分析:
# consumer中把所有的任务消耗完
# producer端的join感知到,停止阻塞
# 所有的producer进程结束
# 主进程中的p.join结束
# 主进程中代码结束
# 守护进程(消费者的进程)结束

"""
小满生产的第1个包子
小乔 吃了 小满生产的第1个包子
大乔生产的第1个芝麻饼
阿珂 吃了 大乔生产的第1个芝麻饼
大乔生产的第2个芝麻饼
小乔 吃了 大乔生产的第2个芝麻饼
小满生产的第2个包子
阿珂 吃了 小满生产的第2个包子
小满生产的第3个包子
阿珂 吃了 小满生产的第3个包子
大乔生产的第3个芝麻饼
小乔 吃了 大乔生产的第3个芝麻饼
小满生产的第4个包子
大乔生产的第4个芝麻饼
阿珂 吃了 小满生产的第4个包子
小乔 吃了 大乔生产的第4个芝麻饼
大家都吃完了,我被执行了。
大家都吃完了,我被执行了。
"""

# 执行和结束的准确顺序是(chatGPT):
# 主进程启动两个生产者进程和两个消费者进程。
# 生产者进程按照随机的时间间隔生产食物,并将食物放入队列中。
# 消费者进程从队列中获取食物并进行消费,消费完成后标记任务的完成。
# 主进程等待生产者进程执行完毕。
# 当所有生产者进程执行完毕后,主进程继续执行,并等待消费者进程终止。

简单总结JoinableQueue

JoinableQueue的实例p除了与Queue对象相同的方法之外,还具有以下方法:

q.task_done()
使用者使用此方法发出信号,表示q.get()返回的项目已经被处理。如果调用此方法的次数大于从队列中删除的项目数量,将引发ValueError异常。

q.join()
生产者将使用此方法进行阻塞,直到队列中所有项目均被处理。阻塞将持续到为队列中的每个项目均调用q.task_done()方法为止。
下面的例子说明如何建立永远运行的进程,使用和处理队列上的项目。生产者将项目放入队列,并等待它们被处理。

将队列设置为JoinableQueue然后实例化,比如q = JoinableQueue(10)

主进程中,给消费者设置为守护进程(记得设置在start()之前)

记得在最后给生产者加上join

在生产者这边的,生产完毕(即put完毕全部数据之后)写上q.join()

在消费者这边,每一次q.get()到数据后都加入一个q.task_done()

回顾总结

信号量 Semaphore
from multiprocessing import Semaphore
用锁的原理实现的, 内置了一个计数器
在同一时间内,只能由指定数量的进程执行某一段被控制住的代码

事件
wait阻塞收到事件状态控制的同步组件
状态 True False is_set()
	- 状态为True 不阻塞 --> 变成 False 通过 clear()
	- 状态为False 阻塞  --> 变成 True  通过 set()

队列
Queue
	- put  当队列满的时候阻塞等待队列有空位置
	- get  当队列空的时候阻塞等待队列有数据
	- full empty 不完全准确
	
JoinableQueue
	- get task_done()  这两个一般结合使用 (同一个函数内)
	- put join()  这两个一般结合使用(同一个函数内)

管道

part1

from multiprocessing import Pipe, Process

conn1, conn2 = Pipe()
conn1.send('124523')  # 和socket区别 不一定是bytes类型
print(conn2.recv())   # 收的时候不需要指定大小,不会有粘包现象

# 特点 双向通信 即便两个不同的进程里面也可以通信

"""
124523
"""

part2

from multiprocessing import Pipe, Process


def func(conn):
    while True:
        print(conn.recv())


if __name__ == '__main__':
    conn1, conn2 = Pipe()
    Process(target=func, args=(conn1,)).start()

    for index in range(20):
        conn2.send("吃了吗")

"""
吃了吗
吃了吗
吃了吗
吃了吗
... 
循环完20此吃了吗之后,程序就一直阻塞
"""

part3

能解决问题,但是不推荐

from multiprocessing import Pipe, Process


def func(conn):
    while True:
        msg = conn.recv()
        if msg is None:
            break
        print(msg)


if __name__ == '__main__':
    conn1, conn2 = Pipe()
    Process(target=func, args=(conn1,)).start()

    for index in range(20):
        conn2.send("吃了吗")
    else:
        conn2.send(None)

"""
打印20次 吃了吗 然后程序正常结束
"""

part4

管道有一个EOFError错误,就算当管道取到头,没有东西取了的时候,就会报错

但是触发这个错误有一个很苛刻的条件,没有数据可取的时候,还继续recv()就会触发这个异常,只要一旦触发了这个错误,就可以通过捕获异常的方式取解决了

另:以后写管道的时候,如果要两端通信,Pipe的返回值(conn1, conn2)都要传递过去,哪一个不用关闭哪一个。

from multiprocessing import Pipe, Process


def func(conn1, conn2):
    conn2.close() # 用不到conn2 关闭
    while True:
        msg = conn1.recv()
        print(msg)


if __name__ == '__main__':
    conn1, conn2 = Pipe()
    Process(target=func, args=(conn1, conn2)).start()
    conn1.close()  # 用不到conn1 关闭
    for index in range(20):
        conn2.send("吃了吗")
    else:
        conn2.close()  # 传完之后 conn2 也要关闭

"""
打印20次 吃了吗  
然后触发  EOFError 错误
"""

part5

解决版

from multiprocessing import Pipe, Process


def func(conn1, conn2):
    conn2.close() # 用不到conn2 关闭
    while True:
        try:
            msg = conn1.recv()
            print(msg)
        except EOFError:
            conn1.close()  # 不要忘记关
            break


if __name__ == '__main__':
    conn1, conn2 = Pipe()
    Process(target=func, args=(conn1, conn2)).start()
    conn1.close() # 用不到conn1 关闭  注意,这个一定要写在start的后面,不会会报错
    for index in range(20):
        conn2.send("吃了吗")
    else:
        conn2.close() # 传完之后 conn2 也要关闭

疑问

1.为什么主进程关闭conn1并不会影响子进程的recv
	- 官网的解释:值得注意的是,传给子进程或多个进程之间的conn是不会互相应影响,也就算主进程关闭后,在子进程的conn并不会受到影响,任然可以接收数据。
2.为什么全部conn口(所有链接)都关闭之后,才会触发EOFError
	- 为了确保正确地读取和写入数据,主进程和子进程的连接(conn)都需要在使用完之后关闭。如果只关闭了一个进程的连接而没有关闭另一个进程的连接,那么在读取或写入数据时会导致错误。
	- 当你试图从已关闭的连接中读取数据时,Python会引发EOFError异常。这是因为EOFError表示已到达文件末尾或连接已关闭,无法继续读取数据。
	- 因此,为了避免EOFError异常,你需要在主进程和子进程中分别关闭连接(conn)。这样,当一个进程尝试读取数据时,如果连接已关闭,它会得到一个明确的信号,而不会引发异常。

应该特别注意管道端点的正确管理问题。如果是生产者或者消费者中都没有使用管道的某个端点,就应该将它关闭。这也说明了为何在生产中关闭了管道的输出端,在消费者中关闭管道的输入端。如果忘记执行这些步骤,程序可能在消费者中的recv()操作上挂起即阻塞。管道是由操作系统进行引用计数的,必须在所有的进程中关闭管道后才能生成EOFError异常。

因此,在生产者中关闭管道不会有任何效果,除非消费者也关闭了相同的管道端点。

使用管道写消费者生产者模型

import time
import random
from dataclasses import dataclass
from multiprocessing import Process, Pipe, Lock


def producer(con, pro, name, index):
    con.close()
    food_list = ['包子', '煎饼', '油条', '鸡蛋']
    for index in range(1, index + 1):
        time.sleep(random.random())
        msg = f"\033[21m{name}生产的第{index}个{random.choice(food_list)}.\033[0m"
        pro.send(msg)
        print(msg)
    else:
        pro.close()  # 生产完了,记得关闭管道


def consumer(con, pro, name, lock):
    pro.close()
    while True:
        try:
            lock.acquire()
            food = con.recv()  # 加锁 防止数据混乱

            msg = f"\033[31m{name}\033[0m 得到了 {food}"
            print(msg)
            time.sleep(random.random())
        except EOFError:
            con.close()
            break
        finally:
            lock.release()

@dataclass
class Timer:
    f: callable

    def __call__(self, *args, **kwargs):
        t1 = time.time()
        res = self.f(*args, **kwargs)
        print(f"程序已结束,耗时{time.time() - t1:.2f}秒.")
        return res


@Timer
def main():
    con, pro = Pipe()
    index = 3
    lock = Lock()

    p = Process(target=producer, kwargs={"con": con, 'pro': pro, 'name': '小满', 'index': index})
    p1 = Process(target=producer, kwargs={"con": con, 'pro': pro, 'name': '大乔', 'index': index})
    c1 = Process(target=consumer, kwargs={"con": con, 'pro': pro, 'name': '阿珂', 'lock': lock})
    c2 = Process(target=consumer, kwargs={"con": con, 'pro': pro, 'name': '小乔', 'lock': lock})
    c3 = Process(target=consumer, kwargs={"con": con, 'pro': pro, 'name': '貂蝉', 'lock': lock})

    conn_list = [p, p1, c1, c2, c3]
    for conn in conn_list:
        conn.start()
    else:
        con.close()
        pro.close()
    [conn.join() for conn in conn_list]


if __name__ == '__main__':
    main()

# pipe 数据不安全性 
# IPC
#      (一个生产者 一个消费者不会涉及安全性问题),多个会有不安全性,比如多个消费者都想要管道里面的同一个数据
#      加锁来控制操作管道的行为 来避免进程之间争抢数据造成的数据不安全现象
#      怎么加锁 应该加在消费者取数据的时候,即  con.recv()前后,不过关闭锁应该加到异常里面,如本代码所示
#      为什么:
#      当锁被获取时,它会阻塞其他进程获取该锁,直到释放为止。因为lock.release()语句只有在con.recv()成功的情况下才会执行。
#      如果锁加在conn.recv()的上一行和下一行,如果在接收数据时出现错误或异常,似乎锁可能永远不会被释放,导致程序卡住。
#

# 队列 进程之间的数据是安全的
# 因为队列的实现原理就算管道 + 锁  (管道是相对底层的东西)

# 建议: 以后应该尽量使用队列,而不是使用管道,不过管道应该可能会作为面试题


"""
小满生产的第1个鸡蛋.
小乔 得到了 小满生产的第1个鸡蛋.
大乔生产的第1个鸡蛋.
阿珂 得到了 大乔生产的第1个鸡蛋.
大乔生产的第2个包子.
貂蝉 得到了 大乔生产的第2个包子.
大乔生产的第3个鸡蛋.
小乔 得到了 大乔生产的第3个鸡蛋.
小满生产的第2个油条.
阿珂 得到了 小满生产的第2个油条.
小满生产的第3个包子.
貂蝉 得到了 小满生产的第3个包子.
程序已结束,耗时3.70秒.
"""

总结:别用管道 _

进程之间的数据共享

展望未来,基于消息传递的并发编程是大势所趋
即便是使用线程,推荐做法也是将程序设计为大量独立的线程集合,通过消息队列交换数据。
这样极大地减少了对使用锁定和其他同步手段的需求,还可以扩展到分布式系统中。
但进程间应该尽量避免通信,即便需要通信,也应该选择进程安全的工具来避免加锁带来的问题。
以后我们会尝试使用数据库来解决现在进程之间的数据共享问题。

Manager模块介绍

进程间数据是独立的,可以借助于队列或管道实现通信,二者都是基于消息传递的
虽然进程间数据独立,但可以通过Manager实现数据共享,事实上Manager的功能远不止于此

A manager object returned by Manager() controls a server process which holds Python objects and allows other processes to manipulate them using proxies.
A manager returned by Manager() will support types list, dict, Namespace, Lock, RLock, Semaphore, BoundedSemaphore, Condition, Event, Barrier, Queue, Value and Array.

part1

from multiprocessing import Manager, Process


def main(dic):
    dic['count'] -= 1
    print("子进程", dic)


if __name__ == '__main__':
    manager = Manager()
    dic = manager.dict({'count': 100})  # 此时的字典 就是数据共享的字典

    p_list = []
    p = Process(target=main, args=(dic,))
    p.start()
    p.join()

    print("主进程的dict", dic)

    
"""
子进程 {'count': 99}
主进程的dict {'count': 99}
"""

part2

如果使用Manager 记得加锁

from multiprocessing import Manager, Process, Lock


def main(dic, lock):
    # 不加锁而操作共享的数据,肯定会出现数据错乱
    lock.acquire()  # 获取锁
    dic['count'] -= 1  # 修改共享字典
    lock.release()  # 释放锁  


if __name__ == '__main__':
    lock = Lock()  # 创建锁对象
    manager = Manager()
    dic = manager.dict({'count': 100})  # 创建共享的字典

    p_list = []
    for index in range(10):
        p = Process(target=main, args=(dic, lock))  # 创建进程,传入共享字典和锁
        p.start()
        p_list.append(p)
    [i.join() for i in p_list]

    print("主进程的dict", dic)

"""
主进程的dict {'count': 90}
"""

简单总结:

只会用到 Lock Semaphore Event Queue JoinableQueue 
不会用到 Pipe Manange 

进程池

进程池就是用少的进程,解决更多的事

为什么要有进程池?进程池的概念。

在程序实际处理问题过程中,忙时会有成千上万的任务需要被执行,闲时可能只有零星任务。那么在成千上万个任务需要被执行的时候,我们就需要去创建成千上万个进程么?首先,创建进程需要消耗时间,销毁进程也需要消耗时间。第二即便开启了成千上万的进程,操作系统也不能让他们同时执行,这样反而会影响程序的效率。因此我们不能无限制的根据任务开启或者结束进程。那么我们要怎么做呢?

在这里,要给大家介绍一个进程池的概念,定义一个池子,在里面放上固定数量的进程,有需求来了,就拿一个池中的进程来处理任务,等到处理完毕,进程并不关闭,而是将进程再放回进程池中继续等待任务。如果有很多任务需要执行,池中的进程数量不够,任务就要等待之前的进程执行任务完毕归来,拿到空闲进程才能继续执行。也就是说,池中进程的数量是固定的,那么同一时间最多有固定数量的进程在运行。这样不会增加操作系统的调度难度,还节省了开闭进程的时间,也一定程度上能够实现并发效果。

Pool([numprocess  [,initializer [, initargs]]]):创建进程池

numprocess:要创建的进程数,如果省略,将默认使用cpu_count()的值
initializer:是每个工作进程启动时要执行的可调用对象,默认为None
initargs:是要传给initializer的参数组

主要方法

p.apply(func [, args [, kwargs]]):在一个池工作进程中执行func(*args,**kwargs),然后返回结果。
'''需要强调的是:此操作并不会在所有池工作进程中并执行func函数。如果要通过不同参数并发地执行func函数,必须从不同线程调用p.apply()函数或者使用p.apply_async()'''

p.apply_async(func [, args [, kwargs]]):在一个池工作进程中执行func(*args,**kwargs),然后返回结果。
'''此方法的结果是AsyncResult类的实例,callback是可调用对象,接收输入参数。当func的结果变为可用时,将理解传递给callback。callback禁止执行任何阻塞操作,否则将接收其他异步操作中的结果。'''
也可以通过关键字去传参 kwds={}
   
p.close():关闭进程池,防止进一步操作。如果所有操作持续挂起,它们将在工作进程终止前完成

P.jion():等待所有工作进程退出。此方法只能在close()或teminate()之后调用

# chatGPT
apply(func, args=(), kwds={}):同步执行一个函数,阻塞直到返回结果。
map(func, iterable, chunksize=None):同步地将函数应用于可迭代对象的每个元素,返回结果列表。
apply_async(func, args=(), kwds={}, callback=None):异步执行一个函数,立即返回一个AsyncResult对象,可以通过它获取结果。
map_async(func, iterable, chunksize=None, callback=None):异步地将函数应用于可迭代对象的每个元素,返回一个AsyncResult对象,可以通过它获取结果。
close():关闭进程池,不再接受新的任务。
join():阻塞直到所有任务完成。
terminate():立即终止所有工作进程。

其它方法(了解)

方法apply_async()和map_async()的返回值是AsyncResul的实例obj。实例具有以下方法
obj.get():返回结果,如果有必要则等待结果到达。timeout是可选的。如果在指定时间内还没有到达,将引发一场。如果远程操作中引发了异常,它将在调用此方法时再次被引发。
obj.ready():如果调用完成,返回True
obj.successful():如果调用完成且没有引发异常,返回True,如果在结果就绪之前调用此方法,引发异常
obj.wait([timeout]):等待结果变为可用。
obj.terminate():立即终止所有工作进程,同时不执行任何清理或结束任何挂起工作。如果p被垃圾回收,将自动调用此函数

part1

# 为什么会有进程池的概念
# 效率(能起很多个进程比如(100) 执行效率非常慢)
    # 每开启进程,开启属于这个进程的内存空间(很耗时)
    # 寄存器 堆栈 文件  (开启一个进程就要创建这些)
# 进程过多 操作系统的调度

# 进程池
# python中的 先创建一个进程的池子
# 这个池子指定能存放多少个进程
# 先将这些进程创建好

# 更高级的进程池(python中没有)
# n, m 下限上限进程池 比如3-20 一般情况下进程池最多20个
# 3  三个进程
#    数据量增大 +进程
# 20 20个进程

# 进程怎么开
# 一般是cpu的数目+1 比如四核八线程 就开5个进程
# 进程池怎么开
# 超过5个进程,就用进程池去启

import time
from multiprocessing import Pool, Process, cpu_count


def func(n):
    for index in range(10):
        print(n + 1)


if __name__ == '__main__':
    t = time.time()
    pool = Pool(cpu_count() // 2 + 1)  # 我的电脑是4c8t 此处是5个进程
    pool.map(func, range(30))  # 100个任务  map内部是自带join的  map的第二个参数必须给一个可迭代类型
    t1 = time.time() - t  # 进程池的时间

    start = time.time()
    p_list = []
    for index in range(30):
        p = Process(target=func, args=(index,))
        p.start()
        p_list.append(p)
    [p.join() for p in p_list]
    t2 = time.time() - start  # 正常多进程的时间

    print('进程池执行时间', t1)
    print('多进程行时间', t2)

"""
进程池执行时间 0.602675199508667
多进程行时间 4.1624181270599365
"""

part2 同步提交数据apply

p.apply 了解就可以了,同步就没有必要这样写了

import time
import os
from dataclasses import dataclass
from multiprocessing import Pool


@dataclass
class Timer:
    f: callable

    def __call__(self, *args, **kwargs):
        t1 = time.time()
        res = self.f(*args, **kwargs)
        print(f"使用同步的方式p.apply耗时{time.time() - t1:.2f}秒")
        return res

def func(n):
    print(f"\033[32mstart func {n} | {os.getpid()}\033[0m")
    time.sleep(1)
    print(f"\033[31mend func {n} | {os.getpid()}\033[0m", )

@Timer
def main():
    p = Pool(5)
    for index in range(10):
        p.apply(func, args=(index,))  # apply 就是同步提交的意思

if __name__ == '__main__':
    main()

    
"""
start func 0 | 20496
end func 0 | 20496
start func 1 | 9520
end func 1 | 9520
start func 2 | 18168
end func 2 | 18168
start func 3 | 3376
end func 3 | 3376
start func 4 | 14356
end func 4 | 14356
start func 5 | 20496
end func 5 | 20496
start func 6 | 9520
end func 6 | 9520
start func 7 | 18168
end func 7 | 18168
start func 8 | 3376
end func 8 | 3376
start func 9 | 14356
end func 9 | 14356
使用同步的方式p.apply耗时10.71秒
"""

part3 异步提交apply_async

这种也可以随性所欲的传任何参数

import time
import os
from dataclasses import dataclass
from multiprocessing import Pool


@dataclass
class Timer:
    f: callable

    def __call__(self, *args, **kwargs):
        t1 = time.time()
        res = self.f(*args, **kwargs)
        print(f"使用同步的方式p.apply_async耗时{time.time() - t1:.2f}秒")
        return res


def func(n):
    print(f"\033[32mstart func {n} | {os.getpid()}\033[0m")
    time.sleep(1)
    print(f"\033[31mend func {n} | {os.getpid()}\033[0m", )


@Timer
def main():
    p = Pool(5)
    for index in range(10):
        # p.apply(func, args=(index,))  # apply 就是同步提交的意思
        p.apply_async(func, args=(index, ))  # apply_async 就算异步提交的意思
                                             # 此处的异步是真异步,之前子进程没有执行完,主进程会等子进程
                                             # 现在就是主进程不等子进程了
    p.close()  # 结束进程池接收任务
    # 所有要执行的任务都提交完毕了,且进程中的任务也全部结束了的时候,才去感知这个任务结束的行为
    p.join()  # 感知进程池中的的任务结束  需要注意的是,进程池永远是活着的,里面的任务交替执行而已


if __name__ == '__main__':
    main()



"""
start func 0 | 24268
start func 1 | 5544
start func 2 | 24164
start func 3 | 8048
start func 4 | 21764
end func 0 | 24268
start func 5 | 24268
end func 1 | 5544
end func 3 | 8048end func 2 | 24164

start func 6 | 24164
start func 7 | 5544
start func 8 | 8048
end func 4 | 21764
start func 9 | 21764
end func 5 | 24268
end func 8 | 8048
end func 7 | 5544end func 6 | 24164

end func 9 | 21764
使用同步的方式p.apply耗时2.80秒
"""

使用进程池实现socket服务端的并发效果

# config.py
IP = '127.0.0.1'
PORT = 8050
# 服务端
from socket import *
from multiprocessing import Pool, cpu_count
from config import *

server = socket()
server.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
server.bind((IP, PORT))
server.listen()


def func(conn, addr):
    conn.send(f'你好! 来自 {addr}的朋友'.encode())
    
    ret = conn.recv(1024).decode()
    print(f'收到来自{addr}的消息 | {ret}')

    conn.close()


if __name__ == '__main__':
    p = Pool(cpu_count() // 2 + 1)

    while True:
        conn, addr = server.accept()
        p.apply_async(func, args=(conn, addr))

    p.close()
# 客户端
from socket import *
from config import *

client = socket()
client.connect((IP, PORT))

ret = client.recv(1024).decode()
print(ret)

msg = input(">>>").strip().encode()
client.send(msg)

client.close()

简单总结

p = Pool()
p.map(funcname, iterable)  默认异步的执行任务,自带close和join
p.apply  同步调用
p.apply_async 异步调用 和主进程完全异步 需要手动close 和 join

进程池的返回值

part1

from multiprocessing import Pool


def func(i):
    return i * i


if __name__ == '__main__':
    p = Pool(5)
    for index in range(10):
        res = p.apply(func, args=(index,)) # apply的结果就是func的返回值
        print(res)

"""
0
1
4
9
16
25
36
49
64
81
"""

part2


from multiprocessing import Pool


def func(i):
    return i * i


if __name__ == '__main__':
    p = Pool(5)
    for index in range(10):
        res = p.apply_async(func, args=(index,))
        print(res)

"""
<multiprocessing.pool.ApplyResult object at 0x000001FCF9D1AB30>
<multiprocessing.pool.ApplyResult object at 0x000001FCF9D1AB60>
<multiprocessing.pool.ApplyResult object at 0x000001FCF9D1AD10>
<multiprocessing.pool.ApplyResult object at 0x000001FCF9D1AE00>
<multiprocessing.pool.ApplyResult object at 0x000001FCF9D1AEF0>
<multiprocessing.pool.ApplyResult object at 0x000001FCF9D1B010>
<multiprocessing.pool.ApplyResult object at 0x000001FCF9D1B130>
<multiprocessing.pool.ApplyResult object at 0x000001FCF9D1B250>
<multiprocessing.pool.ApplyResult object at 0x000001FCF9D1B370>
<multiprocessing.pool.ApplyResult object at 0x000001FCF9D1B4C0>
"""

part3

import time
from multiprocessing import Pool


def func(i):
    time.sleep(0.5)
    return i * i


if __name__ == '__main__':
    p = Pool(5)
    for index in range(10):
        res = p.apply_async(func, args=(index,))
        print(res.get())  # 阻塞 等待func的计算结果 因为要一直等待执行结果,所以这里还是很慢 part4优化

"""
0
1
4
9
16
25
36
49
64
81
"""

part4

没有阻塞,速度很快

import time
from multiprocessing import Pool


def func(i):
    time.sleep(0.5)
    return i * i


if __name__ == '__main__':
    p = Pool(5)
    p_list = []
    for index in range(10):
        res = p.apply_async(func, args=(index,))
        p_list.append(res)

    for line in p_list:
        print(line.get())
       
"""
0
1
4
9
16
25
36
49
64
81
"""

part5

使用p.map

import time
from multiprocessing import Pool


def func(i):
    time.sleep(0.5)
    return i * i


if __name__ == '__main__':
    p = Pool(5)
    res = p.map(func, range(10))
    print(res)  # [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]  map的返回值是一个列表,是要等全部结果都完成后在返回

回调函数

回调函数是在主进程中执行的

回调函数一般是在爬虫的时候去用到

爬虫中耗时最长的是网络延迟

什么时候使用回调函数

执行callback的函数体代码的逻辑,远远小于传入参数到callback函数的网络延迟时间,再使用callback

需要回调函数的场景:进程池中任何一个任务一旦处理完了,就立即告知主进程:我好了额,你可以处理我的结果了。主进程则调用一个函数去处理该结果,该函数即回调函数

我们可以把耗时间(阻塞)的任务放到进程池中,然后指定回调函数(主进程负责执行),这样主进程在执行回调函数时就省去了I/O的过程,直接拿到的是任务的结果。

# 回调函数

import os
from multiprocessing import Pool


def func1(n):
    print("func1的pid", os.getpid())
    print("in func1")
    return n * n


def func2(m):
    print('func2的pid', os.getpid())
    print("in func2")  # 把func1的返回值当做回调函数的参数,然后去执行回调函数
    print(m)  # 回调函数不能传其它参数,唯一的参数只能是func1的返回值


if __name__ == '__main__':
    print('主进程的pid', os.getpid())
    p = Pool(5)
    p.apply_async(func1, args=(10,), callback=func2)  # 回调函数是在主进程执行的
    p.close()
    p.join()

"""
主进程的pid 11992
func1的pid 11708
in func1
func2的pid 11992
in func2
100
"""

简单总结:

apply
    同步的:只有当func执行完只会,才会继续执行其它代码
    ret = apply(func, args())
    返回值就是func的return

apply_async
	异步的:当func被注册进入一个进程之后,程序就继续执行下去
	apply_async(func, args=())
	返回值: apply_async返回对象的obj
		- 为了用户能从中获取func的返回值obj.get()
	get会阻塞直到对应的func执行完毕拿到结果
	需要先close()然后join()来保持多进程和主进程代码的同步性

线程概念的引入

之前我们已经了解了操作系统中进程的概念,程序并不能单独运行,只有将程序装载到内存中,系统为它分配资源才能运行,而这种执行的程序就称之为进程。程序和进程的区别就在于:程序是指令的集合,它是进程运行的静态描述文本;进程是程序的一次执行活动,属于动态概念。在多道编程中,我们允许多个程序同时加载到内存中,在操作系统的调度下,可以实现并发地执行。这是这样的设计,大大提高了CPU的利用率。进程的出现让每个用户感觉到自己独享CPU,因此,进程就是为了在CPU上实现多道编程而提出的。

有了进程为什么要有线程

  进程有很多优点,它提供了多道编程,让我们感觉我们每个人都拥有自己的CPU和其他资源,可以提高计算机的利用率。很多人就不理解了,既然进程这么优秀,为什么还要线程呢?其实,仔细观察就会发现进程还是有很多缺陷的,主要体现在两点上:

  • 进程只能在一个时间干一件事,如果想同时干两件事或多件事,进程就无能为力了。
  • 进程在执行的过程中如果阻塞,例如等待输入,整个进程就会挂起,即使进程中有些工作不依赖于输入的数据,也将无法执行。

  如果这两个缺点理解比较困难的话,举个现实的例子也许你就清楚了:如果把我们上课的过程看成一个进程的话,那么我们要做的是耳朵听老师讲课,手上还要记笔记,脑子还要思考问题,这样才能高效的完成听课的任务。而如果只提供进程这个机制的话,上面这三件事将不能同时执行,同一时间只能做一件事,听的时候就不能记笔记,也不能用脑子思考,这是其一;如果老师在黑板上写演算过程,我们开始记笔记,而老师突然有一步推不下去了,阻塞住了,他在那边思考着,而我们呢,也不能干其他事,即使你想趁此时思考一下刚才没听懂的一个问题都不行,这是其二。

  现在你应该明白了进程的缺陷了,而解决的办法很简单,我们完全可以让听、写、思三个独立的过程,并行起来,这样很明显可以提高听课的效率。而实际的操作系统中,也同样引入了这种类似的机制——线程。

线程的出现

  60年代,在OS中能拥有资源和独立运行的基本单位是进程,然而随着计算机技术的发展,进程出现了很多弊端,一是由于进程是资源拥有者,创建、撤消与切换存在较大的时空开销,因此需要引入轻型进程;二是由于对称多处理机(SMP)出现,可以满足多个运行单位,而多个进程并行开销过大。

  因此在80年代,出现了能独立运行的基本单位——线程(Threads)

  注意:进程是资源分配的最小单位,线程是CPU调度的最小单位.

     每一个进程中至少有一个线程。 

进程和线程的关系

image-20240122151840622

线程与进程的区别可以归纳为以下4点:

  1)地址空间和其它资源(如打开文件):进程间相互独立,同一进程的各线程间共享。某进程内的线程在其它进程不可见。

  2)通信:进程间通信IPC,线程间可以直接读写进程数据段(如全局变量)来进行通信——需要进程同步和互斥手段的辅助,以保证数据的一致性。

  3)调度和切换:线程上下文切换比进程上下文切换要快得多。

  4)在多线程操作系统中,进程不是一个可执行的实体。

  *通过漫画了解线程进程

线程的特点

在多线程的操作系统中,通常是在一个进程中包括多个线程,每个线程都是作为利用CPU的基本单位,是花费最小开销的实体。线程具有以下属性。

  1)轻型实体

  线程中的实体基本上不拥有系统资源,只是有一点必不可少的、能保证独立运行的资源。

  线程的实体包括程序、数据和TCB。线程是动态概念,它的动态特性由线程控制块TCB(Thread Control Block)描述。

TCB包括以下信息:
(1)线程状态。
(2)当线程不运行时,被保存的现场资源。
(3)一组执行堆栈。
(4)存放每个线程的局部变量主存区。
(5)访问同一个进程中的主存和其它资源。
用于指示被执行指令序列的程序计数器、保留局部变量、少数状态参数和返回地址等的一组寄存器和堆栈。

  2)独立调度和分派的基本单位。

  在多线程OS中,线程是能独立运行的基本单位,因而也是独立调度和分派的基本单位。由于线程很“轻”,故线程的切换非常迅速且开销小(在同一进程中的)。

  3)共享进程资源。

  线程在同一进程中的各个线程,都可以共享该进程所拥有的资源,这首先表现在:所有线程都具有相同的进程id,这意味着,线程可以访问该进程的每一个内存资源;此外,还可以访问进程所拥有的已打开文件、定时器、信号量机构等。由于同一个进程内的线程共享内存和文件,所以线程之间互相通信不必调用内核。

  4)可并发执行。

  在一个进程中的多个线程之间,可以并发执行,甚至允许在一个进程中所有线程都能并发执行;同样,不同进程中的线程也能并发执行,充分利用和发挥了处理机与外围设备并行工作的能力。

线程的使用场景

image-20240122152209227

启一个字处理软件进程,该进程肯定需要办不止一件事情,比如监听键盘输入,处理文字,定时自动将文字保存到硬盘,这三个任务操作的都是同一块数据,因而不能用多进程。只能在一个进程里并发地开启三个线程,如果是单线程,那就只能是,键盘输入时,不能处理文字和自动保存,自动保存时又不能输入和处理文字。

内存中的线程

image-20240122152405723

多个线程共享同一个进程的地址空间中的资源,是对一台计算机上多个进程的模拟,有时也称线程为轻量级的进程。

  而对一台计算机上多个进程,则共享物理内存、磁盘、打印机等其他物理资源。多线程的运行也多进程的运行类似,是cpu在多个线程之间的快速切换。

  不同的进程之间是充满敌意的,彼此是抢占、竞争cpu的关系,如果迅雷会和QQ抢资源。而同一个进程是由一个程序员的程序创建,所以同一进程内的线程是合作关系,一个线程可以访问另外一个线程的内存地址,大家都是共享的,一个线程干死了另外一个线程的内存,那纯属程序员脑子有问题。

  类似于进程,每个线程也有自己的堆栈,不同于进程,线程库无法利用时钟中断强制线程让出CPU,可以调用thread_yield运行线程自动放弃cpu,让另外一个线程运行。

  线程通常是有益的,但是带来了不小程序设计难度,线程的问题是:

  1. 父进程有多个线程,那么开启的子线程是否需要同样多的线程

  2. 在同一个进程中,如果一个线程关闭了文件,而另外一个线程正准备往该文件内写内容呢?

  因此,在多线程的代码中,需要更多的心思来设计程序的逻辑、保护程序的数据。

# 进程 是 最小的 内存分配单位
# 线程 是 操作it调度的最小单位
# 线程被CPU执行了
# 进程内至少包含一个线程
# 进程中可以开启多个线程
    # 开启一个线程所需要的时间远远小于一个进程
    # 多个线程内部有自己的数据栈,数据不共享
    # 全局变量在多个进程之间是共享的
# 在Cpython解释器下的python程序性 在同一时刻 多个线程中只能有一个线程被CPU执行
# 高CPU(用多进程):计算类  --- 高CPU利用率  
# 高IO(用多线程) :爬取网页
		# qq聊天   send recv
    	# 处理日志文件
        # 处理web请求
        # 读数据库 写数据库
      
# 线程之间的资源是共享的
# 线程的开启和关闭以及切换的时间开销远远小于进程
# python与线程
	# Cpython解释器在解释代码过程中容易产生数据不安全的问题
    # GIL 全局解释器锁 锁的是线程

threading模块

线程的创建

需要注意的是:进程multiprocessing需要写在if __name__ == '__main__'下面,线程threading则不用。

part1

import time
from threading import Thread


# 多线程并发
def func(n):
    time.sleep(1)
    print(n)


for index in range(10):
    t = Thread(target=func, args=(index,))
    t.start()

"""
451
279


8
6

30
"""

part2

通过继承Thread类,然后如果要传参一样记得要super一下父类的构造方法,然后新增自己的。

import time
from threading import Thread


class MyThread(Thread):
    def __init__(self, name, age):
        super().__init__()
        self.name = name
        self.age = age

    def run(self):
        time.sleep(1)
        print(self.name, f'{self.age}岁啦!')


for age in range(3, 18):
    t = MyThread('小满', age)
    t.start()

"""
小满小满小满小满    3岁啦!7岁啦!

4岁啦!8岁啦!

小满 10岁啦!
小满小满小满小满 小满 14岁啦! 9岁啦!小满小满   6岁啦!16岁啦!

 15岁啦!


小满 12岁啦!11岁啦!小满 小满 5岁啦!17岁啦!



13岁啦!
"""

part3

主线程和子线程永远都在同一个进程里面,所以并没有开启多进程

import os
from threading import Thread


def func(a, b):
    n = a + b
    print(n, os.getpid())

print("主线程pid", os.getpid())

for index in range(10):
    Thread(target=func, args=(index, 5)).start()



"""
主线程pid 26568
5 26568
6 26568
7 26568
8 26568
9 26568
10 26568
11 26568
12 26568
13 26568
14 26568
"""

part4

数据共享问题:在同一个进程的多个线程之间,数据是共享的

import os
from threading import Thread


def func(a, b):
    global g
    g = 0
    print(g, os.getpid())


print("主线程pid", os.getpid())
g = 100

t_list = []
for index in range(10):
    t = Thread(target=func, args=(index, 5))
    t.start()
    t_list.append(t)

for t in t_list:
    t.join()

print(g)

"""
主线程pid 12220
0 12220
0 12220
0 12220
0 12220
0 12220
0 12220
0 12220
0 12220
0 12220
0 12220
0
"""

全局解释器锁GIL

  Python代码的执行由Python虚拟机(也叫解释器主循环)来控制。Python在设计之初就考虑到要在主循环中,同时只有一个线程在执行。虽然 Python 解释器中可以“运行”多个线程,但在任意时刻只有一个线程在解释器中运行。
  对Python虚拟机的访问由全局解释器锁(GIL)来控制,正是这个锁能保证同一时刻只有一个线程在运行。

  在多线程环境中,Python 虚拟机按以下方式执行:

  a、设置 GIL;

  b、切换到一个线程去运行;

  c、运行指定数量的字节码指令或者线程主动让出控制(可以调用 time.sleep(0));

  d、把线程设置为睡眠状态;

  e、解锁 GIL;

  d、再次重复以上所有步骤。
  在调用外部代码(如 C/C++扩展函数)的时候,GIL将会被锁定,直到这个函数结束为止(由于在这期间没有Python的字节码被运行,所以不会做线程切换)编写扩展的程序员可以主动解锁GIL。

全局解释器锁 GIL
	- 同一个时刻只有一个线程访问CPU
	
锁的是什么?
	- 线程
	
这个是python语言的问题吗?
	- 不是,是Cpython解释器的特性

多进程对现场速度对比

part1

计算的数据量比较小,多进程的优势体现不出来

import time
from multiprocessing import Pool
from concurrent.futures import ThreadPoolExecutor


def func(n: int):
    r = n + n


urls = [item for item in range(200)]

if __name__ == '__main__':
    start = time.time()
    with Pool() as pool:
        pool.map(func, urls)
    end = time.time()
    t1 = end - start

    start = time.time()
    with ThreadPoolExecutor() as executor:
        executor.map(func, urls)
    end = time.time()
    t2 = end - start

    print("多进程", t1)
    print("多线程", t2)

"""
多进程 0.8933084011077881
多线程 0.011969327926635742
"""

part2

计算的数据量比较大,多进程更有优势

import time
from multiprocessing import Pool
from concurrent.futures import ThreadPoolExecutor


def func(n: int):
    r = n + n


urls = [item for item in range(2000000)]

if __name__ == '__main__':
    start = time.time()
    with Pool() as pool:
        pool.map(func, urls)
    end = time.time()
    t1 = end - start

    start = time.time()
    with ThreadPoolExecutor() as executor:
        executor.map(func, urls)
    end = time.time()
    t2 = end - start

    print("多进程", t1)
    print("多线程", t2)

"""
多进程 3.6921138763427734
多线程 83.95533204078674
"""

使用多线程实现socket服务端的并发效果

多进程内不能input,多线程内没问题

# config.py
IP = '127.0.0.1'
PORT = 8080
# 服务端

from socket import *
from config import *
from threading import Thread

server = socket()
server.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
server.bind((IP, PORT))
server.listen()


def chat(conn, addr):
    conn.send(f'hello {addr}'.encode())
    ret = conn.recv(1024).decode()
    print(ret)
    conn.close()


while True:
    conn, addr = server.accept()
    Thread(target=chat, args=(conn, addr)).start()

server.close()

# 客户端
import socket
from config import *

client = socket.socket()
client.connect((IP, PORT))
msg = client.recv(1024).decode()
print(msg)

inp = input(">>>").strip().encode()
client.send(inp)

client.close()

线程模块中的其它方法

threading.current_thread() 查看线程名
threading.get_ident() 查看线程ID
threading.active_count() 查看存活的线程数量

part1

current_thread() 和 get_ident()

import threading


def func(n):
    print(n, threading.current_thread(), threading.get_ident())


print(threading.current_thread())
for idx in range(10):
    threading.Thread(target=func, args=(idx,)).start()

"""
<_MainThread(MainThread, started 16160)>
0 <Thread(Thread-1 (func), started 21576)> 21576
1 <Thread(Thread-2 (func), started 21616)> 21616
2 <Thread(Thread-3 (func), started 32196)> 32196
3 <Thread(Thread-4 (func), started 22400)> 22400
4 <Thread(Thread-5 (func), started 26096)> 26096
5 <Thread(Thread-6 (func), started 15268)> 15268
6 <Thread(Thread-7 (func), started 8508)> 8508
7 <Thread(Thread-8 (func), started 27828)> 27828
8 <Thread(Thread-9 (func), started 6892)> 6892
9 <Thread(Thread-10 (func), started 17100)> 17100
"""

part2

threading.active_count()

import time
import threading


def func():
    time.sleep(0.5) # 加上一点小延迟 不然线程全死了


for idx in range(10):
    threading.Thread(target=func).start()

print('存活的线程', threading.active_count())

"""
存活的线程 11  
"""
# 1个主线程 + 10个子线程

part3

threading.enumerate()

import time
import threading


def func():
    time.sleep(0.5)
    pass


for idx in range(10):
    threading.Thread(target=func).start()

print(threading.enumerate(), len(threading.enumerate()))

"""
[<_MainThread(MainThread, started 13352)>, 
<Thread(Thread-1 (func), started 20264)>, 
<Thread(Thread-2 (func), started 8432)>, 
<Thread(Thread-3 (func), started 24016)>, 
<Thread(Thread-4 (func), started 31448)>, 
<Thread(Thread-5 (func), started 25884)>, 
<Thread(Thread-6 (func), started 17600)>, 
<Thread(Thread-7 (func), started 15580)>, 
<Thread(Thread-8 (func), started 25484)>, 
<Thread(Thread-9 (func), started 16156)>, 
<Thread(Thread-10 (func), started 12420)>] 
11
"""

守护线程

无论是进程还是线程,都遵循:守护xx会等待主xx运行完毕后被销毁。****需要强调的是:运行完毕并非终止运行

1.对主进程来说,运行完毕指的是主进程代码运行完毕
2.对主线程来说,运行完毕指的是主线程所在的进程内所有非守护线程统统运行完毕,主线程才算运行完毕
主进程在其代码结束后就已经算运行完毕了(守护进程在此时就被回收),然后主进程会一直等非守护的子进程都运行完毕后回收子进程的资源(否则会产生僵尸进程),才会结束,
主线程在其他非守护线程运行完毕后才算运行完毕(守护线程在此时就被回收)。`因为主线程的结束意味着进程的结束,进程整体的资源都将被回收,而进程必须保证非守护线程都运行完毕后才能结束`

part1

import time
from threading import Thread


def func1():
    print(666)
    time.sleep(10)


def func2():
    pass


t = Thread(target=func1, )
t.start()

# 主线程会等子线程的结束
"""
打印666 然后程序等待10秒后结束
"""

part2

import time
from threading import Thread


def func1():
    print(666)
    time.sleep(10)


def func2():
    pass


t = Thread(target=func1, )
t.daemon = True  # 设置子线程为守护线程,主线程结束,子线程随之结束,不用等待子线程执行完毕
t.start()

print('这是主线程')

"""
666
这是主线程
"""

part3

import time
from threading import Thread


def func1():
    while True:
        print('*' * 10)
        time.sleep(1)


def func2():
    print('in func2')
    time.sleep(5)


t = Thread(target=func1, )
t.daemon = True  # 设置子线程为守护线程,主线程结束,子线程随之结束,不用等待子线程执行完毕
t.start()

t2 = Thread(target=func2, )
t2.start()

print('这是主线程')

"""
**********
in func2
这是主线程
**********
**********
**********
**********
"""

# 守护进程随着主进程代码的执行结束而结束
# 守护线程会在主线程结束之后等待其它子线程的结束才结束
# 主进程在执行完自己的代码之后不会立即结束 而是等待子进程程序结束之后 回收子进程的资源

线程锁

part1

import time
from threading import Lock, Thread


def func():
    global n
    temp = n
    n = temp - 1


n = 10
t_list = []
for index in range(10):
    t = Thread(target=func, )
    t.start()
    t_list.append(t)

for t in t_list:
    t.join()

print(n)

"""
0
"""

part2

import time
from threading import Lock, Thread


def func():
    global n
    temp = n
    time.sleep(0.2)  # 因为这里睡了0.2秒 在在这个睡0.2秒的时间还没有执行之前,每一个线程都拿到这个N了,就是都拿到10了,然后睡了0.2秒之后将结果减去1并赋值给n 所以结果是9
    n = temp - 1


n = 10
t_list = []
for index in range(10):
    t = Thread(target=func, )
    t.start()
    t_list.append(t)

for t in t_list:
    t.join()

print(n)


"""
9
"""

part3

import time
from threading import Lock, Thread

lock = Lock()  # 同进程锁一样 


def func():
    global n

    lock.acquire()  # 加锁
    temp = n
    time.sleep(0.2)
    n = temp - 1
    lock.release()  # 释放锁


n = 10
t_list = []
for index in range(10):
    t = Thread(target=func, )
    t.start()
    t_list.append(t)

for t in t_list:
    t.join()

print(n)

"""
0
"""

死锁

科学家吃面

import time
from threading import Lock, Thread

# 科学家吃面
nood_lock = Lock()
fork_lock = Lock()


def eat1(name):
    nood_lock.acquire()
    print(f"{name} 拿到面条啦")
    fork_lock.acquire()
    print(f"{name} 拿到叉子了")
    print(f'{name} 准备吃面')
    fork_lock.release()
    nood_lock.release()


def eat2(name):
    fork_lock.acquire()
    print(f"{name} 拿到叉子了")
    time.sleep(1)
    nood_lock.acquire()
    print(f"{name} 拿到面条啦")
    print(f'{name} 准备吃面')
    nood_lock.release()
    fork_lock.release()


Thread(target=eat1, args=('小满',)).start()
Thread(target=eat2, args=('小乔',)).start()
Thread(target=eat1, args=('大乔',)).start()
Thread(target=eat1, args=('海月',)).start()

"""
小满 拿到面条啦
小满 拿到叉子了
小满 准备吃面
小乔 拿到叉子了
大乔 拿到面条啦

然后程序一直阻塞
"""

递归锁

from threading import RLock

rlock = RLock()
rlock.acquire()
rlock.acquire()
rlock.acquire()
rlock.acquire()
print('正常打印')

"""
正常打印
"""

解决科学家吃面的问题

递归锁在同一个线程里拿多少次钥匙都可以,递归锁是为了解决死锁问题,且递归锁在一个线程里可以acquire()多次。

import time
from threading import RLock, Thread

# 科学家吃面
nood_lock = fork_lock = RLock()  # 一个钥匙串的两把钥匙


def eat1(name):
    nood_lock.acquire()  # 一把钥匙, 拿到了一把钥匙就意味着拿到了一串钥匙
    print(f"{name} 拿到面条啦")
    fork_lock.acquire()
    print(f"{name} 拿到叉子了")
    print(f'{name} 准备吃面')
    print()
    fork_lock.release()
    nood_lock.release()


def eat2(name):
    fork_lock.acquire()  # 另外一把钥匙
    print(f"{name} 拿到叉子了")
    time.sleep(1)
    nood_lock.acquire()
    print(f"{name} 拿到面条啦")
    print(f'{name} 准备吃面')
    print()
    nood_lock.release()
    fork_lock.release()


Thread(target=eat1, args=('小满',)).start()
Thread(target=eat2, args=('小乔',)).start()
Thread(target=eat1, args=('大乔',)).start()
Thread(target=eat1, args=('海月',)).start()

"""
小满 拿到面条啦
小满 拿到叉子了
小满 准备吃面

小乔 拿到叉子了
小乔 拿到面条啦
小乔 准备吃面

大乔 拿到面条啦
大乔 拿到叉子了
大乔 准备吃面

海月 拿到面条啦
海月 拿到叉子了
海月 准备吃面
"""

信号量

和多进程的信号量一样的,同一个时间只能由N个线程执行这段代码,而且是被acquire保护的代码

import threading
import time
from threading import Semaphore, Thread


def func(sem: threading.Semaphore, a, b):
    sem.acquire()
    time.sleep(1)
    print(a + b)
    sem.release()


sem = Semaphore(4)
for index in range(10):
    t = Thread(target=func, args=(sem, index, index + 5))
    t.start()

"""
75
9

11
171915

13

2321
"""

事件

同进程的事件

# 事件被创建的时候
    # False状态
# wait() 阻塞
# True状态
# wait() 非阻塞
# clear() 设置状态为False
# set() 设置状态为True

# 数据库 - 文件夹
# 文件夹里有好多excel表格
# 1. 能够更方便的对数据进行增删改查
# 2. 安全访问的机制

# 案例
# 起两个线程
    # 第一个线程:链接数据库
    # 等待一个信号 告诉我们之间的网络是通的

# 第二个线程:检测数据库可链接情况
    # time.sleep(0, 2) 2
    # 将事件的状态设置为True

import time
import random
from threading import Thread, Event


def connect_db(e):
    count = 0
    while count < 3:
        e.wait(1)  # 状态为False的时候,我只等待1秒钟
        if e.is_set() is True:
            print("链接数据库")
            break
        else:
            print(f"第{count}次连接失败")
            count += 1
    else:
        raise TimeoutError('数据库链接超时')


def check_web(e):
    time.sleep(random.randint(0, 3))
    e.set()


e = Event()
t1 = Thread(target=connect_db, args=(e,))
t2 = Thread(target=check_web, args=(e,))
t1.start()
t2.start()

"""
链接数据库
"""

红绿灯

import random
import threading
import time


def cars(event: threading.Event, index, lock):
    lock.acquire() # 这里记得要加锁,不然结果是乱的

    if not event.is_set():
        print(f"\033[33m第{index}辆车正在等红绿灯.\033[0m")
        event.wait()
    print(f"\033[45m第{index}辆车已通过红绿灯.\033[0m")

    lock.release()


def light(event: threading.Event):
    while True:
        if event.is_set():
            event.clear()
            print("\033[31m红灯亮了\033[0m")
        else:
            event.set()
            print("\033[32m绿灯亮了\033[0m")
        time.sleep(2)


if __name__ == '__main__':
    e = threading.Event()
    lock = threading.Lock()

    # 创建并启动交通灯线程
    traffic = threading.Thread(target=light, kwargs=({"event": e}))
    traffic.daemon = True
    traffic.start()

    car_list = []
    for i in range(1, 11):
        # 创建并启动车辆线程
        t = threading.Thread(target=cars, kwargs=({"event": e, 'index': i, "lock": lock}))
        t.start()
        car_list.append(t)
        time.sleep(random.random())

    # 等待所有车辆线程结束
    [car.join() for car in car_list]

    print("所有车辆已通过,程序结束。")

"""
绿灯亮了
第1辆车已通过红绿灯.
第2辆车已通过红绿灯.
第3辆车已通过红绿灯.
第4辆车已通过红绿灯.
第5辆车已通过红绿灯.
第6辆车已通过红绿灯.
红灯亮了
第7辆车正在等红绿灯.
绿灯亮了
第7辆车已通过红绿灯.
第8辆车已通过红绿灯.
第9辆车已通过红绿灯.
第10辆车已通过红绿灯.
所有车辆已通过,程序结束。
"""

条件

Condition

# 条件
# from threading import Condition


# 条件
# 可以理解为更复杂的锁
# 除了有 acquire release 之外
# 还有 wait和notify

# 一个条件被创建之初 默认有一个False状态
# False 会影响 wait一直处于等待状态
# notify (int的数据类型) 制造N把钥匙

from threading import Thread, Condition


def func(con, index):
    con.acquire()
    con.wait()  # 等钥匙 且使用了条件造的钥匙,子线程使用了之后不会归还
    print(f'在第{index}个循环里')
    con.release()


con = Condition()
for index in range(10):
    Thread(target=func, args=(con, index)).start()
while True:
    num = int(input(">>>"))
    con.acquire()
    con.notify(num)  # 造钥匙
    con.release()

"""
>>>2
>>>在第0个循环里
在第1个循环里
3
>>>在第2个循环里
在第3个循环里
在第4个循环里
5
>>>在第5个循环里
在第9个循环里
在第7个循环里
在第8个循环里
在第6个循环里
1
>>>1
>>>1
线程结束了,继续输入就进入死循环了
"""

# 写代码很少用到,一般会出现在面试题里面

定时器(了解)

Timer

# 使用Timer时不用引入Thread
import time
from threading import Timer


def func():
    print("时间同步")


while True:
    Timer(2, func).start()  # 延迟几秒钟去起一个线程, 做一个时间同步
    time.sleep(2)

"""
时间同步
时间同步
一直循环
"""

队列

# 队列是数据安全的
import queue

# q = queue.Queue()  # 队列  先进先出
# q.put()
# q.get()
# q.put_nowait()
# q.get_nowait()

# q = queue.LifoQueue()  # 栈  先进后出
# q.put(1)
# q.put(2)
# q.put(3)
#
# print(q.get())  # 3
# print(q.get())  # 2

q = queue.PriorityQueue()  # 优先级队列  谁的数小 谁先出来 如果优先级一样 就按照ASCII码排序
q.put((10, 'a'))
q.put((30, 'b'))
q.put((5, 'c'))
q.put((20, 'd'))
q.put((1, 'A'))
q.put((1, 'B'))

print(q.get())
print(q.get())
print(q.get())
print(q.get())
print(q.get())
print(q.get())

"""
(1, 'A')
(1, 'B')
(5, 'c')
(10, 'a')
(20, 'd')
(30, 'b')
"""

基于threading和queue的生产者消费者模式(待优化)

import queue
import random
import time
from threading import Thread


def producer(q, name, index):
    food_list = ['包子', '豆浆', '油条', '煎饼']
    for i in range(1, index + 1):
        food = random.choice(food_list)
        time.sleep(random.randint(1, 3))
        rel_food = f'{name}生产的第{i}个{food}.'
        print(rel_food)
        q.put(rel_food)


def consumer(q, name):
    while True:
        food = q.get()
        if food is None:
            break
        print(f"{name} 得到了 {food}")
        time.sleep(random.randint(1, 3))


q = queue.Queue(5)

p = Thread(target=producer, kwargs={'q': q, 'name': '小乔', 'index': 5})
p1 = Thread(target=producer, kwargs={'q': q, 'name': '大乔', 'index': 5})
c1 = Thread(target=consumer, kwargs={'q': q, 'name': '小满'})
c2 = Thread(target=consumer, kwargs={'q': q, 'name': '阿珂'})

p.start()
p1.start()
c1.daemon = True
c2.daemon = True
c1.start()
c2.start()

p.join()
p1.join()

q.put(None)
q.put(None)

"""
大乔生产的第1个包子.
小满 得到了 大乔生产的第1个包子.
小乔生产的第1个油条.
阿珂 得到了 小乔生产的第1个油条.
大乔生产的第2个油条.
小满 得到了 大乔生产的第2个油条.
小乔生产的第2个包子.
阿珂 得到了 小乔生产的第2个包子.
小乔生产的第3个煎饼.
大乔生产的第3个油条.
小满 得到了 小乔生产的第3个煎饼.
阿珂 得到了 大乔生产的第3个油条.
小乔生产的第4个豆浆.
小满 得到了 小乔生产的第4个豆浆.
大乔生产的第4个豆浆.
小乔生产的第5个油条.
阿珂 得到了 大乔生产的第4个豆浆.
小满 得到了 小乔生产的第5个油条.
大乔生产的第5个包子.
阿珂 得到了 大乔生产的第5个包子
"""

线程池

进程池和线程池在一个新的模块里面concurrent.futures

part1

import time
from concurrent.futures import ThreadPoolExecutor


def func(index):
    time.sleep(2)
    print(index)


t_pool = ThreadPoolExecutor(max_workers=5)  # 线程池不要超过cpu个数*5  比如我的电脑是4c8t 开进程池最多开5个 线程池最多开20个

for index in range(15):
    t_pool.submit(func, index)  # 这里传参直接传就可以了,方法内部是*args和**kwargs 不定长参数

"""
413


0
2
9675



8
131210

11

14
"""

part2

import time
from concurrent.futures import ThreadPoolExecutor


def func(index):
    time.sleep(2)
    print(index)


t_pool = ThreadPoolExecutor(max_workers=5)  # 线程池不要超过cpu个数*5  比如我的电脑是4c8t 开进程池最多开5个 线程池最多开20个

for index in range(15):
    t_pool.submit(func, index)

t_pool.shutdown()  # 等于之前学过的  close() + join()  close 关闭池子不让新任务进去  join 等待任务完成
print("主线程")

"""
234


1
0
56

98

7
1110

1413

12
主线程
"""

part3

返回值

import time
from concurrent.futures import ThreadPoolExecutor


def func(index):
    time.sleep(2)
    print(index)
    return index * index


t_pool = ThreadPoolExecutor(max_workers=5)  # 线程池不要超过cpu个数*5  比如我的电脑是4c8t 开进程池最多开5个 线程池最多开20个

t_list = []
for index in range(10):
    t = t_pool.submit(func, index)
    t_list.append(t)

t_pool.shutdown()  # 等于之前学过的  close() + join()
print("主线程")

for t in t_list:
    print(t.result())   # 这里的打印结果一定是按顺序的,因为上面t_list.append(t)的时候就算按顺序进去的

"""
43
21
0


7
85

6
9
主线程
0
1
4
9
16
25
36
49
64
81
"""

part4

as_completed

import time
from concurrent.futures import ThreadPoolExecutor, as_completed


def func(index):
    time.sleep(2)
    print(index)
    return index * index


t_pool = ThreadPoolExecutor(max_workers=5)  # 线程池不要超过cpu个数*5  比如我的电脑是4c8t 开进程池最多开5个 线程池最多开20个

t_list = []
for index in range(10):
    t = t_pool.submit(func, index)
    t_list.append(t)

t_pool.shutdown()  # 等于之前学过的  close() + join()
print("主线程")

for t in as_completed(t_list):  # 使用as_completed()函数来迭代已完成的任务, 就是谁完成了,就执行,提高了效率,不过顺序是乱的。
    print('***', t.result())

"""
431


2
0
5867



9
主线程
*** 49
*** 81
*** 4
*** 0
*** 16
*** 36
*** 64
*** 1
*** 9
*** 25
"""

进程池

把线程池换成进程池,只需要把ThreadPoolExecutor替换成ProcessPoolExecutor,然后放到if __name__ == '__main__'里面就可以了

import time
from concurrent.futures import ProcessPoolExecutor, as_completed


def func(index):
    time.sleep(2)
    print(index)
    return index * index


if __name__ == '__main__':
    t_pool = ProcessPoolExecutor(max_workers=5)  # 线程池不要超过cpu个数*5  比如我的电脑是4c8t 开进程池最多开5个 线程池最多开20个
    t_list = []
    for index in range(10):
        t = t_pool.submit(func, index)
        t_list.append(t)

    t_pool.shutdown()  # 等于之前学过的  close() + join()
    print("主进程")

    for t in as_completed(t_list):  # 使用as_completed()函数来迭代已完成的任务, 就算谁完成了,就执行,提高了效率,不过顺序是乱的。
        print('***', t.result())

"""
0
12

3
4
5
76

89

主进程
*** 0
*** 64
*** 9
*** 1
*** 16
*** 36
*** 49
*** 4
*** 81
*** 25
"""

如果使用map就拿不到返回值了

import time
from concurrent.futures import ProcessPoolExecutor, as_completed, ThreadPoolExecutor


def func(index):
    time.sleep(2)
    print(index)


if __name__ == '__main__':
    t_pool = ThreadPoolExecutor(max_workers=10)  # 不要超过cpu个数*5  比如我的电脑是4c8t 开进程池最多开5个 线程池最多开20个
    t_pool.map(func, range(20))  # 如果是map 后面放的是可迭代对象,需要注意

shutdown相关

如果不需要等待子线程结束做一些操作,完全可以不用写shutdown(即 jon() + close()) ,这样速度可以提高很多。

import time
from concurrent.futures import ProcessPoolExecutor, as_completed, ThreadPoolExecutor


def func(index):
    time.sleep(2)
    print(index)
    return index * index


if __name__ == '__main__':
    t_pool = ThreadPoolExecutor(max_workers=10)  # 不要超过cpu个数*5  比如我的电脑是4c8t 开进程池最多开5个 线程池最多开20个
    # t_pool.map(func, range(20))
    t_list = []
    for index in range(10):
        t = t_pool.submit(func, index)
        t_list.append(t)

    # t_pool.shutdown()  # 等于之前学过的  close() + join()
                         # 如果不需要等待子进程结束的一些特殊操作,可以不用写shutdown() 这样会提高很多效率和速度
    print("主进程")

    for t in as_completed(t_list):  # 使用as_completed()函数来迭代已完成的任务, 就是谁完成了,就执行,提高了效率,不过顺序是乱的。
        print('***', t.result())

# 速度更快了
"""
主进程
967
45
8
3
*** 8110
2



*** 25
*** 64
*** 9


*** 16
*** 49
*** 36
*** 1
*** 4
*** 0
"""

回调函数 add_done_callback

参考下面的案例

import time
import psutil
from concurrent.futures import ThreadPoolExecutor


def func(index):
    time.sleep(0.1)
    return index * index


def call_back(s):
    print("结果是", s.result())


pool = ThreadPoolExecutor(psutil.cpu_count() // 2 * 5)  # 获取最合适的线程数

for index in range(100):
    pool.submit(func, index).add_done_callback(call_back)

# 结果略

协程

先看一个生成器的例子实现的一个简单的协程

# 进程 启动多个进程,进程之间由操作系统负责调用
# 线程 启动多个线程,真正被CPU执行的最小单位实际是线程
    # 开启一个线程 创建一个线程 寄存器 堆栈
    # 关闭一个线程
    # 都需要时间
# 协程
    # 本质上是一个线程
    # 能够再多个任务之间切换来节省一些IO时间
    # 协程中任务之间的切换也消耗时间,但开销远远要小于进程线程之间的切换

# 进程、线程、协程 都是实现并发的重要手段

def consumer():
    while True:
        x = yield
        print('处理了数据', x)


def producer():
    c = consumer()
    next(c)
    for i in range(10):
        print('生产了数据', i)
        c.send(i)


producer()


"""
生产了数据 0
处理了数据 0
生产了数据 1
处理了数据 1
...
生产了数据 8
处理了数据 8
生产了数据 9
处理了数据 9
"""

asyncio模块

asynico模块通过一个线程执行并发任务

通过async和wait关键字提供支持

通过事件循环来实现

image-20240123171109253

定义协程

使用async关键字来指定一个函数为协程

没有加的就算一个普通函数,加了async的才是协程

async def call_api():
    # 函数的内容
    pass

part1

import asyncio


async def calculate(n1: int, n2: int):
    return n1 + n2


o = calculate(1, 2)
print(o)

"""
<coroutine object calculate at 0x00000216FFF126C0>
sys:1: RuntimeWarning: coroutine 'calculate' was never awaited
"""

# 使用普通方法调用,并不是调用这个函数,而是得到了一个协程对象。

part2

使用asycnio.run()函数来运行协程

import asyncio


async def calculate(n1: int, n2: int):
    res = n1 + n2
    print(res)


asyncio.run(calculate(1, 2))  # asyncio.run 会启动事件循环,然后把协程扔到事件循环里,让事件循环去运行

"""
3
"""

part3

使用await()来运行另一个协程

import asyncio


async def calculate(n1: int, n2: int):
    res = n1 + n2
    print(res)


async def main():
    print("main -step 1")
    await calculate(1, 2)  # 如何再一个协程里面调用另外一个协程
    print('main -step 2')  # 需要再前面加一个await


asyncio.run(main())  # asyncio.run 会启动事件循环,然后把协程扔到事件循环里,让事件循环去运行

"""
main -step 1
3
main -step 2
"""

创建任务

使用asycnio.creat_task来创建任务

事件循环:谁先醒来,先执行谁

part1

import asyncio
import time


async def call_api(name: str, delay: float):
    print(f"{name} - step 1")
    await asyncio.sleep(delay)  # 这里只能用asyncio的sleep不能用time模块的
                                # 因为整个协程用在同一个线程里面,如果用time.sleep这个线程就自动切换了
                                # 一个协程里面,如果进调用了await,那么这个协程就会进入等待状态
                                # 事件循环就会把他扔一边了
    print(f"{name} - step 2")

async def main():
    time_1 = time.perf_counter()
    print("start A coroutine")
    await call_api('A', 3)
    print("finished A coroutine")

    print("start B coroutine")
    await call_api('B', 3)
    print("finished B coroutine")

    time_2 = time.perf_counter()
    print(f"Spent {time_2 - time_1}")

asyncio.run(main())

"""
A - step 1
A - step 2
finished A coroutine
start B coroutine
B - step 1
B - step 2
finished B coroutine
Spent 6.014270999992732
"""

part2

import time
import asyncio


async def call_api(name: str, delay: float):
    print(f"{name} - step 1")
    await asyncio.sleep(delay)
    print(f"{name} - step 2")


async def main():
    time_1 = time.perf_counter()
    print("start A coroutine")
    task_1 =asyncio.create_task(call_api('A', 3))  # main不会在这一行等待(这里没有等待),会把这个协程包装成一个任务,
                                           # 交给事件循环,事件循环什么时候执行它,已经不管了,程序会继续向下
    print("start B coroutine")
    task_2 = asyncio.create_task(call_api('B', 3))

    await task_1
    print("task 1 completed")

    await task_2
    print("task 2 completed")

    time_2 = time.perf_counter()  # 当程序执行到这里,A协程和B协程还没有执行,因为main一直在执行,所以其它协程没有机会执行
    print(f"Spent {time_2 - time_1}")


asyncio.run(main())

"""
start A coroutine
start B coroutine
A - step 1
B - step 1
A - step 2
B - step 2
task 1 completed
task 2 completed
Spent 3.0040691000176594
"""

part3

import time
import asyncio


async def call_api(name: str, delay: float):
    print(f"{name} - step 1")
    await asyncio.sleep(delay)
    print(f"{name} - step 2")


async def main():
    time_1 = time.perf_counter()
    print("start A coroutine")
    task_1 =asyncio.create_task(call_api('A', 3))  # main不会在这一行等待(这里没有等待),会把这个协程包装成一个任务,
                                           # 交给事件循环,事件循环什么时候执行它,已经不管了,程序会继续向下
    print("start B coroutine")
    task_2 = asyncio.create_task(call_api('B', 2))  # 修改了B的时间

    await task_1
    print("task 1 completed")

    await task_2
    print("task 2 completed")

    time_2 = time.perf_counter()  # 当程序执行到这里,A协程和B协程还没有执行,因为main一直在执行,所以其它协程没有机会执行
    print(f"Spent {time_2 - time_1}")


asyncio.run(main())

"""
start A coroutine
start B coroutine
A - step 1
B - step 1
B - step 2
A - step 2
task 1 completed
task 2 completed
Spent 3.0237911000149325
"""

part4

import time
import asyncio


async def call_api(name: str, delay: float):
    print(f"{name} - step 1")
    await asyncio.sleep(delay)
    print(f"{name} - step 2")


async def main():
    time_1 = time.perf_counter()
    print("start A coroutine")
    task_1 =asyncio.create_task(call_api('A', 2))  # main不会在这一行等待(这里没有等待),会把这个协程包装成一个任务,
                                           # 交给事件循环,事件循环什么时候执行它,已经不管了,程序会继续向下
    print("start B coroutine")
    task_2 = asyncio.create_task(call_api('B', 5))  # 修改了B的时间

    await task_1
    print("task 1 completed")

    await task_2
    print("task 2 completed")

    time_2 = time.perf_counter()  # 当程序执行到这里,A协程和B协程还没有执行,因为main一直在执行,所以其它协程没有机会执行
    print(f"Spent {time_2 - time_1}")


asyncio.run(main())

"""
start A coroutine
start B coroutine
A - step 1
B - step 1
A - step 2
task 1 completed
B - step 2
task 2 completed
Spent 5.0040694999624975
"""

posted @ 2024-01-17 22:16  小满三岁啦  阅读(44)  评论(0编辑  收藏  举报