Python-Level2-day14:创建带参线程与线程类,同步互斥方法Event&Lock解决资源争夺,线程死锁问题,线程的GIL问题,进程线程对比,多进程/线程网络并发模型实现

2.3.3 创建线程类

  1. 创建步骤

    【1】 继承Thread类

    【2】 重写__init__方法添加自己的属性,使用super()加载父类属性

    【3】 重写run()方法

  2. 使用方法

    【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 线程同步互斥

  • 线程通信方法: 线程间使用全局变量进行通信

    机制优点:保证数据处理的正确性,缺点是损失了执行效率

  • 线程之间共享资源争夺问题

    • 共享资源:多线程都可以操作的资源称为共享资源。对共享资源的操作代码段称为临界区。

    • 影响 :对共享资源的无序操作可能会带来数据的混乱,或者操作错误。此时往往需要同步互斥机制协调操作顺序

  • 解决方法:同步互斥机制(创造阻塞原理阻止其他进程操作资源,程序执行效率上略低,但换取了安全)

    • 同步 : 同步是一种协作关系,为完成操作,线程间形成一种协调,按照必要的步骤有序执行操作。例如消息队列。不能只放不取出或者只取不放。线程之间相互配合。

img编辑

    • 互斥 : 互斥是一种制约关系,当一个进程或者线程优先占有资源时会进行加锁处理,此时其他进程线程就无法操作该资源需要等待,直到解锁后才能操作。例如上厕所。

img编辑

  • 线程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 死锁

  • 什么是死锁

    死锁是指两个或两个以上的线程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁。线程双方需要先得到对方资源才能释放自己手握对方需要的资源。

img编辑

  • 死锁产生条件

    • 互斥条件:指线程使用了互斥方法,使用一个资源时其他线程无法使用。

    • 请求和保持条件:指线程已经保持至少一个资源,但又提出了新的资源请求,在获取到新的资源前不会释放自己保持的资源。

    • 不剥夺条件:不会受到线程外部的干扰,一直阻塞。可以做超时检测让他不阻塞。

    • 环路等待条件:发生在多个线程之间,不一定是两个线程间的死锁。在发生死锁时,必然存在一个线程——资源的环形链,如 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 进程线程的区别联系

  • 区别联系

  1. 两者都是多任务编程方式,都能使用计算机多核资源

  2. 进程的创建删除过程消耗的计算机资源比线程多

  3. 进程空间独立,数据互不干扰,有专门通信方法;线程使用全局变量通信

  4. 一个进程可以有多个分支线程,两者有包含关系

  5. 多个线程共享进程资源,在共享资源操作时往往需要同步互斥处理

  6. Python线程存在GIL问题,但是进程没有。

  • 使用场景

    img编辑

  1. 任务场景:一个大型服务,往往包含多个独立的任务模块,每个任务模块又有多个小独立任务构成,此时整个项目可能有多个进程,每个进程又有多个线程。

  2. 编程语言:Java,C#之类的编程语言在执行多任务时一般都是用线程完成,因为线程资源消耗少;而Python由于GIL问题往往使用多进程。

3. 网络并发模型

3.1 网络并发模型概述

  • 什么是网络并发

    在实际工作中,一个服务端程序往往要应对多个客户端同时发起访问的情况。如果让服务端程序能够更好的同时满足更多客户端网络请求的情形,这就是并发网络模型。利用进程线程与套接字tcp/udp结合的产物。

img编辑

  • 循环网络模型问题

    循环网络模型只能循环接收客户端请求,处理请求。同一时刻只能处理一个客户端请求,处理完毕后再处理下一个。这样的网络模型虽然简单,资源占用不多,但是无法同时处理多个客户端请求就是其最大的弊端,往往只有在一些低频的小请求任务中才会使用。

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. 进程线程复习
​
​
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

dpq666dpq666

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值