【死锁与递归锁】
死锁
1 # 当知道所得使用抢锁必须要释放锁,其实在操作锁的时候也极其容易产生死锁现象(程序阻塞) 2 # 互斥锁的死锁现象 3 from threading import Thread, Lock 4 import time 5 6 mutexA = Lock() 7 mutexB = Lock() 8 9 10 # 类只要加()多次,产生的肯定是不同的对象,如果想要实现多次加()得到的是相同的对象,使用单例模式 11 # 单例模式(Singleton Pattern) 是一种创建型设计模式,它确保一个类只有一个实例,并提供一个全局访问点来访问这个唯一实例。 12 13 14 class MyThead(Thread): 15 def run(self): 16 self.func1() 17 self.func2() 18 19 def func1(self): 20 mutexA.acquire() 21 print('%s 抢到A锁' % self.name) 22 mutexB.acquire() 23 print('%s 抢到B锁' % self.name) 24 mutexB.release() 25 mutexA.release() 26 27 def func2(self): 28 mutexB.acquire() 29 print('%s 抢到B锁了' % self.name) 30 time.sleep(2) 31 mutexA.acquire() 32 print('%s 抢到A锁了' % self.name) 33 mutexA.release() 34 mutexB.release() 35 36 37 if __name__ == '__main__': 38 for i in range(10): 39 t = MyThead() 40 t.start() 41 42 43 44 主线程同时创建了10子线程,10个子线程同时开始运行函数func1,当一个子线程抢到了A锁后, 45 其他的子线程就已经没资格运行A锁下面的代码了,所以该子线程可以不用抢就能拿到B锁释放B锁, 46 最后再释放A锁。该子线程放掉A锁后,其他的子线程会去抢A锁,但是现在情况来了, 47 该子线程运行完了函数func1后,继续要运行函数func2时,又要抢B锁,此时其他子线程还在抢A锁, 48 所以该子线程还是能顺利的抢到B锁,但这个时候睡了2s, 49 此时其他的子线程肯定已经有一个子线程抢到了A锁,该子线程睡醒了后,想抢A锁, 50 但此时A锁已经被其他子线程抢到了,而且这个抢到A锁的子线程此时也正要想抢B锁, 51 两个子线程都想抢对方手里的锁!!! 52 53 就卡死了!!!这就是死锁现象!!!
递归锁
1 ''' 2 递归锁的特点: 3 可以被连续的acquire和release 4 但是只能被第一个抢到这把锁执行上述操作 5 它的内部有一个计数器,每acquire一次计数器+1,每release一次计数器-1 6 只要计数不为0,那么其他人都无法抢到该锁 7 ''' 8 from threading import Thread, Lock, RLock 9 import time 10 11 mutexA = mutexB = RLock() 12 13 14 class MyThead(Thread): 15 def run(self): 16 self.func1() 17 self.func2() 18 19 def func1(self): 20 mutexA.acquire() 21 print('%s 抢到A锁' % self.name) 22 mutexB.acquire() 23 print('%s 抢到B锁' % self.name) 24 mutexB.release() 25 mutexA.release() 26 27 def func2(self): 28 mutexB.acquire() 29 print('%s 抢到B锁了' % self.name) 30 time.sleep(2) 31 mutexA.acquire() 32 print('%s 抢到A锁了' % self.name) 33 mutexA.release() 34 mutexB.release() 35 36 37 if __name__ == '__main__': 38 for i in range(10): 39 t = MyThead() 40 t.start()
。
。
【信号量】
信号量:信号量在不同的阶段可能对应着不同的技术点,在并发编程中信号量指的是锁
1 # 模拟抢厕所 2 import random 3 import time 4 from threading import Thread, Semaphore 5 6 # 信号量关键字:Semaphore 7 wc = Semaphore(5) # ()中的数字是信号量的初始值 8 9 10 def task(name): 11 wc.acquire() 12 print('%s 正在蹲坑' % name) 13 # time.sleep(3) # 如果蹲坑时间一样,发现5个信号量,那么5个人同时出来 14 time.sleep(random.randint(1, 5)) # 蹲坑时间不一样,发现5个信号量,进去和出来的数量不一样 15 wc.release() 16 17 18 if __name__ == '__main__': 19 for i in range(20): 20 t = Thread(target=task, args=('伞兵%s号' % i,)) 21 t.start()
。
。
【Event事件】
子进程之间可以彼此等待彼此,或者子线程之间可以彼此等待彼此
例如:子进程A运行到某一行代码位置后,发信号告诉子进程B开始运行
也就是说比如开了两个子进程,两个子进程不是同时运行的, 有一个子进程要依赖于另一个子进程给它发信号后,才能执行。 这就叫event事件
1 # 模拟等红绿灯 2 3 4 import time 5 from threading import Thread, Event 6 7 event = Event() # 创建一个事件对象,红绿灯 8 9 10 def light(): 11 print('灯红着呢') 12 time.sleep(3) 13 print('绿灯亮了') 14 # 告诉等待红灯的人可以走了 15 event.set() 16 17 18 def car(name): 19 print('%s 车正在等待红灯' % name) 20 event.wait() # 等待别人给你发消息 21 print('%s 车绿灯亮了,开动' % name) 22 23 24 if __name__ == '__main__': 25 t = Thread(target=light) 26 t.start() 27 28 # 模拟多辆车的场景 29 for i in range(5): 30 t = Thread(target=car, args=('第%s辆' % i,)) 31 t.start()
。
。
【线程Q】
1 ''' 2 线程Q: 3 同一个进程下多个线程数据是共享的 4 为什么还会去使用队列呢? 5 因为队列是:管道+锁 6 这样是为了保证数据的安全 7 ''' 8 9 import queue 10 11 # 后进先出q 12 q = queue.LifoQueue(3) # last in first out 13 q.put(1) 14 q.put(2) 15 q.put(3) 16 print(q.get()) # 3 17 18 # 优先级q ,可以放给队列中的数据设置优先级 19 q = queue.PriorityQueue(4) 20 q.put((10, '111')) 21 q.put((100, '222')) 22 q.put((0, '333')) 23 q.put((-5, '444')) 24 print(q.get()) # (-5, '444'), put()内放一个元组,第一个元素是优先级,数字越小优先级越高
。
。
【进程池,线程池】
无论是开设进程也好还是开设线程也好 是不是都需要消耗资源只不过开设线程的消耗比开设进程的稍微小一点而已
我们是不可能做到无限制的开设进程和线程的 因为计算机硬件的资源更不上!!!硬件的开发速度远远赶不上软件呐
我们的宗旨应该是在保证计算机硬件能够正常工作的情况下最大限度的利用它
因此产生了池:
什么是池?
池是用来保证计算机硬件安全的情况下最大限度的利用计算机,它降低了程序的运行效率但是保证了计算机硬件的安全 从而让我们写的程序能够正常运行
(线程池)
1 from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor 2 import time 3 4 pool = ThreadPoolExecutor(5) # 池子里面固定只有5个线程 5 # ()内可以传数字,不传的话默认会开设当前计算机cpu个数的五倍的线程 6 ''' 7 池子造出来以后,里面会固定存在五个线程 8 这五个线程不会出现重复创建和销毁的过程 9 ''' 10 11 12 def task(n): 13 print(n) 14 time.sleep(2) 15 return n # return方法,同步提交 16 17 18 ''' 19 任务的提交方式: 20 同步提交:提交任务以后,在原地等待任务的返回结果,期间不能做任何事情 21 异步提交:提交任务以后,不会等待任务的返回结果,继续往下执行 22 ''' 23 24 t_list = [] # 保证进程先起起来 25 # pool.submit(task, 1) # 朝池子中提交任务,异步提交(不等待任务的返回结果) 26 # print('主') 27 for i in range(20): 28 res = pool.submit(task, i) 29 # print(res.result()) # 程序由并发变成了串行,return 拿到的就是异步提交的任务的返回结果 30 t_list.append(res) # 等待线池中所有的任务执行完毕以后再继续往下执行 31 # pool.shutdown() # 关闭线程池/等待线程池中所有的任务运行完毕 32 for t in t_list: 33 print('>>>>', t.result()) # 拿到的结果是有序的
(进程池)
1 from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor 2 import time, os 3 4 pool = ProcessPoolExecutor(5) 5 # ()内可以传数字,不传的话默认会开设当前计算机cpu个数进程 6 ''' 7 池子造出来以后,里面会固定存在几个进程 8 这几个进程不会出现重复创建和销毁的过程 9 ''' 10 11 12 def task(n): 13 print(n, os.getpid()) 14 time.sleep(2) 15 return n 16 17 18 # 如何拿到返回结果:异步提交任务的返回结果,应该通过回调机制来获取 19 # 回调机制;相当于给每个异步任务绑定一个定时炸弹,一旦该任务有结果,立刻触发爆炸 20 def call_back(n): 21 print('call_back>>>>', n.result()) # 拿到返回结果 22 23 24 if __name__ == '__main__': 25 t_list = [] # 保证进程先起起来 26 for i in range(20): 27 res = pool.submit(task, i).add_done_callback(call_back) 28 # t_list.append(res) 29 # 等待线池中所有的任务执行完毕以后再继续往下执行 30 pool.shutdown() # 关闭线程池/等待线程池中所有的任务运行完毕 31 for t in t_list: 32 print('>>>>', t.result()) # 拿到的结果是有序的
。
。
【并发的本质】
1 并发的本质 2 基于单线程来实现并发 3 即只用一个主线程(很明显可利用的cpu只有一个)情况下实现并发 4 为此我们需要先回顾下并发的本质: 5 切换+保存状态 6 7 CPU正在运行一个任务 8 会在两种情况下切走去执行其他的任务(切换由操作系统强制控制) 9 一种情况是该任务发生了阻塞 10 另外一种情况是该任务计算的时间过长或有一个优先级更高的程序替代了它。 11 -------------------------------------------------------------------------------------- 12 yield关键字 13 14 yield可以保存状态 15 yield的状态保存与操作系统的保存线程状态很像,但是yield是代码级别控制的,更轻量级 16 2 send可以把一个函数的结果传给另外一个函数 17 以此实现单线程内程序之间的切换 18
。
。
【协程】
进程:资源单位 线程:执行单位 协程:这个概念完全是程序员自己意淫出来的 根本不存在单线程下实现并发 我们程序员自己再代码层面上检测我们所有的Io操作,一旦遇到IO了 我们在代码级别完成切换 这样给CPu的感觉是你这个程序一直在运行 没有IO从而提升程序的运行效率 多道技术 切换+保存状态 CPU两种切换 1.程序遇到IO 2.程序长时间占用 TCP服务端 accept recv 代码如何做到:切换+保存状态? 切换:切换不一定是提升效率,也有可能是降低效率 IO切 提升 没有IO切 降低 保存状态:保存上一次我执行的状态,下一次接着上一次的操作继续往后执行 yield
2】优点
- 协程的切换开销更小,属于程序级别的切换,操作系统完全感知不到,因而更加轻量级
- 单线程内就可以实现并发的效果,最大限度地利用cpu
- 应用程序级别速度要远远高于操作系统的切换
3】缺点
- 协程的本质是单线程下,无法利用多核,可以是一个程序开启多个进程,每个进程内开启多个线程,每个线程内开启协程
- 协程指的是单个线程,因而一旦协程出现阻塞,将会阻塞整个线程(多个任务一旦有一个阻塞没有切,整个线程都阻塞在原地,该线程内的其他的任务都不能执行了)
4】总结
- 1.必须在只有一个单线程里实现并发
- 2.修改共享数据不需加锁
- 3.用户程序里自己保存多个控制流的上下文栈
- 4.附加:一个协程遇到IO操作自动切换到其它协程(如何实现检测IO,yield、greenlet都无法实现,就用到了gevent模块(select机制))
代码示例
1 协程代码示例: 2 3 import time 4 from gevent import monkey;monkey.patch_all() 5 # 固定编写 用于检测所有的IO操作(猴子补丁) 6 7 from gevent import spawn 8 9 10 def func1(): 11 print('func1 running') 12 time.sleep(3) 13 print('func1 over') 14 15 16 def func2(): 17 print('func2 running') 18 time.sleep(5) 19 print('func2 over') 20 21 22 if __name__ == '__main__': 23 start_time = time.time() 24 # func1() 25 # func2() 26 s1 = spawn(func1) # 检测代码 一旦有IO自动切换(执行没有io的操作,变向的等待io结束) 27 s2 = spawn(func2) 28 s1.join() 29 s2.join() 30 print(time.time() - start_time) # 8.01237154006958 协程 5.015487432479858 31 32 33 34 # spawn监管IO操作,gevent模块本身无法检测常见的一些io操作,在使用的时候需要额外的导入一句话:from gevent import monkey 35 # monkey.patch_all() 36 # 又由于上面的两句话在使用gevent模块的时候需要经常性的重复导入,所以gevent模块还提供了简写:from gevent import monkey;monkey.patch_all(),
。
。
【gevent模块】
1 ''' 2 Gevent模块的功能包括: 3 4 实现并发同步或异步编程:Gevent模块使得使用协程实现并发编程变得简单高效。通过Greenlet模式,我们可以创建并管理多个并发任务,实现异步执行。 5 事件驱动的网络编程:Gevent模块提供了一个事件驱动的网络编程框架,可以轻松处理各种网络I/O操作,如socket连接、数据读取和写入等。 6 支持异步调用:Gevent模块支持异步调用,使得我们可以编写非阻塞的代码,无需担心线程或进程阻塞问题。 7 简化并发编程:Gevent模块简化了并发编程的复杂性,提供了简单易用的API和模式,使得我们能够轻松地处理并发任务。 8 高性能:Gevent模块在处理大量并发任务时表现出高性能,可以有效地利用系统资源,提高程序的执行效率。 9 ''' 10 import time 11 from gevent import spawn 12 from gevent import monkey 13 14 monkey.patch_all() # 猴子补丁,将程序中所有的阻塞操作都替换为非阻塞操作 15 16 17 def heng(): 18 print('哼') 19 time.sleep(2) 20 print('哼哼') 21 22 23 def ha(): 24 print('哈') 25 time.sleep(3) 26 print('哈哈') 27 28 # 新增函数,让它在执行时休眠4秒(最长) 29 def heiheihei(): 30 print('嘿嘿嘿') 31 time.sleep(4) 32 print('嘿嘿嘿') 33 34 35 start_time = time.time() 36 g1 = spawn(heng) 37 g2 = spawn(ha) 38 g3 = spawn(heiheihei) 39 g1.join() 40 g2.join() # 等待被检查的任务执行完毕,再往后继续执行,3.042381525039673 41 g3.join() # 4.035170078277588 验证: 耗时和最长的时间有关系 42 print(time.time() - start_time)
。
。
【协程实现TCP并发】
1 服务端 2 3 # 不借助多线程,多进程,就单纯的单核状态下,实现并发 4 from gevent import monkey;monkey.patch_all() 5 import socket 6 from gevent import spawn 7 8 9 def encapsulation(conn): 10 while True: 11 try: 12 data = conn.recv(1024) 13 if len(data) == 0: break 14 conn.send(data.upper()) 15 except ConnectionResetError as e: 16 print(e) 17 break 18 conn.close() 19 20 21 def server(ip, port): 22 server = socket.socket() 23 server.bind((ip, port)) 24 server.listen(5) 25 while True: 26 conn, addr = server.accept() 27 spawn(encapsulation, conn) 28 29 30 if __name__ == '__main__': 31 g1 = spawn(server, '127.0.0.1', 8000) 32 g1.join() 33 34 35 36 ============================================== 37 客户端 38 39 import socket 40 from threading import Thread, current_thread 41 42 43 # 模拟多个客户端 44 def x_client(): 45 client = socket.socket() 46 client.connect(('127.0.0.1', 8000)) 47 n = 0 48 49 while True: 50 msg = '%s say hello %s' % (current_thread().name, n) 51 n += 1 52 client.send(msg.encode('utf-8')) 53 data = client.recv(1024) 54 print(data.decode('utf-8')) 55 56 57 if __name__ == '__main__': 58 for i in range(100): 59 t = Thread(target=x_client) 60 t.start() 61 62 63 # ===========================总结 64 ''' 65 单线程下实现并发 66 我们可以通过多进程下面开始多线程 67 多线程下再开设协程 68 从而使我们的程序更加的高效 69 70 '''
分类:
网络编程
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】