day04.21并发编程02
多线程优势的代码验证
单个CPU:
多个IO密集型任务>>>:
多进程:浪费资源,无法利用多个CPU
多线程:节省资源,切换+保存状态
多个计算密集型任务>>>:
多进程:耗时更长,创建进程的消耗+切换消耗
多线程:耗时较短,切换消耗
多个CPU:
多个IO密集型任务>>>:
多进程:浪费资源,多个CPU无用武之地
多线程:节省资源,切换+保存状态
多个计算密集型任务>>>:
多进程:利用多核,速度更快
多线程:速度较慢
# 计算密集型 from threading import Thread from multiprocessing import Process import os import time def work(): res = 1 for i in range(1, 10000): res *= i if __name__ == '__main__': print(os.cpu_count()) # 12 查看当前计算机CPU个数 start_time = time.time() p_list = [] for i in range(12): p = Process(target=work) p.start() p_list.append(p) for p in p_list: p.join() t_list = [] for i in range(12): t = Thread(target=work) t.start() t_list.append(t) for t in t_list: t.join() print('总耗时:%s' % (time.time() - start_time))
# I/O密集型 from threading import Thread from multiprocessing import Process import os import time def work(): time.sleep(2) # 模拟纯IO操作 if __name__ == '__main__': start_time = time.time() # t_list = [] # for i in range(100): # t = Thread(target=work) # t.start() # for t in t_list: # t.join() p_list = [] for i in range(100): p = Process(target=work) p.start() for p in p_list: p.join() print('总耗时:%s' % (time.time() - start_time))
死锁现象
产生死锁的根本原因是两个或者两个以上线程在执行过程中,因争抢资源而产生相互等待的一种现象。在申请锁的时候发生了交叉闭环申请。
死锁产生的条件:
1.互斥。共享资源同时只能被一个线程访问。
2.占有且等待。线程T1在取得共享资源A的时候,请求等待资源B的时候并不释放资源A。
3.不可抢占。其他线程不能强行抢占线程的资源。
4.循环等待条件。线程T1在持有资源A1,同时在请求等待获取资源B,线程T2在持有资源B,然后在请求等待线程T1的持有资源,形成了交叉闭环申请。
from threading import Thread, Lock import time mutexA = Lock() mutexB = Lock() class MyThread(Thread): def run(self): self.f1() self.f2() def f1(self): mutexA.acquire() print(f'{self.name}抢到了A锁') mutexB.acquire() print(f'{self.name}抢到了B锁') mutexB.release() mutexA.release() def f2(self): mutexB.acquire() print(f'{self.name}抢到了B锁') time.sleep(2) mutexA.acquire() print(f'{self.name}抢到了A锁') mutexA.release() mutexB.release() for i in range(20): t = MyThread() t.start()
信号量(了解)
信号量在不同的知识体系中,展示出来的功能是不一样的。
在并发编程中信号量意思是多把互斥锁。在django框架中信号量意思是达到某个条件自动触发特定功能。
信号量就相当于多个互斥锁的功能。
eg:
from threading import Thread, Semaphore import time import random sp = Semaphore(5) # 创建一个相当于五个互斥锁的功能 def task(name): sp.acquire() # 抢锁 print('%s正在蹲坑' % name) time.sleep(random.randint(1, 5)) sp.release() # 放锁 for i in range(1, 31): t = Thread(target=task, args=('伞兵%s号' % i, )) t.start()
event事件(了解)
同进程的一样。线程的一个关键特性是每个线程都是独立运行且状态不可预测。如果程序中的其他线程需要通过判断某个线程的状态来确定自己下一步的操作,这时线程同步问题就会变得非常棘手。为了解决这些问题,我们需要使用threading库中的Event对象。 对象包含一个可由线程设置的信号标志,它允许线程等待某些事件的发生。
即:子线程的运行可以由其他子线程决定。
from threading import Thread, Event import time event = Event() # 类似于造了一个红绿灯 def light(): print('红灯亮着的 所有人都不能动') time.sleep(3) print('绿灯亮了 油门踩到底 给我冲!!!') event.set() def car(name): print('%s正在等红灯' % name) event.wait() print('%s加油门 飙车了' % name) t = Thread(target=light) t.start() for i in range(20): t = Thread(target=car, args=('熊猫PRO%s' % i,)) t.start()
进程池与线程池
进程池与线程池本质上就是一个存储进程或线程的列表。如果是I/O密集型任务,使用线程池,如果是计算密集型任务,则使用进程池。
在很多情况下需要控制进程或者线程的数量在一个合理的范围,这样可以在保证计算机硬件安全的情况下提升程序的运行效率。例如CPU程序中,一个客户端对应一个线程,虽然线程开销小,但是不能无限开,否则会耗尽系统资源,所以要控制线程数量。进程池/线程池不仅帮我们控制进程/线程的数量,还可以帮我们完成进程/线程的创建,销毁,以及任务的分配。
进程池和线程池其实降低了程序的运行效率,但是保证了硬件的安全!!!
from concurrent.futures import ThreadPoolExecutor from threading import active_count,current_thread import os,time # 创建线程池 指定最大线程数为5 如果不指定 默认为CPU核心数 * 5 pool = ThreadPoolExecutor(5)# 不会立即开启子线程,上面的代码执行之后就会立刻创建五个等待工作的线程 print(active_count()) def task(): print("%s running.." % current_thread().name) time.sleep(1) #提交任务到线程池 for i in range(10): pool.submit(task)
from concurrent.futures import ProcessPoolExecutor,ThreadPoolExecutor from threading import active_count,current_thread import os,time # 创建进程池 最大进程数为5 默认为cpu个数*5 pool = ProcessPoolExecutor(5)# 不会立即开启子进程 # time.sleep(10) def task(): print("%s running.." % os.getpid()) time.sleep(1) if __name__ == '__main__': # #提交任务到进程池 for i in range(10): pool.submit(task) # 第一次提交任务时会创建进程 ,后续再提交任务,直接交给以及存在的进程来完成,如果没有空闲进程就等待
协程
进程:资源单位
线程:执行单位
协程:单线程下实现并发
单线程下,不可避免会出现IO操作,但是如果我们能在自己的程序中(即用户程序级别,而非操作系统级别)控制单线程下的多个任务能在一个任务遇到IO阻塞就切换去执行另一个任务,这样就保证了该线程能够最大限度的处于就绪态,也就是随时可以被CPU执行的状态,相当于我们在用户程序级别将自己的IO操作最大程度的隐藏起来,从而可以迷惑操作系统:该线程好像一直在计算,IO较少,从而更多的将CPU执行权限分配给我们的线程。
协程就是单线程下的并发,又称微线程。协程是一种用户态的轻量级线程,即协程是由用户程序自己控制调度的。
对比操作系统控制线程的切换,用户在单线程内控制协程的切换:
优点:1.协程的切换开销更小,属于程序级别的切换,操作系统完全感知不到,因此更加轻量级
2.单线程内就可以实现并发的效果,最大限度利用CPU
缺点:1.协程的本质是单线程下,无法利用多核,可以是一个程序开启多个线程,每个进程内开启多个线程,每个线程内开启协程
2.协程指的是单个线程,因而一旦协程出现阻塞,将会阻塞整个线程
协程特点:
1.必须在只有一个单线程里实现并发
2.修改共享数据不需要加锁
3.用户程序里自己保存多个控制流的上下线
4.附加:一个协程遇到IO操作自动切换到其他协程
from gevent import monkey;monkey.patch_all() # 固定编写 用于检测所有的IO操作 from gevent import spawn import time def play(name): print('%s play 1' % name) time.sleep(5) print('%s play 2' % name) def eat(name): print('%s eat 1' % name) time.sleep(3) print('%s eat 2' % name) start_time = time.time() g1 = spawn(play, 'jason') g2 = spawn(eat, 'jason') g1.join() # 等待检测任务执行完毕 g2.join() # 等待检测任务执行完毕 print('总耗时:', time.time() - start_time) # 正常串行肯定是8s+ # 5.00609827041626 代码控制切换
基于协程实现的TCP服务端的并发
from gevent import monkey;monkey.patch_all() from gevent import spawn import socket def communication(sock): while True: data = sock.recv(1024) # IO操作 print(data.decode('utf8')) sock.send(data.upper()) def get_server(): server = socket.socket() server.bind(('127.0.0.1', 8080)) server.listen(5) while True: sock, addr = server.accept() # IO操作 spawn(communication, sock) g1 = spawn(get_server) g1.join()
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?