线程的基础知识
概要
-
消息队列
-
IPC(inter-producer-consumer)机制(进程间通信)
-
生产者消费模型
-
线程理论(重要)
-
开设线程的两种方式
-
线程实现TCP(Transmission Control Protocol)服务端并发
-
线程join方法
-
线程间数据共享
-
守护线程
-
GIL(Global-interpreter-lock)全局解释器锁
内容
1.消息队列
由于目前的知识储备还不够直接学习消息队列 所以先学习内置中的队列
消息队列可以实现进程间通信(本地、网络),并且消息队列还起到了保存数据的功能(队列中的数据如果不被取走,会一直在队列中)
消息队列将生产者与消费者解耦合!!! 解耦合:就是把程序中互相不相关或有限相关的模块分割开来,把不同模块互相之间的关系用接口进行准确定义,解耦前,两个模块之间共享所有信息
生产者只需要将数据放入队列中即可 无需考虑是否有人消费
消费者之需要将数据从队列中取出 无需考虑是否有生产者生产
队列:先进先出(使用频率很高 因为现实生活中大部分都是使用的是队列)
队栈:先进后出(只在一些特定场景下会使用到)
# 以后我们会直接使用别人封装好的消息队列 实现各种数据传输
from multiprocessing import Queue
q = Queue(5) # 自定义队列的长度
q.put(111)
q.put(222)
q.put(333)
print(q.full()) # False 判断队列是否满了
q.put(444)
q.put(555)
print(q.full()) # True
# q.put(666) # 超出最大长度 原地阻塞等待队列中出现空位
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() 如果队列中没有值 直接保错
上述方法能否在并发的场景下精准使用?
答案是不能 因为我们可以往极限方向去想 就是在一个进程中
1.我在取的时候 已经取完了然后使用empty() 然后在那瞬间put这边又要来一个值 这里通empty()出来的结果还算是正确的结果吗
2.我在放在时候 已经全部放完后使用full(),然后在那瞬间get()这边又取走了一个值 这里通过full()出来的结果还算是正确的结果吗
综上:不能用 之所以还介绍队列的目的是因为它还支持进程间数据通信 我们需要学习进程间数据通信
"""
2.IPC(inter-producer-consumer)机制(进程间通信)
1.主进程与子进程数据交互
2.两个子进程数据交互
本质:不同内存空间中的进程数据交互
from multiprocessing import Process,Queue
def producer(q):
q.put('子进程producer从队列中添加值')
def consumer(q):
print('子进程consumer从队列中取值>>>>:',q.get())
if __name__ == '__main__':
q = Queue()
p = Process(target = producer,args =(q,))
p1 = Process(target = consumer,args=(q,))
p.start()
p1.start()
# q.put(123) # 主进程往队列中存放数据123
print('主进程')
运行之后产生的结果是:
主进程
子进程consumer从队列中取值>>>: 子进程producer往队列中添加值
3.生产者消费者模型
# 生产者 负责生产/制作数据
# 消费者 负责消费/处理数据
比如在爬虫领域中
会先通过代码爬取网页数据 (爬取网页的代码就可以称之为是生产者)
之后针对网页数据做筛选处理(处理网页的代码就可以称之为消费者)
如果使用进程来掩饰
除了有至少两个进程之外 还需要一个媒介(消息队列)
以后遇到该模型需要考虑的问题其实就是供需平衡的问题
生产力与消费力要均衡
from multiprocessing import Process, Queue, JoinableQueue
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() # 每次去完成数据必须给队列一个反馈
if __name__ == '__main__':
q = JoinableQueue()
p1 = Process(target=producer, args=('大厨jason', '沙县小吃', q))
p2 = Process(target=producer, args=('大厨kevin', '韭菜炒蛋', q))
c1 = Process(target=consumer,args=('tony', q))
c2 = Process(target=consumer,args=('jerry', q))
c1.daemon = True
c2.daemon = True
p1.start()
p2.start()
c1.start()
c2.start()
# 生产者生产完所有数据之后 往队列中添加结束的信号
p1.join()
p2.join()
# q.put(None) # 结束信号的个数要跟消费者个数要一致才可以
# q.put(None)
# 队列中其实已经自己加了锁 所有多进程取值也不会冲突 并且取走了就没了
q.join() # 等待队列中数据全部被取出(一定要让生产这全部结束才能判断正确)
"""
执行完上述的join方法表示消费者也已经消费完数据了"""
4.线程理论
# 什么是线程
进程:相当于一个资源单位
线程:进程中的执行单位
进程相当于车间(一个个空间),线程相当于车间里面的流水线(真正干活的)
"""一个进程中至少有一个线程
进程仅仅是在内存中开辟一块空间(提供线程工作所需的资源)
线程真正被CPU执行 线程需要的资源跟所在进程的去拿就可以"""
# 为什么要有线程
开设线程的消耗远远小于进程
开进程过程:1.申请内存空间 2.拷贝代码
开线程:一个进程内可以开设多个线程 无需申请内存空间 拷贝代码
一个进程内的多个线程数据是共享的 都是在同一个线程内 大家的资源都是共享的
"""开发一个文本编辑器
获取用户输入并实时展示到屏幕上
并实时保存到硬盘中
多种功能应该开设多线程而不是多进程"""
5.开设线程的两种方式
'进程与线程的代码实操几乎是一样的 主要是因为线程与进程的理论是同一个人写的 所以两者的用法几乎一样'
from threading import Thread
import time
def task(name):
print(f'{name} is running')
time.sleep(3)
print(f'{name} is over')
# 创建线程无需在__main__下面编写 但是为了统一 还是习惯在子代码中写
t = Thread(target=task,args=('jason',))
t.start() # 创建线程的开销极小 几乎是一瞬间就可以创建
print('主线程')
class MyThread(Thread):
def __init__(self,username):
super().__init__()
self.username = username
def run(self):
print(f'{self.username} jason is running')
time.sleep(3)
print(f'{self.username} is over')
t = MyThread('jasonNB')
t.start()
print('主线程')
6.线程实现TCP服务端的并发
需要比对开设进程与线程的本质区别
import socket
from threading import Thread
server = socket.socket()
server.bind(('127.0.0.1',8080))
server.listen()
def talk(sock): # 封装成一个函数为了实现多个服务实现并发的效果
while True:
data = sock.recv(1024)
print(data.dacode('utf8'))
sock.send(data.upper())
while True:
sock,addr = server.accept()
# 每类一个客户端就创建一个线程做数据交互
t = Thread(target=talk,args=(sock,))
t.start()
7.线下join方法
from threading import Thrad
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('主线程')
"主线程为什么要等着子线程结束后才会执行整个进程
因为主线程结束也就标志着整个进程的结束 为要确保子线程运行过程中所需要的各项资源 所以主线程不能先结束
"
8.同一个进程内的多个线程数据共享
from threading import Thread
money = 10000
def task():
global money
money = 1
t = Thread(target=task)
t.start()
t.join()
print(money) # 1
9.线程对象属性与方法
1.验证一个进程下的多个线程是否真的处于一个进程
验证确实如此
2.统计进程下活跃的线程数
active_count() # 注意主线程也算!!
3.获取线程的名字
1.current_thread().name
MainThread 主线程
Thread-1、Thread-2 子线程
2.self.name
10.守护线程
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('主线程')
11.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.)
1.回顾
python解释器的类别有很多(根据解释器是由什么编写的)
C-python、J-python、P-python(pyhton)
垃圾回收机制
应用计数、标记清除、分代回收
"""
GIL只存在于Cpython解释器中 不是python的特征
GIL是一把互斥锁用于阻止同一个进程下的多个线程同时执行
原因是因为Cpython解释器中的垃圾回收机制不是线程安全的
反向验证GIL的存在 如果不存在会产生垃圾回收机制与正常线程之间数据错乱
GIL是加在Cpython解释器上面的互斥锁
同一个进程下的多个线程要想执行必须先前抢GIL锁 所以同一个进程下多个线程肯定不能同时运行 即无法利用多核优势
强调:同一个进程下的多个线程不能同时执行即不能利用多核优势
很多不懂python的程序员会喷python是垃圾 速度太慢 有多核都不能用
反怼:虽然用一个进程下的多个线程不能利用多核优势 但是还可以开设多进程
再次强调:python的多线程就是垃圾
反怼:要结合实际情况而定
如果多个任务都是IO密集型 那么多线程更有优势(消耗的资源更少) 多道技术:切换+保存状态
如果多个任务都是计算密集型 那么多线程确实没有优势 但是可以用多进程来弥补
cpu 越多越好
以后用python就可以多进程下面开设多线程从而达到效率最大化
"""
ps:1.所有的解释型语言都无法做到同一个进程下多个线程利用多核优势
2.GIL在实际编程中其实不用考虑

浙公网安备 33010602011771号