day04.20并发编程的线程理论
消息队列理论
先了解学习内置队列。学习消息队列实现各种数据之间的传输。
使用模块from multiprocessing import Queue实现消息队列。
队列:先进先出(使用频率很高)
堆栈:先进后出(特定常见下用)
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()) # 队列中没有值 继续获取则阻塞等待队列中给值 print(q.get_nowait()) # 队列中如果没有值 直接报错
IPC机制>>>:进程之间的数据通信
主进程与子进程数据交互and两个子进程数据交互>>>:本质区别在于:不同内存空间中的进程数据交互。
from multiprocessing import Process, Queue def producer(q): # print('子进程producer从队列中取值>>>:', q.get()) 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('主进程')
生产者消费者模型
生产者:负责生产/制作数据。
消费者:负责消费/处理数据。
如果使用进程来演示生产者消费者模型>>>:除了有至少两个进程之外,还需要一个媒介(消息队列)
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() # if food == None: # print('完蛋了 没得吃了 要饿死人了') # break time.sleep(random.random()) print(f'{name}吃了{food}') q.task_done() # 每次去完数据必须给队列一个反馈 if __name__ == '__main__': # q = Queue() q = 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.put(None) # 结束信号的个数要跟消费者个数一致才可以 # q.put(None) """队列中其实已经自己加了锁 所以多进程取值也不会冲突 并且取走了就没了""" q.join() # 等待队列中数据全部被取出(一定要让生产者全部结束才能判断正确) """执行完上述的join方法表示消费者也已经消费完数据了"""
线程理论
什么是线程
线程是程序的执行线路,相当于一条流水线,其包含了程序的具体执行步骤,如果我们把操作系统比喻为一个工厂,进程就是车间,线程就是流水线。流水线的工作需要电源,电源就相当于cpu。
进程是资源单位(进程仅仅是在内存中开辟一块空间(提供线程工作所需的资源)),线程是执行单位(线程真正被CPU执行,线程需要的资源跟所在进程的要)。
一个进程中至少有一个线程。
进程只是用来把资源集中到一起(进程只是一个资源单位,或者说资源集合),线程才是CPU的最小执行单位。
线程存在的原因
多线程的概念是,在一个进程中存在多个控制线程,多个控制线程共享该进程的地址空间。相当于一个车间有多条流水线,都共用一个车间的资源。
使用线程的原因:为了提高效率,多线程可以使CPU在一个进程内进行切换,从而提高CPU占用率。
创建一个进程就是就相当于创建一个车间(申请空间,在空间至少一条流水线),而创建线程,就是在一个车间内造一条流水线,无需申请空间,所以创建开销小。
一个进程内可以开设多个线程,并且无需申请内存空间、拷贝代码;一个进程内的多个线程数据是共享的;开设线程的消耗远远小于进程。
线程与进程的关系
不同的进程之间直接是竞争关系,是不同的程序员写的程序运行的;而同一个进程的线程是合作关系,是同一个程序员写的程序。
进程中包含了运行该程序需要的所有资源。每个进程一旦被创建,就默认开启了一条线程,称之为主线程,一个进程可以包含多个线程,进程包含线程,而线程依赖进程。
区别:进程对于操作系统的资源耗费非常高,而线程相反非常低(比进程低10~100倍)。
在同一个进程,多个线程之间资源共享。
开启线程的两种方式
创建线程并不要求必须在__main__下面编写子程序,但是为了统一标准,还是习惯把写在__main__的子代码当中。
产生线程也是用模块来实现>>>:from threading import Thread。
-
方式一:实例化Thread类,产生一个对象
from threading import Thread def task(): print('task run') t1=Thread(target=task) t1.start() print('over')
-
方式二:继承Thread类,覆盖run方法
from threading import Thread class MyThread(Thread): def run(self): print('子线程running') MyThread().start() print('over 2')
用线程理论来实现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.decode('utf8')) sock.send(data.upper()) while True: sock, addr = server.accept() # 每类一个客户端就创建一个线程做数据交互 t = Thread(target=talk, args=(sock,)) t.start()
线程的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)
线程的相关方法
统计进程下活跃的线程数:active_count() # 注意主线程也算!!!
获取线程的名字:current_thread().name self.name
Thread实例对象的方法 # isAlive(): 返回线程是否活动的。 # getName(): 返回线程名。 # setName(): 设置线程名。 threading模块提供的一些方法: # threading.currentThread(): 返回当前的线程变量。 # threading.enumerate(): 返回一个包含正在运行的线程的list。正在运行指线程启动后、结束前,不包括启动前和终止后的线程。 # threading.activeCount(): 返回正在运行的线程数量,与len(threading.enumerate())有相同的结果。
守护线程
无论是进程还是线程,都遵循:守护XXX会等待主XXX运行完毕后被销毁。
需要强调的是:运行完毕并非终止运行。
对主进程来说:运行完毕就是指主进程代码运行完毕
对与主线程来说:运行完毕就是指主线程所在的进程内所有非守护线程统统运行完毕,主线程才算运行完毕。
详细解释:
1.主进程在其代码结束后就已经算运行完毕了(守护进程在此时就被回收),然后主进程会一直等待非守护的子进程都运行完毕后回收子进程的资源(否则会产生僵尸进程),才会结束。
2.主线程在其他非守护线程运行完毕后才算运行完毕(守护线程在此时就被回收)。因为主线程的结束意味着进程的结束,进程整体的资源都将被回收,而进程必须保证非守护线程运行完毕后才能结束。
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全局解释器锁
GIL是Global Interpreter Lock的缩写,全局解释器锁,是加在解释器上的互斥锁。
需要申明一点的是:
GIL并不是Python的特性,它是在实现Python解释器(Cpython)时引入的概念。python中一段代码可以通过Cpython,PyPy,Psyco等不同的Python执行环境来执行。
然而因为Cpython是大部分环境下默认的Python执行环境。所以在很多人的概念里Cpython就是Python。
所以需要明确的一点是,GIL仅存在于Cpython中,这不是Python这门语言的缺陷,而是Cpython解释器的问题。
GIL是一把互斥锁用于阻止同一个进程下的多个线程同时执行。原因是因为CPython解释器中的垃圾回收机制不是线程安全的。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?