• 并发编程之线程进阶


    一、互斥锁

    多进程中数据不安全,所以要加锁。

    多线程虽然有GIL锁,但是由于GIL锁轮转的策略(多线程之间时间片的轮转),仍存在数据不安全的情况,但是相对几率较低。

    GIL锁轮转的策略:早期执行700条指令(不是700行,+= 操作相当于4条指令),现在是执行一个时间片时间,当前线程会让出cpu给其他线程使用。

    dis模块中的方法可以查看某个操作对应的cpu指令

     

    解决线程之间的数据安全的问题:

    ①多线程中,不在线程中操作全局变量

    ②涉及+=,-=,lis[0]+1,类似的操作一定要加锁

    ③列表、字典自带的方法都是线程安全的

    ④队列也是数据安全的

     

    线程不安全的案例

    from threading import Thread
    
    count = 0
    def fun_add():
        global count
        for i in range(100000):
            count += 1
    
    def fun_sub():
        global count
        for i in range(100000):
            count -= 1
    
    t_lis = []
    for i in range(10):
        t1 = Thread(target=fun_add)
        t1.start()
        t_lis.append(t1)
        t2 = Thread(target=fun_sub)
        t2.start()
        t_lis.append(t2)
    for t in t_lis:
        t.join()
    print(count)            # -98445

     

    使用互斥锁解决线程安全问题(操作的指令都加上锁)

    from threading import Thread,Lock
    
    count = 0
    def fun_add(lock):
        global count
        for i in range(100000):
            lock.acquire()
            count += 1
            lock.release()
    
    def fun_sub(lock):
        global count
        for i in range(100000):
            lock.acquire()
            count -= 1
            lock.release()
    
    t_lis = []
    lock = Lock()                   # 创建锁对象
    for i in range(10):
        t1 = Thread(target=fun_add,args=(lock,))
        t1.start()
        t_lis.append(t1)
        t2 = Thread(target=fun_sub,args=(lock,))
        t2.start()
        t_lis.append(t2)
    for t in t_lis:
        t.join()                    # 等待所有的子线程执行完
    print(count)                    # 0

     

    二、递归锁

    当在并发的情况下使用两把锁,会造成死锁的现象。一个线程抢占到一把锁,另一个线程抢占到另一把锁,而操作需要同时抢占两把锁才能执行操作。

    解决方案:递归锁

    多少个acquire上锁,就要有多少个release释放锁,一个线程先acquire后,其他的线程只能等着。这个锁好比像一串钥匙。

     

    递归锁和互斥锁的区别:

    互斥锁是两把锁多个线程抢占,而递归锁是一把锁多个线程抢占

    在一个线程里,用多个锁的时候,用递归锁实例化一个锁,acquire多次

    在一个线程里,只用一个所的时候,用互斥锁为了提高效率,在锁多个资源的时候,应该酌情选用互斥锁,用完一个资源应该马上释放

     

    递归锁能够快速的解决死锁问题,但是递归锁并不是一个好的解决方案,死锁现象的发生不是互斥锁的问题,而是代码的逻辑问题,递归锁只是临时快速解决死锁的有效方案,解决时只需将递归锁替换互斥锁。后续需要将递归锁重新替换成互斥锁,完善代码的逻辑,并且提高代码的效率

    多线程之间,用完一个资源再用另一个资源,应该先释放一个资源再去获取一个资源的锁

     

    经典死锁案例:科学家吃面(互斥锁)

    from threading import Thread,Lock
    import time
    
    noodles_lock = Lock()
    fork_lock = Lock()
    def eat1(name,i,):
        fork_lock.acquire()
        print('%s%s拿到叉子'%(name,i))
        noodles_lock.acquire()
        print('%s%s拿到面条'%(name,i))
        print('%s%s吃面'%(name,i))
        time.sleep(0.5)
        fork_lock.release()
        print('%s%s放下叉子' % (name, i))
        noodles_lock.release()
        print('%s%s放下面条' % (name, i))
    
    def eat2(name,i,):
        noodles_lock.acquire()
        print('%s%s拿到面条'%(name,i))
        fork_lock.acquire()
        print('%s%s拿到叉子' % (name, i))
        print('%s%s吃面'%(name,i))
        time.sleep(0.5)
        noodles_lock.release()
        print('%s%s放下面条' % (name, i))
        fork_lock.release()
        print('%s%s放下叉子' % (name, i))
    
    for i in range(2):Thread(target=eat1,args=('科学家',i+1)).start()
    for i in range(3,5):Thread(target=eat2,args=('科学家',i)).start()
    
    '''
    科学家1拿到叉子
    科学家1拿到面条
    科学家1吃面
    科学家1放下叉子
    科学家1放下面条
    科学家3拿到面条
    科学家2拿到叉子
    '''

     

    递归锁解决方案

    from threading import Thread,RLock
    import time
    
    fork_lock = noodles_lock = RLock()          # 递归锁
    def eat1(name,i,):
        fork_lock.acquire()
        print('%s%s拿到叉子'%(name,i))
        noodles_lock.acquire()
        print('%s%s拿到面条'%(name,i))
        print('%s%s吃面'%(name,i))
        time.sleep(0.5)
        fork_lock.release()
        print('%s%s放下叉子' % (name, i))
        noodles_lock.release()
        print('%s%s放下面条' % (name, i))
    
    def eat2(name,i,):
        noodles_lock.acquire()
        print('%s%s拿到面条'%(name,i))
        fork_lock.acquire()
        print('%s%s拿到叉子' % (name, i))
        print('%s%s吃面'%(name,i))
        time.sleep(0.5)
        noodles_lock.release()
        print('%s%s放下面条' % (name, i))
        fork_lock.release()
        print('%s%s放下叉子' % (name, i))
    
    for i in range(2):Thread(target=eat1,args=('科学家',i+1)).start()
    for i in range(3,5):Thread(target=eat2,args=('科学家',i)).start()
    
    '''
    科学家1拿到叉子
    科学家1拿到面条
    科学家1吃面
    科学家1放下叉子
    科学家1放下面条
    科学家2拿到叉子
    科学家2拿到面条
    科学家2吃面
    科学家2放下叉子
    科学家2放下面条
    科学家3拿到面条
    科学家3拿到叉子
    科学家3吃面
    科学家3放下面条
    科学家3放下叉子
    科学家4拿到面条
    科学家4拿到叉子
    科学家4吃面
    科学家4放下面条
    科学家4放下叉子
    '''

     

    三、信号量

    信号量是基于锁+计数器实现的,使用方式跟进程的信号量一样使用

    from threading import Semaphore,Thread

    from threading import Semaphore,Thread
    import time
    
    def func(index,sem):
        sem.acquire()
        print(index)
        time.sleep(2)
        sem.release()
    
    sem = Semaphore(4)          
    for i in range(12):
        Thread(target=func,args=(i,sem)).start()

     

    四、事件

    事件的应用:检测数据库连接

    from threading import Event,Thread

    方法:

    wait()    # 可以设置阻塞的时间

    set()     # 将信号设置为true

    clear()       # 将信号设置为False

    is_set()   # 查看信号的状态

    事件的默认状态时False

    # 检测数据库连接
    
    from threading import Event,Thread
    import time
    
    def check(e):
        time.sleep(2)
        e.set()             # 将信号设置为True
    
    def connect(e):
        for i in range(3):
            e.wait(1)       # 阻塞一秒
            if e.is_set():  # 查看信号的状态        
                print('连接成功')
                break
        else:print('连接失败')
    
    e = Event()
    Thread(target=check,args=(e,)).start()
    Thread(target=connect,args=(e,)).start()

     

    五、条件

    方法:

    notify()      # 控制流量,通知多少个可以通过,有参数。

    wait()        # 阻塞所有进程

    notify_all()      # 全部放行,一般配合notify()使用

    这两个方法都是线程不安全的,每个方法使用的前后都需要加锁,条件里面有锁的方法。

    # 条件
    
    from threading import Condition,Thread
    import time
    
    def get_through(name,c):
        print('%s在等待'%name)
        c.acquire()
        c.wait()                # 阻塞,等待通过线程的命令
        print('%s通过'%name)
        c.release()
    
    name_list = ['刘一','陈二','张三','李四','王五','赵六','孙七','周八']
    c = Condition()
    for i in name_list:
        t = Thread(target=get_through,args=(i,c))
        t.start()
    
    for k in range(4):
        c.acquire()
        c.notify(2)         # 设置每次通过的线程数
        c.release()
        time.sleep(5)

     

    六、定时器

     使用场景:定时任务

    Timer(n,函数)    实例化时接收两个参数,(执行的m秒数,执行的函数)

    不影响主线程

    # 定时器
    
    from threading import Timer
    
    def func():
        print('action')
    
    t = Timer(5,func)           # 创建子线程,并且设置开启子线程的时间
    t.start()

    七、队列

    qps概念:每秒钟接收到的请求数

    队列的线程是安全的,队列用于做排队相关的逻辑,帮助维持相应的顺序

    特点:先进先出

    方法:

    get()

    put()

    get_nowait()

    put_nowait()

    import queue
    
    q = queue.Queue()
    q.put(1)
    print(q.get())

    八、新的队列

    from queue import LifoQueue

    类似于栈,特点是后进先出,并且不允许插队

    应用:算法的完成,有点类似分布式的思想,例如:三级菜单

    from queue import LifoQueue
    
    q = LifoQueue()
    for i in range(1,6):
        q.put(i)
    
    for i in range(1,6):
        print(q.get(),end=' ')      # 5 4 3 2 1 

    九、优先级队列

    只能放同一种类似的值

    应用场景:会员服务

    ①如果是数值,按照数值从小到大取值

    from queue import PriorityQueue
    
    q = PriorityQueue()
    q.put(10)
    q.put(5)
    q.put(20)
    
    for i in range(3):
        print(q.get(),end=' ')      # 5 10 20 

    ②如果是字符串,按照ASICC编码来取值

    from queue import PriorityQueue
    
    q = PriorityQueue()
    q.put('c')
    q.put('a')
    q.put('b')
    
    for i in range(3):
        print(q.get(), end=' ')     # a b c 

    ③如果是数字、字母组成的元组,按第一个元素来取值,从小到大取值

    from queue import PriorityQueue
    
    q = PriorityQueue()
    
    q.put((3,'zxc'))
    q.put((3,'abc'))
    q.put((1,'asd'))
    q.put((2,'qwe'))
    
    for i in range(4):
        print(q.get(),end=' ')      # (1, 'asd') (2, 'qwe') (3, 'abc') (3, 'zxc') 

    十、线程池

    concurrent.futures 模块不仅提供线程池,还提供进程池。

    from concurrent.futures import ThreadPoolExecutor         # 线程池
    from concurrent.futures import ProcessPoolExecutor            # 进程池

    实例化的线程池数量 = 5 * cpu_count

     

    方法:

    submit(函数,参数)    异步提交任务,只能按位置传参,不用加args=

    ret = submit()         获取返回值,需要通过result()方法取值

    ret.result()        获取值

    map(函数,iterable)     取代for循环submit操作

    shutdown()         等于进程池的close()和join()方法,阻塞直到任务完成

     

    ① 有返回值

    from concurrent.futures import ThreadPoolExecutor
    from threading import currentThread
    
    def func(i):
        print('子线程号:',currentThread().ident)        # 打印子线程的线程号
        return i * '*'
    
    tp = ThreadPoolExecutor(5)          # 创建线程池,创建5个线程
    ret_lis = []
    for i in range(15):
        ret = tp.submit(func,i)         # 异步提交任务
        ret_lis.append(ret)             # 将返回值存到列表
    for ret in ret_lis:
        print(ret.result())               # 通过result()方法获取返回值的值
    print('主线程',currentThread().ident)   # 打印主线程的线程号

     

     

    ② 无返回值

    from concurrent.futures import ThreadPoolExecutor
    from threading import currentThread
    
    def func():
        print('子进程',currentThread().ident)      # 打印子线程的线程号
    
    tp = ThreadPoolExecutor(3)                    # 创建线程池,开启3个线程
    for i in range(9):
        tp.submit(func)                           # 异步提交任务
    tp.shutdown()                                 # 阻塞主线程,待所有的子线程运行完
    print('主线程',currentThread().ident)

     

    ③ map方法

    使用may方法必须传入参数

    from concurrent.futures import ThreadPoolExecutor
    from threading import currentThread
    import time
    
    def func(n):                            # 使用map必须有一个参数
        print('子线程号:',currentThread().ident)
        time.sleep(1)
    
    tp = ThreadPoolExecutor(3)
    ret = tp.map(func,range(15))            # map函数会传入一个参数
    # for i in range(15):
    #     tp.submit(func,i)
    #  异步提交任务
    print('主线程:',currentThread().ident)

     十一、回调函数

    线程池和进程池的回调函数通过submit实现的,

    add_done_callback调用回调函数,不需要传参,回调函数需要通过result()取值

    线程池的回调函数由子线程完成

    from concurrent.futures import ThreadPoolExecutor
    from threading import currentThread
    import time
    
    def func(i):
        print('子线程:',currentThread().ident)         # 获取子线程的线程号
        time.sleep(1)
        return i
    
    def call_back(ret):
        print('ret>',ret.result())                      # 通过result()方法取值
        print('callback线程号:',currentThread().ident)   # 获取回调函数的线程号
    
    tp = ThreadPoolExecutor(3)
    for i in range(9):
        tp.submit(func,(i+1)).add_done_callback(call_back)  # add_done_callback回调函数的方法,函数不需要传入参数
    tp.shutdown()                                           # 阻塞主线程,等待所有子线程执行完
    print('主线程号:',currentThread().ident)

    进程池的回调函数由主进程完成

    from concurrent.futures import ProcessPoolExecutor
    import time,os
    
    def func(i):
        print('子进程:',os.getpid())         # 获取子进程的进程号
        time.sleep(1)
        return i
    
    def call_back(ret):
        print('ret>',ret.result())            # 通过result()方法取值
        print('callback进程号:',os.getpid())   # 获取回调函数的进程号
    
    if __name__ == '__main__':
    
        tp = ProcessPoolExecutor(3)
        for i in range(9):
            tp.submit(func,(i+1)).add_done_callback(call_back)  # add_done_callback回调函数的方法,函数不需要传入参数
        tp.shutdown()                           # 阻塞主进程
        print('主线程号:',os.getpid())

    十二、local模块

    from threading import local

    不同线程的ID存储的值和取到的值是不同的

    多个线程之间使用threading.local对象,可以实现多个线程之间的数据隔离

    import time
    import random
    from threading import local,Thread
    
    loc = local()
    def func2():
        global loc
        print(loc.name,loc.age)
    
    def func1(name,age):
        global loc
        loc.name = name
        loc.age = age
        time.sleep(random.random())
        func2()
    
    Thread(target=func1,args=('xiaobai',20)).start()
    Thread(target=func1,args=('xiaohei',25)).start()
    
    '''
    xiaobai 20
    xiaohei 25
    '''
  • 相关阅读:
    使用Azure CLI实现自动关闭Azure虚拟机的脚本
    Azure自动化部署服务 (1)
    证书相关知识
    Azure上七层负载均衡APP Gateway
    简谈 Java 中的泛型通配符
    表单提交后为什么要重定向?
    eclipse 误删文件的恢复,代码的恢复
    书籍列表
    Mybatis Generator最完整配置详解
    学习spring mvc
  • 原文地址:https://www.cnblogs.com/st-st/p/9700265.html
Copyright © 2020-2023  润新知