Python--并发编程
1.并发和并行的区别是什么?
- 1个核,最大1个并行。可以N个并发。
- 4个核,最大4个并行。可以N个并发。
- 对于人的使用来讲,对并发和并行不感知。
2.四核八线程是什么意思?
- 四核八线程,是指,原来4个脑袋工作,现在让这个4个脑袋比较灵活。可以每个脑袋承担2个任务。所以叫四核八线程。
- 比单纯的4个脑袋,快。
- 比单纯的8个脑袋,慢。
- 性能比较: 真四核< 四核八线程 < 真八核
3.进程是什么?
- 程序运行的过程。
- 程序正在进行的过程。
4.一个任务运行的三种状态?
- 运行态
- 阻塞态(遇到IO阻塞)
- 就绪态(IO阻塞结束)
5.提交任务的两种方式?
- 同步
- 叫完外卖,啥也没干,一直等待外卖。
- 异步
- 叫完外卖,先去干别的事。
6.异步非阻塞是什么意思?
- 异步非阻塞,是两个不同纬度的概念,部分人喜欢放在一起说。
- 异步是提交任务的方式,非阻塞是任务的状态。放在一起更多的是要表达,这种流程很快。
7.同步阻塞是什么意思?
- 跟6一样,也是不同维度的概念。
from multiprocessing import Process import os import time def task(n): print(f"task的父进程{os.getppid()},task的进程{os.getpid()},开始") time.sleep(n) print(f"task的父进程{os.getppid()},task的进程{os.getpid()},结束") if __name__ == '__main__': # p = Process(target=task,args=(3,)) p = Process(target=task,kwargs={"n":3}) # 这样写亦可以 p.start() # 通知操作系统的开始。(异步) print("主进程",os.getpid())
- 同步是提交任务的方式,
- 阻塞是任务的状态。
- 阻塞可能的原因1,计算密集型。(例如:一直在计算十亿的加减法)
- 阻塞可能的原因2,IO密集型。(例如:一直再等待10G的文件下载完成)
8.如何开多进程呢?
- 进程是操作系统负责调用的。
- 在win中用CreateProcess
- 在linux中用fork
- 但这些,都有一个模块替我们封装了。叫from multiprocessing import Process
9.如何开一个进程?
- 利用Process(target=函数,arg=(参数1,参数2...))
-
from multiprocessing import Process import os import time def task(n): print(f"task的父进程{os.getppid()},task的进程{os.getpid()},开始") time.sleep(n) print(f"task的父进程{os.getppid()},task的进程{os.getpid()},结束") if __name__ == '__main__': # p = Process(target=task,args=(3,)) p = Process(target=task,kwargs={"n":3}) # 这样写亦可以 p.start() # 通知操作系统的开始。(异步) print("主进程",os.getpid())
-
效果
10.开子进程的那一刻,会不会把父进程的数据拷贝一份到子进程中?
- 会的。
- 子进程在开启的那一刻,是共享父进程的数据的。
11.子进程开启后,后续的执行,是否影响父进程?
- 不影响
- 彼此独立。
- 也就是,子进程运行的过程中。产生的数据只在子进程自己里面。内存空间是彼此独立的。
12.可以用类的方式开启进程吗?
- 继承Process,把要执行的内容放入run中,参数放在init中。
- 示例:
-
from multiprocessing import Process import os import time class MyProcess(Process): def __init__(self,n): super().__init__() self.n = n def run(self) -> None: print(f"task的父进程{os.getppid()},task的进程{os.getpid()},开始") time.sleep(self.n) print(f"task的父进程{os.getppid()},task的进程{os.getpid()},结束") if __name__ == '__main__': p = MyProcess(2) p.start() # 通知操作系统的开始。(异步) print("主进程",os.getpid())
13.进程间的内存空间彼此隔离?
- 是的
-
from multiprocessing import Process import time count = 100 def task(): global count count = 0 if __name__ == '__main__': p = Process(target=task) p.start() # 通知操作系统的开始。(异步) time.sleep(3) print("主进程", count)
14.p.join是替代time.sleep?
- 是的。
- 让主进程等待子进程,我们可以写time.sleep
- 但却不够灵活。
- 所以用p.join代替。
- 代码:
-
from multiprocessing import Process import os import time def task(n): print(f"task的父进程{os.getppid()},task的进程{os.getpid()},开始") time.sleep(n) print(f"task的父进程{os.getppid()},task的进程{os.getpid()},结束") if __name__ == '__main__': p = Process(target=task,args=(3,)) p.start() p.join() # 主进程等待子进程 替换原来的time.sleep print("主进程",os.getpid())
-
15.下面代码一共花费的时间是?
-
from multiprocessing import Process import os import time def task(n): print(f"task的进程{os.getpid()},开始") time.sleep(n) if __name__ == '__main__': p1 = Process(target=task,args=(3,)) p2 = Process(target=task,args=(2,)) p3 = Process(target=task,args=(1,)) start = time.time() p1.start() p2.start() p3.start() p1.join() p2.join() p3.join() end = time.time() print("一共花费的时间:",end-start) print("主进程",os.getpid())
- 3秒左右。
16.其他知识点
- p.name获取进程名字
- p.pid 获取进程id
- p.is_alive() 进程是否存活
- p.terminate() 结束进程
- p.kill() 结束进程
-
from multiprocessing import Process import os import time def task(n): time.sleep(n) print(f"task的父进程{os.getppid()},task的进程{os.getpid()},结束") if __name__ == '__main__': p = Process(target=task,args=(3,),name="进程a") p.start() print(p.is_alive()) # 是否存活 p.terminate() # 结束进程,(发送指令让操作系统结束这个进程) time.sleep(0.01) print(p.is_alive()) # 是否存活 p.join() print(p.name, p.pid) # 进程的名字, 进程的id # print(p.is_alive()) # 是否存活 # p.terminate() # 结束进程,(发送指令让操作系统结束这个进程) print("主进程",os.getpid())
17.如何让自己开发的程序变快?
- 我们开发的应用程序,是跟操作系统打交道的。
- 让程序变快,就是尽量把CPU抱在自己怀里。
- CPU的是计算型的。它遇到IO就会跳过。
- 也就是说,我们在设计的程序的时候,尽量减少IO,相应的就多了计算。
- 那么CPU相对来讲,就在我这边的时间就多了。也就变快了。
18.IO分为哪些?如何减少IO呢?
- IO有读写硬盘的IO,有网络通信的IO。
- 这里面最慢的就是网络通信的IO。
- 至于如何减少IO,看一下chatGPT的回答:
-
减少IO操作可以提高Python程序的效率。以下是一些设计技巧可以减少IO操作:
-
使用内存缓存 内存是非常快的,使用内存缓存可以避免频繁地读写文件。可以将经常使用的数据缓存到内存中,以便程序在需要时快速访问它们。
-
批量处理数据 如果可能的话,尽可能将数据分成批次处理,而不是逐个处理。这样可以减少IO操作的次数,从而提高程序的效率。
-
使用生成器 如果您需要读取大量数据,并且无法将其全部加载到内存中,则可以使用生成器来逐个生成数据。这样可以避免一次性读取整个文件,而是逐行读取。
-
使用二进制文件 如果您需要处理大型文件,则可以使用二进制文件而不是文本文件。由于二进制文件不需要解析,因此可以更快地读取和写入数据。
-
使用多线程或异步IO 如果您的程序需要进行大量的IO操作,可以使用多线程或异步IO来并行处理IO操作,以提高效率。
-
避免重复的IO操作 如果您在程序中进行了某个IO操作,例如读取一个文件,可以将结果存储在内存中,以避免重复的IO操作。
-
使用压缩文件 如果您需要处理大型文件,可以考虑将它们压缩并解压缩。这样可以减少文件的大小,从而减少IO操作的次数,提高程序的效率。
总之,减少IO操作可以提高Python程序的效率。可以使用以上设计技巧来减少IO操作,以优化程序的性能。
参考自 chatGPT -
19.一个秒杀的场景,如何优化?
- 服务端。增加redis,内存数据库,增加读写效率。
- 客户端。提前缓存好,一些不变的图片。
- 客户端原则是:能从本地(缓存)拿数据,就不要从网络中拿。
- 服务端原则是:能从内存里取,就不要去硬盘里取。
20.主进程的父进程是谁?
- 在pycharm中执行xxx.py文件,xxx.py的父进程就是pycharm
-
from multiprocessing import Process import os import time def task(n): print(f"task的父进程{os.getppid()},task的进程{os.getpid()},开始") time.sleep(n) print(f"task的父进程{os.getppid()},task的进程{os.getpid()},结束") if __name__ == '__main__': p = Process(target=task,args=(1,)) p.start() # 通知操作系统的开始。(异步) print(f"主进程的自己{os.getpid()},主进程的父进程{os.getppid()}") # 主进程的父进程35220 # 在终端敲击 tasklist | findstr 35220 # 可以看到是 pycharm是父进程
- 在终端敲击命令并验证
21.常用的进程命令
- win查看任务,并杀掉进程
-
tasklist | findstr 123456
-
taskkill /F /PID 123465
- linux
-
ps aux | grep 1974 kill -9 1974
22.僵尸进程是什么意思?
- 是linux操作系统中的特殊的数据结构。
- 在linux系统中所有子进程在死后都会进入僵尸进程的状态。
- 僵尸进程,会释放掉重型资源(cpu,内存等),但会保留Pid等信息。
- 是linux方便父进程查看自己都有哪些子进程存在过。
- 僵尸进程会保留id,这是linux为每一个子进程存在过保留的痕迹。
23.父进程如果开了一堆子进程,当子进程都结束了变为僵尸进程,没有及时回收,会发生什么?
- 会留下许多子进程的僵尸id号
- 这些id号,不会占用CPU。
- 会占用一点点内存。
- 最重要的是,这些id会占用id号。而id号是有上限的。
- 最极端的情况就是,会看到cpu够用,内存也够用,但是就是起不来进程。原因就是id号不够用的。
- (这里的id号 指代进程号)
24.造成僵尸进程的原因是?
- 有个不靠谱的爹。
- 一直生儿子,却不管儿子,儿子死了变成僵尸,也不收尸。
25.如何解决僵尸进程?
-
方式1:
-
作为开发者,设计的时候,就要及时清理。
-
- 方式2:
- 干掉那个不靠谱的爹。
- kill -9 父进程的pid
- 这会导致孤儿进程。
25.5如何查看僵尸进程?
- top命令里的zombie显示的僵尸进程的数量。
-
ps aux | grep Z 会列出属于Z状态的进程,就是僵尸进程。
-
26.什么是孤儿进程?
- 父进程被杀掉了,子进程就变为孤儿进程。
27.孤儿进程一定是僵尸进程吗?
- 不是。
- 当父进程被杀掉的时候,子进程活着叫孤儿进程,子进程死掉的,也叫孤儿进程。
- 活着的子进程,叫活着的孤儿进程。(我的理解哈)
- 死的子进程,叫僵尸的孤儿进程。(我的理解哈)
28.子进程变为孤儿进程之后呢?
- 子进程变为孤儿进程,会被pid为1的进程接管。
29.如何在linux模拟一个僵尸进程?
- vim test.py 输入下面内容:
-
#coding:utf-8 from multiprocessing import Process import time,os def run(): print('子',os.getpid()) if __name__ == '__main__': p=Process(target=run) p.start() print('主',os.getpid()) time.sleep(1000)
-
杀掉父进程,就会释放掉。
-
kill -9 父进程的pid
- 查看僵尸进程:
-
ps aux |grep Z
30.python帮我们处理了僵尸进程?
- 是的,更准确的说是导入的Process模块帮我做了。
- 比如,这个p.start模块,帮我在开进程的时候,就会清理一下。
-
比如:这个p.join()也有清理的
-
-
没有什么岁月静好,只是有人在你背后默默付出。(感动了o(╥﹏╥)o)
30.5 僵尸进程是linux专属的概念?
- 是的
- win没有这个概念
31. 什么是守护进程?
- 崇祯皇帝死的时候,他身边的太监王承恩随皇帝一起自缢。
- 这里的太监王承恩就是守护者,守护的是皇帝。
- 在python中,主进程就是皇帝,假设我让一个子进程作为一个守护者。
- 那么,当主进程结束的时候,子进程也会随之结束。
- 代码示例:
-
import os import time from multiprocessing import Process def task(): print(f"进程{os.getpid()} 开启") time.sleep(10) print(f"进程{os.getpid()} 结束,我结束了吗???") # 这一行没执行 if __name__ == '__main__': p = Process(target=task) p.daemon = True # 设置 子进程task 为守护者,类比为太监 p.start() print(f"主进程{os.getppid()}") time.sleep(3)
-
32.守护进程是守护的主进程的生命周期吗?
- 不是!
- 守护的是主进程的代码。
- 一旦主进程的代码运行完毕,守护进程也就随之结束。守护的不是生命周期。
- 代码演示:
-
# 主进程代码运行完毕,守护进程就会结束 from multiprocessing import Process import time def foo(): print("foo函数的", 123) time.sleep(1) print("foo函数的", "end123") def bar(): print("bar函数的", 456) time.sleep(3) print("bar函数的", "end456") if __name__ == '__main__': p1 = Process(target=foo) p2 = Process(target=bar) p1.daemon = True p1.start() p2.start() print("main-------")
33.什么是互斥锁?
- 保护共享资源的一种机制。
34.什么时候用到互斥锁?
- 当有多个进程都要访问同一个资源,并且还要修改这个资源的时候,加上互斥锁,能保护数据的安全性。
- 比如,抢票。抢票抢的是,谁先把数据库里的数据减一就抢到了。
- 但为了保证数据安全,不能能让所有人同时修改数据。
- 就提出了一种锁的机制。谁先抢到锁,谁先改。后面的必须等锁释放掉了,才能继续抢。
35.没有锁的时候,模拟抢票的过程是:
- 代码
import os import time from multiprocessing import Process import json def check(): with open('db.json', mode='rt', encoding='utf-8') as f: time.sleep(1) # 模拟查看的延迟 dic = json.load(f) print(f"{os.getpid()}查看剩余票数为:{dic['count']}") def get(): with open('db.json', mode='rt', encoding='utf-8') as f: time.sleep(1) # 模拟网络延迟 dic = json.load(f) if dic['count'] > 0: # 发现有票 dic['count'] -= 1 # 在内存里减去 time.sleep(3) # 沿着网络发送数据,有延迟 with open('db.json', mode='wt', encoding='utf-8') as f: json.dump(dic, f) # 把内存改好的数据,再存进去 print(f"{os.getpid()}购票成功") def func(): # 模拟人买票的行为 check() get() if __name__ == '__main__': for i in range(10): p = Process(target=func) p.start() print("主进程")
36.有锁后,模拟抢票的行为:
- 代码:
-
import os import time from multiprocessing import Process,Lock import json # mutex = Lock() def check(): with open('db.json', mode='rt', encoding='utf-8') as f: time.sleep(0.1) # 模拟查看的延迟 dic = json.load(f) print(f"{os.getpid()}查看剩余票数为:{dic['count']}") def get(): with open('db.json', mode='rt', encoding='utf-8') as f: time.sleep(0.1) # 模拟网络延迟 dic = json.load(f) if dic['count'] > 0: # 发现有票 dic['count'] -= 1 # 在内存里减去 time.sleep(1) # 沿着网络发送数据,有延迟 with open('db.json', mode='wt', encoding='utf-8') as f: json.dump(dic, f) # 把内存改好的数据,再存进去 print(f"{os.getpid()}购票成功!!") else: print(f"{os.getpid()}购票失败!!") def func(mutex): # 模拟人买票的行为 check() # mutex.acquire() # 锁上 # get() # mutex.release() # 释放 with mutex: get() if __name__ == '__main__': mutex = Lock() for i in range(10): p = Process(target=func,args=(mutex,)) p.start() # p.join() # 串行 print("主进程")
37.这个锁可以放在main以外吗?
- 不可以。
- 每个子进程,都会复制一份main上面的代码。也就会造成创建多把锁。
- 所以,这个锁就没有意义,没有加上。
- 示例代码:
-
import os import time from multiprocessing import Process,Lock import json mutex = Lock() def check(): with open('db.json', mode='rt', encoding='utf-8') as f: time.sleep(0.1) # 模拟查看的延迟 dic = json.load(f) print(f"{os.getpid()}查看剩余票数为:{dic['count']}") def get(): with open('db.json', mode='rt', encoding='utf-8') as f: time.sleep(0.1) # 模拟网络延迟 dic = json.load(f) if dic['count'] > 0: # 发现有票 dic['count'] -= 1 # 在内存里减去 time.sleep(1) # 沿着网络发送数据,有延迟 with open('db.json', mode='wt', encoding='utf-8') as f: json.dump(dic, f) # 把内存改好的数据,再存进去 print(f"{os.getpid()}购票成功!!") else: print(f"{os.getpid()}购票失败!!") def func(): # 模拟人买票的行为 check() mutex.acquire() # 锁上 get() mutex.release() # 释放 # with mutex: # get() if __name__ == '__main__': # mutex = Lock() for i in range(10): p = Process(target=func) p.start() # p.join() # 串行 print("主进程")
38.加锁的精妙之处在?
- 根据需要来加锁,
- 涉及数据安全的时候,加锁。
- 不涉及的时候,不需要加。
- 例如:代码里的抢票行为。
38.队列是什么?
- 队列是基于管道+锁来实现的。
39.同一时间只有一个任务在执行,一定是串行吗?
- 不一定。可以是串行,也可以使并发+锁,导致的。
40.IPC是什么?
- IPC是(Inter-Process Communication),进程间通信的意思。
- 进程间通信是指两个进程的数据之间产生交互
41.共享内存是什么?
- 是让两个进程进行数据交换的。
- 是在内存上实现,所以叫共享内存
- 具体:
- 1.管道
- 2.队列
42.python中的event有什么用?
- 我们知道Python中多进程是相互执行互不干扰的,但是如果多进程之间需要对同一资源对象进行操作或者多个进程之间有相互依赖的,那就需要一个共享变量供多进程使用。
- Python multiprocessing 多进程之间相互协调的方式有如下几种:
- Lock:锁,Queue:队列, Semaphore:信号量 ,Event:事件,Pipe:管道 。
- event是实现进程间的通信的一种方式。
- 参考资料1:https://cloud.tencent.com/developer/article/1485103
43.GIL是什么?
- 是一种线程锁,为了保证数据安全。
- 是Cpython解释器,特有的机制,是绑定在CPython解释器的。(这个解释器也是官方的解释器)
- 因为这把锁,所以导致python开1个进程,N个线程,只有并发的效果。没有并行的效果。
44.在CPython解释器下,如何利用多核优势?
- 因为GIL的影响。
- 所以,需要开N个进程。
- 开N个进程,就是为了跳过GIL锁,或者说,是同时产出N个GIL锁,有多个进程去抢。就利用到了多核优势
45.
参考资料:
https://www.cnblogs.com/linhaifeng/articles/6817679.html
https://www.cnblogs.com/linhaifeng/articles/7428874.html