僵尸进程与孤儿进程
引子:进程与进程之间是相互独立的,但是为什么主进程还要等子进程结束才会结束呢?
所有子进程在运行结束后都会编程僵尸进程,还保留着pid及占用cpu时间等信息。
这些信息会把被主进程主动回收(两种回收可能:主进程调用join的等待子进程结束回收,或主进程正常结束调用wait回收)
孤儿进程
子进程还没结束,主进程先死了,意味着子进程的pid等信息不能被即使回收。
要等到init回收机制来帮忙回收,会稍微延长时间即主进程已经结束 子进程仍然运行
僵尸进程
进程代码运行结束之后并没有直接结束而是需要等待挥手子进程资源才能结束
主进程的存在是为了等待子进程结束之后的回收子进程的资源,若主进程结束,子进程没有了主进程,会造成很大程度的资源浪费,尤其是进程号不会被主进程自动回收
守护进程
import time
from multiprocessing import Process
def run(n):
print(f'总管: {n} 开始')
time.sleep(3)
print(f'总管: {n} 结束')
if __name__ == '__main__':
p = Process(target=run, args=('junjie',))
p.daemon = True # 跟随主进程,主进程结束,守护进程结束
p.start()
print('皇帝俊杰666')
# 输出 皇帝俊杰666
但是,守护进程结束主进程可以不结束。
import time
from multiprocessing import Process
def run(n):
print(f'总管: {n} 开始')
time.sleep(3)
print(f'总管: {n} 结束')
if __name__ == '__main__':
p = Process(target=run, args=('junjie',))
p.daemon = True
p.start()
print('皇帝俊杰666')
time.sleep(4)
print('dddd')
"""
皇帝俊杰666
总管: junjie 开始
总管: junjie 结束
dddd
"""
上述情况反应了,守护进程结束但主进程可以不用结束,但主进程结束,守护进程一定结束
互斥锁(⭐️⭐️⭐️⭐️⭐️)
在讲互斥锁之前,先引入一个概念
相信大家都有过12306购票的经历,买票时查看到余票还有假设五张,但是点击此票并且要购买时,有时会提示暂无此票,这是怎么回事呢?
此时你在看票的过程,其他人也同时在浏览,并且其他人可能会进行购买的操作,此时你点击进入程序需要购买的同时,终端会经历两个操作,查看剩余车票+销售车票。
模拟抢票代码
a.txt
{"ticket_num": 0}
import json
import random
import time
# 查票
def search():
with open(r'a.txt', 'r', encoding='utf8') as f:
data_dict = json.load(f)
ticket_num = data_dict.get('ticket_num')
print(f'余票:{ticket_num}')
# 买票
def buy():
# 查票
with open(r'a.txt', 'r', encoding='utf8') as f:
data_dict = json.load(f)
ticket_num = data_dict.get('ticket_num')
# 模拟购票是延迟,加入随机模块,每个终端的延迟不一致
time.sleep(random.random())
# 判断换是否有票
if ticket_num > 0:
# 将余票减一
data_dict['ticket_num'] -= 1
# 重新写入数据库
with open(r'a.txt', 'w', encoding='utf8') as f:
json.dump(data_dict,f)
print('购买成功')
else:
print('没票了')
def run():
search()
buy()
run()
模拟多人抢票
import json
import random
import time
from multiprocessing import Process
# 查票
def search(name):
with open(r'a.txt', 'r', encoding='utf8') as f:
data_dict = json.load(f)
ticket_num = data_dict.get('ticket_num')
print(f'{name}查询余票:{ticket_num}')
# 买票
def buy(name):
# 查票
with open(r'a.txt', 'r', encoding='utf8') as f:
data_dict = json.load(f)
ticket_num = data_dict.get('ticket_num')
# 模拟购票是延迟,加入随机模块,每个终端的延迟不一致
time.sleep(random.random())
# 判断换是否有票
if ticket_num > 0:
# 将余票减一
data_dict['ticket_num'] -= 1
# 重新写入数据库
with open(r'a.txt', 'w', encoding='utf8') as f:
json.dump(data_dict, f)
print(f'用户:{name}购买成功')
else:
print('没票了')
def run(name):
search(name)
buy(name)
if __name__ == '__main__':
for i in range(1, 11):
p = Process(target=run, args=(f'用户:{i}',))
p.start()
如图所示,这是一个非常典型的情况,并发情况下操作同一份数据,极其容易造成数据错乱。
解决:将并发编程串行,虽然将降低了效率但是提升了数据的安全
那么如何将并发编程变成串行的效果呢?
互斥锁
着重介绍行锁,表锁。
行锁:假设有一张纸,现在有10个人都需要使用这张的第一行做数据的增删改查,此时A拿到了张纸,那么剩余的9个人都必须等待A操作完这一行再公平竞争才能继续对这张纸进行操作,类推。
表锁:同样假设有一张纸,现在有10个人都需要使用这张的第一行做数据的增删改查,此时A拿到了张纸,那其他9个人只能等A使用完并且将纸放回至原来的位子才能公平竞争。
# 使用锁的注意事项
1.一定要在需要的地方加锁,不可所以添加
2.不可轻易使用锁(可能造成死锁现象)
# 注:在以后的编程生涯,几乎不会接触到自己操作锁的情况
互斥锁代码
import json
from multiprocessing import Process, Lock
import time
import random
# 查票
def search(name):
with open(r'a.txt', 'r', encoding='utf8') as f:
data_dict = json.load(f)
ticket_num = data_dict.get('ticket_num')
print('%s查询余票:%s' % (name, ticket_num))
# 买票
def buy(name):
# 先查票
with open(r'a.txt', 'r', encoding='utf8') as f:
data_dict = json.load(f)
ticket_num = data_dict.get('ticket_num')
# 模拟一个延迟(选座) 随机模块
time.sleep(random.random())
# 判断是否有票
if ticket_num > 0:
# 买票 将余票减一
data_dict['ticket_num'] -= 1
# 重新写入数据库
with open(r'a.txt', 'w', encoding='utf8') as f:
json.dump(data_dict, f)
print('%s: 购买成功' % name)
else:
print('不好意思 没有票了!!!')
def run(name, mutex):
search(name)
mutex.acquire() # 抢锁/上锁 (一次只有一个人可以通过)
buy(name)
mutex.release() # 释放锁
if __name__ == '__main__':
# 创建互斥锁,赋值(串行)
mutex = Lock()
# 循环10次 模拟抢票
for i in range(1, 11):
# 创建子进程
p = Process(target=run, args=('用户%s' % i, mutex))
# 执行子进程
p.start()
程序在查票时公平,抢票加入random随机模块,异步创建子进程,用户1-10,谁的时间最短先抢到锁,才能执行购票,那么其他用户只能等待已经抢到锁的用户将锁释放才能继续抢锁。
消息队列
队列是共用的
队列的使用就可以打破进程间默认无法通信的情况
有了队列,进程之间就有了可以公共存放数据的地方
# 若队列中是存放数据,产生别名,消息队列
实现进程间的数据交互(ipc机制)
利用队列实现进程与进程之间的交互
生产者消费者(⭐️⭐️⭐️⭐️⭐️)
joinableQueue
可翻译:为可join的队列
该队列相比普通的Queue的区别在于该对列额外增加的了join函数
join函数的作用:
该函数为阻塞函数,会阻塞直到等待队列中所有数据都被处理完毕。
q = JoinableQueue()
q.put('junjie')
print(q.get())
q.join() # 阻塞 等待队列中所有数据都被处理完毕
print("over")
执行以上函数,将导致进程无法结束,注释掉join调用就正常,发现join的确有阻塞的效果,
但是队列中一共就一个数据,明明已经调用get取出了,为什么join依然阻塞呢?
这是因为get仅仅是取出数据,而join是等待数据处理完毕,也就是说:
取出数据还不算完,你处理完以后必须告知队列处理完毕,通过task_done
q = JoinableQueue()
q.put('junjie')
print(q.get())
q.task_done() # 数据处理完毕
q.join() # 阻塞 等待队列中所有数据都被处理完毕
print("over")
# 输出:
junjie
over
需要注意的时,task_done的调用次数必须大于等于队列中的数据个数,join才能正常结束阻塞
q = JoinableQueue()
q.put(1)
q.put(2)
q.get()
q.get()
q.task_done() # 数据处理完毕
q.task_done() # 数据处理完毕
q.join() # 阻塞 等待队列中所有数据都被处理完毕
print("over")
所以 : 主进程可以明确知道队列中的数据何时被处理完毕
以下该模型需要解决供需不平衡现象
生产者
负责产生数据的 (相当于做包子的)
消费者
负责处理数据 (相当于吃包子的)
JoinableQueue 代码块
from multiprocessing import Queue, Process,JoinableQueue
import time
import random
def put(name, food, q):
for i in range(2):
print(f'{name} 生产了 {food}')
q.put(food)
time.sleep(random.random())
def take(name, q):
while True:
data = q.get()
print(f'{name} 吃了 {data}')
q.task_done() # 监测队列中剩余数据
if __name__ == '__main__':
q = JoinableQueue()
# 生产者
p1 = Process(target=put, args=('大厨俊杰', '玛莎拉', q,))
p2 = Process(target=put, args=('印度路路', '飞饼', q,))
p3 = Process(target=put, args=('泰国嘉明', '榴莲', q))
# 消费者
c1 = Process(target=take, args=('阿飞', q))
# 告诉操作系统开设子进程
p1.start()
p2.start()
p3.start()
c1.daemon = True
c1.start()
p1.join()
p2.join()
p3.join()
q.join() # 等待队列中所有数据被取干净
print('主')
线程理论
什么是线程?
进程其实是一个资源单位 真正被cpu执行的其实是进程里面的线程
程序运行之后称之为进程,起了一个进程之后,每一个进程中都有至少有一个线程,cpu真正执行的是线程,进程仅仅是提供给线程各项资源
其实可以将进程看做工厂,线程类似于工厂中的流水线
并且进程间数据默认是隔离的,但是同一个进程内的多个线程数据是共享的**
开设进程需要做的操作:
-
重新申请一块内存空间
-
将所需的资源全部导入
开设线程需要的操作:
上述两个步骤都不需要,所以开设线程消耗的资源远比开设进程少!
开设进程代码
from threading import Thread
import time
def test(name):
print(f'{name} is 开始')
time.sleep(3)
print(f'{name} is 结束')
t = Thread(target=test, args=('俊杰',))
t.start()
print('主')
# 输出
俊杰 is 开始
主
俊杰 is 结束
开设线程没有多少准备工作,几乎一瞬间,不需要内存空间,不需要准备代码
开设线程代码第一种方式
from multiprocessing import Process
import time
def test(name):
print(f'{name} is 开始')
time.sleep(3)
print(f'{name} is 结束')
p = Process(target=test, args=('jason',))
p.start()
print('主进程')
# 输出
主进程
jason is 开始
jason is 结束
现在可能大家还没怎么分清楚进程和线程,这样想,无论开启进程或者开启线程,都相当于多增加一个服务员, 虽然两者都是服务员,你可以将进程看做一个团队,线程可以看做个人。
开设线程代码第二种方式
from threading import Thread
import time
class MyClass(Thread):
def __init__(self, name):
super().__init__()
self.name = name
def run(self):
print(f'{self.name} is 开始')
time.sleep(3)
print(f'{self.name} is 结束')
obj = MyClass('junjie')
obj.start()
print('主线程')
线程对象的其他方法
join
from threading import Thread
import time
def test(name):
print(f'{name} is 开始')
time.sleep(3)
print(f'{name} is 结束')
t = Thread(target=test, args=('俊杰',))
t.start()
t.join()
print('主')
# 输出
俊杰 is 开始
俊杰 is 结束
主
pid,验证同一个进程内可以开设多个线程
from threading import Thread
import time,os
def test(name):
print(os.getpid()) # 查看当前进程号
print(f'{name} is 开始')
time.sleep(3)
print(f'{name} is 结束')
t = Thread(target=test, args=('俊杰',))
t.start()
print(os.getpid())
print('主')
# 输出
16871
俊杰 is 开始
16871
主
俊杰 is 结束
active_count统计当前正在活跃的线程数
from threading import Thread, active_count
import time, os
def test(name):
print(os.getpid())
print(f'{name} is 开始')
time.sleep(3)
print(f'{name} is 结束')
t = Thread(target=test, args=('俊杰',))
t.start()
print(os.getpid())
print(active_count())
print('主')
# 输出
16881
16881
2
主
俊杰 is 开始
俊杰 is 结束
还有一种情况
from threading import Thread, active_count
import time, os
def test(name):
print(os.getpid())
print(f'{name} is 开始')
time.sleep(3)
print(f'{name} is 结束')
t = Thread(target=test, args=('俊杰',))
t.start()
print(os.getpid())
t.join()
print(active_count())
print('主')
# 输出
16899
16899
俊杰 is 开始
俊杰 is 结束
1
主
current_threadhuo获取当前线程
from threading import Thread, active_count,current_thread
import time, os
def test(name):
print(current_thread().name)
print(f'{name} is 开始')
time.sleep(3)
print(f'{name} is 结束')
t = Thread(target=test, args=('俊杰',))
t.start()
print(current_thread().name)
print('主')
# 输出
Thread-1
MainThread
俊杰 is 开始
主
俊杰 is 结束
Daemon守护线程
from threading import Thread
import time
def test(name):
print(f'{name} is 开始')
time.sleep(3)
print(f'{name} is 结束')
t = Thread(target=test, args=('俊杰',))
t.daemon = True
t.start()
print('主')
# 输出 (线程启动速度过快)
俊杰 is 开始
主
接下来来一个迷惑性的案例,开动小脑筋吧 : )
from threading import Thread
from multiprocessing import Process
import time
def foo():
print(123)
time.sleep(1)
print("end123")
def bar():
print(456)
time.sleep(3)
print("end456")
if __name__ == '__main__':
t1=Thread(target=foo)
t2=Thread(target=bar)
t1.daemon=True
t1.start()
t2.start()
print("main-------")
# 输出
123
456
main-------
end123
end456
主进程一定要在非守护进程结束之后才能结束,主线程的结束意味着整个进程的结束
线程数据共享,前提在同一进程下
上面讲述过,进程就是给线程提供了资源即为资源单位,在同一个资源单位中多个线程的资源是共享的。
进程之间数据不能共享
from multiprocessing import Process
num = 'junjie'
def task():
global num
num = 'junjie666'
q = Process(target=task)
q.start()
q.join()
print(num)
# 输出
junjie
线程之间数据是共享的
from threading import Thread
num = 'junjie'
def task():
global num
num = 'junjie666'
q = Thread(target=task)
q.start()
q.join()
print(num)
线程互斥锁
多个进程或者线程,在修改同一份数据的同时,都有可能会造成数据的错乱
from threading import Thread
import time
num = 100
def test():
# 局部的变量修改全局变量为局部变量
global num
tmp = num
# 模拟延迟效果
time.sleep(1)
# 修改数值
tmp -= 1
num = tmp
t_list = []
# 同时开启100个线程
for i in range(100):
t = Thread(target=test)
t.start()
t_list.append(t)
# 确保所有的子线程全部结束
for t in t_list:
t.join()
print(num)
# 输出
99
想要数据不混乱,就需要将并发转换为串行
加锁或者消息队列
from threading import Thread, Lock
import time
num = 100
def test(mutex):
# 局部的变量修改全局变量为局部变量
global num
mutex.acquire() # 抢锁
tmp = num
# 模拟延迟效果
time.sleep(0.01)
# 修改数值
tmp -= 1
num = tmp
mutex.release() # 释放锁
t_list = []
mutex = Lock()
# 同时开启100个线程
for i in range(100):
t = Thread(target=test, args=(mutex,))
t.start()
t_list.append(t)
# 确保所有的子线程全部结束
for t in t_list:
t.join()
print(num)
# 输出
0
如何将TCP服务端实现并发形式
- 先不考虑进程或线程,将基础的服务端代码搭建
服务端
import socket
from threading import Thread
from multiprocessing import Process
server = socket.socket()
server.bind(('127.0.0.1', 8080))
while True:
sock, add = server.accept()
print(add)
while True: # 这里开始是与用户交互的位置,那么可不可以封装成函数?
try:
data = sock.recv(1024)
# 代码的健壮性校验
if len(data) == 0: break
print(data.decode('utf8'))
sock.send(data + b'gun')
except Exception as e:
print(e)
break
- 封装
服务端
import socket
from threading import Thread
from socket import SOL_SOCKET, SO_REUSEADDR # 针对于Mac报错 新增模块,解决端口被占用报错
server = socket.socket()
server.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) # 针对Mac,端口被占用报错
server.bind(('127.0.0.1', 8080))
server.listen(6)
def talk(sock):
while True:
try:
data = sock.recv(1024)
# 代码的健壮性校验
if len(data) == 0: break
print(data.decode('utf8'))
sock.send(data + b'gun')
except Exception as e:
print(e)
break
while True:
sock, addr = server.accept() # 链接
print(addr)
# 开设多进程或多线程
t = Thread(target=talk, args=(sock,))
t.start()
客户端
import socket
client = socket.socket()
client.connect(('127.0.0.1', 8080))
while True:
speak = input('说话>>>').strip()
client.send(speak.encode('utf8'))
data = client.recv(1024)
print(data.decode('utf8'))
至此,可以实现多个客户端同时连接服务端。