2.3.3 创建线程类
-
创建步骤
【1】 继承Thread类
【2】 重写
__init__
方法添加自己的属性,使用super()加载父类属性【3】 重写run()方法
-
使用方法
【1】 实例化对象
【2】 调用start自动执行run方法
""" 带参数创建多个线程案例 """ from threading import Thread from time import sleep # 带有参数的线程函数 def func(sec, count): print("线程%s开始执行" % count) sleep(sec) print("线程%s执行完毕" % count) # 循环创建多线程 job = [] for i in range(4): t = Thread(target=func, args=(1, i), daemon=True) # 主线程结束,分支线程随之结束 t.start() job.append(t) # t.join()#等待这个线程完全结束再启动下个线程 for t in job: t.join() # 等待所有分支线程执行完成才执行主线程 print("主线程开始执行") print("主线程执行完毕")
""" 面向对象实现创建线程类 进程与线程到目前为止好像就内存共享这方面有点区别 """ from threading import Thread from time import sleep # 创建进程和进程执行内容都写在类中 --> 面向对象 class MyThread(Thread): def __init__(self, value): self.value = value super().__init__() # 执行父类init self.daemon = True # 主线程结束子线程也结束 # 你想让进程做什么就写什么 def func(self): print('开始子线程执行') sleep(self.value) print('子线程执行结束') # 运行start自动执行run方法,作为线程内容 def run(self): self.func() if __name__ == '__main__': t = MyThread(1) t.start() # 创建进程 t.join() print("主线程结束") # 父类伪代码 # class Thread: # def __init__(self,target=None): # self._target = target # # def run(self):#run是接口函数,帮你规定了,里面就是pass,专门给你从写 # self._target() # # def start(self): # # 创建进程 # self.run()
""" 练习01: 假设有 500张票记为 T1--T500 将这些票存到一个容器里 创建10个分支线程模拟10个窗口 W1--W10 10个窗口卖这500张票,直到卖完为止,每卖出一张 打印 w1----T250 每卖一张需要 0.1秒出票 """ # 出现索引越界原因可能是在sleep的时候几个线程发现循环条件满足都进入循环体 # 其中一个线程优先抢到票列表为空了,但其他线程抢不到却还在循环体里面, # 循环体里面的列表空了却还pop必然报错 # 共享资源线程之间无序抢占 from threading import Thread from time import sleep list01 = [] for i in range(500): list01.append(i) def sell(i): while list01: # sleep(0.1) sleep不要写在上面,写下面去 print(f"第{i}窗口的第{list01.pop()}张票卖出") sleep(0.1) jobs = [] for i in range(10): t = Thread(target=sell, args=(i,)) t.start() jobs.append(t) for i in jobs: i.join() print("have no any ticket")
2.3.4 线程同步互斥
-
线程通信方法: 线程间使用全局变量进行通信
机制优点:保证数据处理的正确性,缺点是损失了执行效率
-
线程之间共享资源争夺问题
-
共享资源:多线程都可以操作的资源称为共享资源。对共享资源的操作代码段称为临界区。
-
影响 :对共享资源的无序操作可能会带来数据的混乱,或者操作错误。此时往往需要同步互斥机制协调操作顺序。
-
-
解决方法:同步互斥机制(创造阻塞原理阻止其他进程操作资源,程序执行效率上略低,但换取了安全)
-
同步 : 同步是一种协作关系,为完成操作,线程间形成一种协调,按照必要的步骤有序执行操作。例如消息队列。不能只放不取出或者只取不放。线程之间相互配合。
-
编辑
-
-
互斥 : 互斥是一种制约关系,当一个进程或者线程优先占有资源时会进行加锁处理,此时其他进程线程就无法操作该资源需要等待,直到解锁后才能操作。例如上厕所。
-
编辑
-
线程Event
from threading import Event e = Event() 创建线程event对象 对象e有set与unset两个状态,当set状态时候调用wait不阻塞,当unset状态调用wait阻塞,初始是unset状态 e.wait([timeout]) 阻塞等待即e被set,参数值阻塞时间,不写默认被通知后就不阻塞。 e.set() 设置e,将unset状态设置set状态、使wait结束阻塞 e.clear() 使e回到未被设置状态,将set状态设置unset状态 e.is_set() 查看当前e是什么状态
""" 线程同步互斥方法案例 Event 有时候口令正确,有时候口令不正确:原因是如果子线程执行快,提前 修改里口令那么主线程判断后就知道自己人,父线程执行快子线程还没有来得及修改口令 就导致主线程判断后是内鬼。 """ from threading import Thread, Event msg = None # 线程间通信变量 e = Event() # event对象 def 杨子荣(): print("杨子荣前来拜山头") global msg msg = '天王盖地虎' e.set() # 通知wait可以结束阻塞:通知主线程可以执行 t = Thread(target=杨子荣) t.start() # 主线程验证 print("说对口令才是自己人") # 方案1:sleep(1)如果用sleep浪费剩余的等待时间,用event通知修改完成机制 # 方案2:t.join()用它虽然可以但导致不能让两个线程同时执行了 # 方案3: e.wait() # 在这阻塞等待 if msg == "天王盖地虎": print("宝塔镇河妖") print("确认过眼神,你是对的人...") else: print("打死他 .....")
-
线程锁 Lock
from threading import Lock lock = Lock() 创建锁对象 lock.acquire() 上锁 如果lock已经上锁第二次再它调用就会阻塞 lock.release() 解锁
""" 线程同步互斥案例 Lock 如果不上锁那么子线程一定打印 父与子线程都上了锁,无论哪一个先执行,被锁住的语句块一定不会被另外 一个线程用上,因为另外一个线程等着操作这个语句块还在阻塞当中。 """ from threading import Thread, Lock lock = Lock() a = b = 1 # 子进程不同就打印 def values(): while True: lock.acquire() if a != b: print("a = %d,b = %d" % (a, b)) lock.release() t = Thread(target=values) t.start() while True: lock.acquire() # 上锁 a += 1 # 主线程a自增了,b还没有来得及做,子线程就执行了 b += 1 lock.release() # 解锁
""" 练习02 :线程的执行顺序管控问题: 有两个分支线程,一个打印 1--26 这26个数 另一个打印 A - Z 这26个字母 两个分支线程一起运行,控制台需要打印顺序为 1A2B ..... 26Z 思路:一个线程上锁,另一个线程执行语句块后给他解锁 另一个线程解锁完成再给刚刚那个线程上锁就可以 执行完语句块后再给他解锁 提示 : chr(65) --> A chr(90) --> Z """ from threading import Thread from threading import Lock l1 = Lock() l2 = Lock() def func(): for i in range(1, 27): l1.acquire() print(i, end="") l2.release() l2.acquire() t = Thread(target=func) t.start() for j in range(65, 91): l2.acquire() print(chr(j), end=" ") l1.release()
2.3.5 死锁
-
什么是死锁
死锁是指两个或两个以上的线程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁。线程双方需要先得到对方资源才能释放自己手握对方需要的资源。
编辑
-
死锁产生条件
-
互斥条件:指线程使用了互斥方法,使用一个资源时其他线程无法使用。
-
请求和保持条件:指线程已经保持至少一个资源,但又提出了新的资源请求,在获取到新的资源前不会释放自己保持的资源。
-
不剥夺条件:不会受到线程外部的干扰,一直阻塞。可以做超时检测让他不阻塞。
-
环路等待条件:发生在多个线程之间,不一定是两个线程间的死锁。在发生死锁时,必然存在一个线程——资源的环形链,如 T0正在等待一个T1占用的资源;T1正在等待T2占用的资源,……,Tn正在等待已被T0占用的资源。
-
-
如何避免死锁
-
逻辑清晰,不要同时出现上述死锁产生的四个条件
-
通过测试工程师进行死锁检测
-
""" 死锁情况展示 """ from threading import Thread, Lock from time import sleep # 账户类 class Account: def __init__(self, id, balance, lock): self._id = id self._balance = balance # 余额 self.lock = lock # 取钱 def withdraw(self, amount): self._balance -= amount # 存钱 def deposit(self, amount): self._balance += amount # 查看余额 def getBlance(self): return self._balance # 转账函数 def transfer(from_, to, amount): # 加下划线区分关键字 from_.lock.acquire() # 自己上锁 from_.withdraw(amount) # 钱减少 from_.lock.release() # 自己解锁 # 这句放这不会死锁 sleep(0.1) # 这点时间让两个线程都运行到这里。 to.lock.acquire() # 对方上锁#两个线程都阻塞在这 to.deposit(amount) # 钱增加 # from_.lock.release() # 这句放这程序就死锁 to.lock.release() # 对方解锁 # 突然有一天两个人同一时间需要相互转账 if __name__ == '__main__': tom = Account("tom", 8000, Lock()) abby = Account("abby", 5000, Lock()) t1 = Thread(target=transfer, args=(tom, abby, 2000)) t2 = Thread(target=transfer, args=(abby, tom, 2000)) t1.start() t2.start() t1.join() t2.join() print('Tom:', tom.getBlance()) print('Abby:', abby.getBlance())
2.3.6 线程的GIL问题
-
什么是GIL问题 (全局解释器锁,解释器层面)
由于python解释器设计中加入了全局解释器锁,导致python解释器同一时刻只能解释执行一个线程,大大降低了线程的执行效率。根本不可能真正并行,即使有多核对应多线程。
-
导致后果 因为遇到阻塞时线程会主动让出解释器,去解释其他线程。所以python多线程在执行多阻塞任务时可以提升程序效率,其他情况并不能对效率有所提升。没有阻塞的多线程还不如单线程。例如10万以内质数求和。
-
关于GIL问题的处理
* 尽量使用进程完成无阻塞的并发行为 * 不使用c作为解释器 (可以用Java C#编写的解释器) Guido的声明:<http://www.artima.com/forums/flat.jsp?forum=106&thread=214235>
-
结论
-
GIL问题与Python语言本身并没什么关系,属于解释器设计的历史问题。
-
在无阻塞状态下,多线程程序程序执行效率并不高,甚至还不如单线程效率。
-
Python多线程只适用于执行有阻塞延迟的任务情形。
-
""" 无阻塞状态下线程效率实验 答案:提高不了,与单个线程差不多 """ from threading import Thread from time import time # 求函数运行时间装饰器 def timeis(func): def wapper(*args, **kwargs): begin = time() res = func(*args, **kwargs) print("执行时间:", time() - begin) return res return wapper def is_prime(num): if num <= 1: return False for i in range(2, num // 2 + 1): if num % i == 0: return False return True # 多个线程求解 class Prime(Thread): def __init__(self, begin, end): self.begin = begin self.end = end super().__init__() def run(self): prime = [] for i in range(self.begin, self.end): if is_prime(i): prime.append(i) print(sum(prime)) @timeis def thread_4(): jobs = [] for i in range(1, 100001, 25000): p = Prime(i, i + 25000) jobs.append(p) p.start() [i.join() for i in jobs] # 执行时间: 9.993874311447144 #thread_1() # 执行时间: 10.204878091812134 thread_4() # 执行时间: 10.364130735397339 #thread_10()
2.3.7 进程线程的区别联系
-
区别联系
-
两者都是多任务编程方式,都能使用计算机多核资源
-
进程的创建删除过程消耗的计算机资源比线程多
-
进程空间独立,数据互不干扰,有专门通信方法;线程使用全局变量通信
-
一个进程可以有多个分支线程,两者有包含关系
-
多个线程共享进程资源,在共享资源操作时往往需要同步互斥处理
-
Python线程存在GIL问题,但是进程没有。
-
使用场景
编辑
-
任务场景:一个大型服务,往往包含多个独立的任务模块,每个任务模块又有多个小独立任务构成,此时整个项目可能有多个进程,每个进程又有多个线程。
-
编程语言:Java,C#之类的编程语言在执行多任务时一般都是用线程完成,因为线程资源消耗少;而Python由于GIL问题往往使用多进程。
3. 网络并发模型
3.1 网络并发模型概述
-
什么是网络并发
在实际工作中,一个服务端程序往往要应对多个客户端同时发起访问的情况。如果让服务端程序能够更好的同时满足更多客户端网络请求的情形,这就是并发网络模型。利用进程线程与套接字tcp/udp结合的产物。
编辑
-
循环网络模型问题
循环网络模型只能循环接收客户端请求,处理请求。同一时刻只能处理一个客户端请求,处理完毕后再处理下一个。这样的网络模型虽然简单,资源占用不多,但是无法同时处理多个客户端请求就是其最大的弊端,往往只有在一些低频的小请求任务中才会使用。
3.2 多进程/线程并发模型
多进程/线程并发模中每当一个客户端连接服务器,就创建一个新的进程/线程为该客户端服务,客户端退出时再销毁该进程/线程,多任务并发模型也是实际工作中最为常用的服务端处理模型。
-
模型特点
-
优点:能同时满足多个客户端长期占有服务端需求,可以处理各种请求。
-
缺点: 资源消耗较大,资源不够加服务器。
-
适用情况:客户端请求较复杂,需要长时间占有服务器。
-
-
创建流程
-
创建网络套接字
-
等待客户端连接
-
有客户端连接,则创建新的进程/线程具体处理客户端请求
-
主进程/线程继续等待处理其他客户端连接
-
如果客户端退出,则销毁对应的进程/线程
-
""" 基于多进程的网络并发模型:tcp面向过程实现 重点代码 !! 服务端 """ import sys from multiprocessing import Process from socket import * # 服务端地址 HOST = "0.0.0.0" PORT = 8888 ADDR = (HOST, PORT) # 应对每个客户端请求 def handle(connfd): while True: data = connfd.recv(1024) if not data:#客户端退出,客户端套借字自动发送空字符 break print(data.decode()) connfd.send(b"OK") print("当前一个客户端关闭") connfd.close()#服务端这边关闭客户端,客户端发信息就接收不到了 # 主服务代码 def main(): # 创建tcp套接字 sock = socket() sock.bind(ADDR) sock.listen(5) #print("Listen the port %d" % PORT) # 循环处理客户端连接 while True: try: connfd, addr = sock.accept()#继续等待下一个客户端连接 print("Connect from", addr) except KeyboardInterrupt: sock.close() sys.exit("优雅退出:服务端结束") # 如果有客户端进来则创建新进程 p = Process(target=handle, args=(connfd,), daemon=True) p.start() if __name__ == '__main__': main() """ 客户端1 """ from socket import * # 服务器地址 ADDR = ("127.0.0.1",8888) # 创建与服务端相同类型套接字 默认参数 tcp_socket = socket() # 连接服务端 tcp_socket.connect(ADDR) # 发送接受 while True: msg = input(">>") if not msg:#客户端这边输入空字符退出 break tcp_socket.send(msg.encode()) data = tcp_socket.recv(1024) print("From server:",data.decode()) # 客户端套接字之前会发出一个空字符给服务端,通知他不需要为我服务, # 关闭那边的套接字吧 tcp_socket.close()
""" 基于线程的多任务并发模型:tcp面向对象实现 重点代码 !! 服务端 """ import sys from threading import Thread from socket import * # 创建线程 class ThreadServer(Thread): def __init__(self, connfd): self.connfd = connfd self.handle = self.handle super().__init__() def handle(self): while True: data = self.connfd.recv(1024) if not data: break print(data.decode()) self.connfd.send(b"OK") self.connfd.close() print("有一个客户端退出了") def run(self): self.handle() # 创建tcp网络模型 class TCPServer: def __init__(self, host="", port=0): # 魔法函数,实例化变量直接执行 self.host = host self.port = port self.address = (host, port) self.sock = self._create_socket() def _create_socket(self): # 单个下划线当前类与子类可以用,外边类最好不用 sock = socket() sock.bind(self.address) return sock def serve_forever(self): self.sock.listen(5) # 等待新的客户端发起连接 # 循环处理客户端连接 while True: try: connfd, addr = self.sock.accept() print("Connect from", addr) except KeyboardInterrupt: self.sock.close() sys.exit("优雅退出:服务结束") # 创建线程 t = ThreadServer(connfd) t.start() if __name__ == '__main__': server = TCPServer(host="0.0.0.0", port=8888) server.serve_forever() """ 客户端1 """ from socket import * # 服务器地址 ADDR = ("127.0.0.1",8888) # 创建与服务端相同类型套接字 默认参数 tcp_socket = socket() # 连接服务端 tcp_socket.connect(ADDR) # 发送接受 while True: msg = input(">>") if not msg:#客户端这边输入空字符退出 break tcp_socket.send(msg.encode()) data = tcp_socket.recv(1024) print("From server:",data.decode()) # 客户端套接字之前会发出一个空字符给服务端,通知他不需要为我服务, # 关闭那边的套接字吧 tcp_socket.close()
前情回顾 1. 进程间通信 套接字---非亲缘 消息队列 : Queue() q.put() q.get() ---亲缘通信 2. 聊天室 * 思考流程: 需求分析 技术分析 模块划分 通信协议 具体逻辑 * 总分结构: 服务端应对客户端多种请求 * 通信协议请求: 请求类型有多种 (我们自己根据需求做应用协议) * 先搭建框架 每个功能先写逻辑流程 每个功能单独测试 3. 线程 * 一个进程(资源分配最小单元)包含多个线程(cpu分配最小单元) ----单进程可以认为是单线程 * 多任务编程,每个线程独立执行 * 这些线程公用进程资源 创建线程 : Thread() t.start() t.join() 练习01: 假设有 500张票记为 T1--T500 将这些票存到一个容器里 创建10个分支线程模拟10个窗口 W1--W10 10个窗口卖这500张票,直到卖完为止,每卖出一张 打印 w1----T250 每卖一张需要 0.1秒出票 练习02 : 有两个分支线程,一个打印 1--52 这52个数 另一个打印 A - Z 这26个字母 两个分支线程一起运行需要打印顺序为 12A34B ..... 5152Z 提示 : chr(65) --> A 训练: 使用面向对象方式,编写多线程网络并发模型 作业 : 1. 重点代码自己独立完成 2. 进程线程复习