2022.4.20进程补充及线程相关理论、方法概念
作者:互联网
2022.4.20进程补充及线程相关理论、方法概念
- 消息队列
- IPC机制(进程间通信)
- 生产者消费者模型
- 线程理论(重要)
- 开设线程的两种方式
- 线程实现TCP服务端并发
- 线程join方法
- 线程间数据共享
- 线程对象属性及方法
- 守护线程
- GIL全局解释器锁
一、消息队列
ps:由于目前的知识储备还不够直接学习消息队列 所以先学习内置队列,以后我们会直接使用别人封装好的消息队列 实现各种数据传输
from multiprocessing import Queue
q = Queue(5) # 定义一个队列并设置长度为5
1.往队列放数据
q.put(111) # 朝队列中存放数据
q.put(222)
q.put(333)
print(q.full()) # False 判断队列是否满了
q.put(444)
q.put(555)
print(q.full()) # True 队列已满
此时再往队列放数据就会超出最大长度,原地阻塞等待队列出现空位
2.从队列取数据
print(q.get()) # 从队列拿数据
print(q.get())
print(q.empty()) # False 判断队列是否空了
print(q.get())
print(q.get())
print(q.get())
print(q.empty()) # True 队列已空
此时队列没有数据,继续获取进入阻塞状态,等待队列中给值
print(q.get_nowait()) # 队列里没有值直接报错
"""
full()
empty()
get_nowait()
上述方法能否在并发的场景下精准使用???
不能用!!!
之所以介绍队列是因为它可以支持进程间数据通信
"""
二、IPC机制(进程间通信)
1.主进程与子进程数据交互
2.两个子进程数据交互
本质:不同内存空间中的进程数据交互
from multiprocessing import Process, Queue
def producer(q):
q.put('666') # 往队列放数据
def consumer(q):
print(q.get()) # 取队列数据
if __name__ == '__main__':
q = Queue() # 设置队列
p1 = Process(target=producer, args=(q, ))
p2 = Process(target=consumer, args=(q,))
p1.start()
p2.start()
# q.put(123) # 主进程往队列中存放数据123
print('主进程')
# 结果:
主进程
666
这样不同进程都在操作和使用这个队列,实现进程间通信
三、生产者消费者模型
1、概念
生产者:负责生产/制作数据
消费者:负责消费/处理数据
"""
比如在爬虫领域中:
会先通过代码爬取网页数据(爬取网页的代码就可以称之为是生产者)
之后针对网页数据做筛选处理(处理网页的代码就可以称之为消费者)
"""
如果使用进程来演示
除了有至少两个进程之外 还需要一个媒介(消息队列)
以后遇到该模型需要考虑的问题其实就是供需平衡的问题
即生产力与消费力要均衡
2、代码演示
from multiprocessing import Process, Queue,JoinableQueue
import time
import random
def producer(name, food, q):
for i in range(5):
data = f'{name}生产了{food}{i}'
print(data)
time.sleep(random.randint(1,3)) # 模拟产生过程
q.put(data) # 将数据放入队列
def consumer(name,q):
while True:
food = q.get()
time.sleep(random.random())
print(f'{name}吃了{food}')
q.task_done() # JoinableQueue队列,每次去完数据必须给队列一个反馈
'''
问题:生产者p在生产完后就结束了,但是消费者c在取空了q之后,则一直处于死循环中且卡在q.get()这一步,进入阻塞态。
解决方式:
普通方法:无非是让生产者在生产完毕后,往队列中再发一个结束信号,这样消费者在接收到结束信号后就可以break出死循环
JoinableQueue 方法: 生产者生产的每个数据上都做一个标记,消费者每 q.get() 取一个值,都用 q.task_done() 标记一次,q.join()感知队列中的数据全部处理完毕,再最终结束
'''
if __name__ == '__main__':
# q = Queue(),这里不使用普通队列
q = JoinableQueue() # 使用JoinableQueue方法
p1 = Process(target=producer, args=('大厨jason', '韭菜炒蛋', q))
p2 = Process(target=producer, args=('老板kevin', '秘制小汉堡', q))
c1 = Process(target=consumer, args=('涛涛', q))
c2 = Process(target=consumer, args=('龙龙', q))
c1.daemon = True
c2.daemon = True
p1.start()
p2.start()
c1.start()
c2.start()
# 生产者生产完所有数据之后 往队列中添加结束的信号
p1.join()
p2.join()
"""队列中其实已经自己加了锁 所以多进程取值也不会冲突 并且取走了就没了"""
q.join() # 等待队列中数据全部被取出(一定要让生产者全部结束才能判断正确)
"""执行完上述的join方法表示消费者也已经消费完数据了,相当于设置一道坎判断队列是否被取完,取完程序结束,守护进程直接结束,不会停留在阻塞态"""
四、线程理论
1、什么是线程
进程:资源单位(在内存空间中申请一块内存)
线程:执行单位(在进程的内存中执行任务,资源从进程空间取)
eg:进程相当于车间,线程相当于车间里面的流水线
一个进程至少有一个线程!
2、为什么要有线程?
答:开设线程的消耗远远小于进程
开进程:申请内存空间--->拷贝代码
开线程:无需申请内存、拷贝代码,进程内多个线程数据共享
总之,开多进程浪费内存空间及资源,线程解决了这个问题
五、开设线程的两种方法
其实开进程和线程的方法几乎是一样的,只不过关键字不同
代码演示:
from threading import Thread
import time
def task(name):
print(f'{name}开始吃饭')
time.sleep(3)
print(f'{name}吃饱了')
# 创建线程无需在__main__下面编写 但是为了统一 还是习惯在子代码中写
t = Thread(target=task, args=('jason',))
t.start()
print('主线程')
# 结果(创建线程开销很小,几乎一瞬间就可以创建,因此运行很快):
jason开始吃饭
主线程
Jason吃饱了
# 用类做线程对象
class MyThread(Thread):
def __init__(self, username):
self.username = username
super().__init__()
def run(self):
print(f'{self.username}开始吃饭')
time.sleep(3)
print(f'{self.username}吃饱了')
t = MyThread('jason')
t.start()
print('主线程')
# 结果:
jason开始吃饭
主线程
Jason吃饱了
六、线程实现TCP服务端的并发
注意区分开设进程和线程的本质区别
import socket
from throading import Throad
# 这里不用封装函数,因为使用线程做交互
server = socket.socket()
server.bind(('127.0.0.1', 8080))
server.listen()
def talk(sock):
data = sock.recv(1024) # 接收信息
print(data.decode('utf8'))
sock.send(data.upper()) # 发送信息
while True:
sock, addr = server.accept()
# 使用循环,每来一个客户端就创建一个线程做数据交互
t = Throad(target=talk, args=(sock,))
t.start()
因此可以发现,使用线程实现并发更为好一点,因为效率高且节省资源
七、线程join方法
线程的join方法和进程相差无几,都是让子线程运行完毕之后再往下执行
from threading import Thread
import time
def task(name):
print(f'{name} is running')
time.sleep(3)
print(f'{name} is over')
t = Thread(target=task, args=('jason', ))
t.start()
t.join() # 主线程代码等待子线程代码运行完毕之后再往下执行
print('主线程')
"""
主线程为什么要等着子线程结束才会结束整个进程
因为主线程结束也就标志着整个进程的结束 要确保子线程运行过程中所需的各项资源
"""
八、同一进程内多个线程数据共享
from threading import Thread
money = 10000000000
def task():
global money
money = 1
t = Thread(target=task)
t.start()
t.join()
print(money)
思考:线程更改进程内数据,数据也会被更改
# 1
九、线程对象属性和方法
1、同一进程下多个线程的进程号一致
2、如何统计进程下活跃的线程数
active_aount()
3、获取线程的名字
1.current_throad().name
MainThread # 主线程
Thread-1、Thread-2 # 子线程
2.self.name # 类对象获取线程名
十、守护线程
from threading import Thread
import time
def task(name):
print(f'{name} is running')
time.sleep(3)
print(f'{name} is over')
t1 = Thread(target=task, args=('jason',))
t2 = Thread(target=task, args=('kevin',))
t1.daemon = True # 守护线程
t1.start()
t2.start()
print('主线程')
# 结果:
jason is running
kevin is running
主线程
kevin is over
jason is over
# 注意,在这里,这个守护进程其实是起不了作用的,因为这里有两个子线程,其中一个是守护线程,但是主线程必须等待所有子线程运行结束才会真正结束,因为可能会有一些需要使用的数据,所以这里守护线程是起不了作用的,除非两个都是守护线程
十一、GIL全局解释器锁
回顾:
python解释器的类别有很多:
Cpython Jpython Ppython
垃圾回收机制:
应用计数、标记清除、分代回收
ps:GIl为纯理论,不影响编程,只不过面试的时候可能会被问到
官方文档:
In CPython, the global interpreter lock, or GIL, is a mutex that prevents multiple native threads from executing Python bytecodes at once. This lock is necessary mainly
because CPython’s memory management is not thread-safe. (However, since the GIL exists, other features have grown to depend on the guarantees that it enforces.)
解释:
GIL只存在于CPython解释器中,不是python的特征
GIL是一把互斥锁用于阻止同一个进程下的多个线程同时执行
原因是因为CPython解释器中的垃圾回收机制不是线程安全的
强调:
GIL是加在CPython解释器上面的互斥锁,
同一个进程下的多个线程要想执行必须先抢GIL锁,所以同一个进程下多个线程肯定不能同时运行,即:无法利用多核优势
优劣势:
优势:保证数据安全,不会被垃圾回收机制回收
劣势:无法发挥多核优势,但是可以通过开设多进程弥补
实际应用:
多任务为IO密集型:多线程更有优势,消耗资源更少(多道技术:切换+保存状态)
多任务为计算密集型:可以使用多进程,CPU越多越好
标签:20,name,队列,进程,线程,print,import,2022.4 来源: https://www.cnblogs.com/williama/p/16171351.html