• python的Lock锁,线程同步


    一、Lock锁

    • 凡是存在共享资源争抢的地方都可以使用锁,从而保证只有一个使用者可以完全使用这个资源一旦线程获得锁,其他试图获取锁的线程将被阻塞
    • acquire(blocking=True,timeout=-1): 默认阻塞,阻塞可以设置超时时间,非阻塞时,timeout禁止设置,成功获取锁,返回True,否则返回False
    • releas() : 释放锁,可以从任何线程调用释放,已上锁的锁,会被重置为unlocked未上锁的锁上调用,抛出RuntimeError异常
    例如:
        订单要求生成1000个杯子,组织10个工人生产
        
    举例1:
    
        import threading
        from threading import Thread, Lock
        import time
        import logging
    
        FORAMT = '%(asctime)s %(threadName)s %(thread)d %(message)s'
        logging.basicConfig(format=FORAMT, level=logging.INFO)
    
        cups = []
    
        def worker(count=10):
            logging.info("I'm working for u")
            flag = False
            while True:
                if len(cups) >= count:
                    flag = True
                time.sleep(0.001)   # 为了看出现场切换效果
                if not flag:
                    cups.append(1)
                if flag:
                    break
            logging.info("{} finished.cups= {}".format(threading.current_thread().name, len(cups)))
    
        for _ in range(10):  # 开启10个线程
            Thread(target=worker, args=(1000,)).start()
    
        从上例的运行结果看出,多线程调度,导致了判断失效,多生成了杯子
        如何修改,加锁
        
        
    举例2:
        import threading
        from threading import Thread, Lock
        import time
        import logging
    
        FORAMT = '%(asctime)s %(threadName)s %(thread)d %(message)s'
        logging.basicConfig(format=FORAMT, level=logging.INFO)
    
        cups = []
        lock = Lock()
    
        def worker(count=10):
            logging.info("I'm working for u")
            flag = False
            while True:
                lock.acquire()
                if len(cups) >= count:
                    flag = True
                #lock.release()   # 这里释放锁合适吗?
                time.sleep(0.001)   # 为了看出现场切换效果
                if not flag:
                    cups.append(1)
                lock.release()  # 这里释放锁对不对
                if flag:
                    break
            logging.info("{} finished.cups= {}".format(threading.current_thread().name, len(cups)))
    
        for _ in range(10):  # 开启10个线程
            Thread(target=worker, args=(1000,)).start()
    
        假设第一句lock.release()合适,分析如下:
        有一个时刻len(cups),正好是999,flag=True,释放锁,线程被打断,另一个线程判断发现是
        999,flag=True,可能线程被打断,可能另一个线程判断也是999,flag也设置为True
        这三个线程只要继续执行到cups.append(1),一定会导致cups的长度超过1000
        
        假设第二句lock.release()合适,分析如下:
        在某一时刻len(cups),正好是999,flag=True,其他线程试图访问这段代码的线程都被阻塞获取
        不到锁,直到当前线程安全的增加了一个数据,然后释放锁,其它线程有一个抢到锁,但发现
        已经是1000了,只好break打印退出,所有其他线程接着都退出
        


    二、加锁、解锁

    • 一般来说加锁后还要一些代码实现,在释放锁之前还有可能抛异常,一旦出现异常所无法释放,但是当前线程可能因为这个异常被终止了,这就产生了死锁
    • 加锁、解锁常用语句:
      • 使用try...finally语句保证锁的释放
      • with上下文管理,锁对象支持上下文管理
    import threading
    from threading import Thread, Lock
    import time
    
    class Counter:
        def __init__(self):
            self._val = 0
            self.__lock = Lock()
    
        @property
        def value(self):
            with self.__lock:
                return self._val
    
        def inc(self):
            try:
                self.__lock.acquire()
                self._val += 1
            finally:
                self.__lock.release()
    
        def dec(self):
            with self.__lock:
                self._val -= 1
    
    def run(c:Counter, count=100):
        for _ in range(count):
            for i in range(-50,50):
                if i < 0:
                    c.dec()
                else:
                    c.inc()
    
    c = Counter()
    c1 = 10
    c2 = 100
    for i in range(c1):
        Thread(target=run, args=(c,c2)).start()
    
    while True:
        time.sleep(1)
        if threading.active_count() == 1:
            print(threading.enumerate())
            print(c.value)
        else:
            print(threading.enumerate())


    三、锁的应用场景

    • 锁适用于访问和修改同一个共享资源的时候,即读写同一个资源的时候,如果全部都是读取同一个共享资源需要锁吗,不需要,因为这时可以认为共享资源是不可变的,每一次读取它都是一样的值,所有不用加锁
    •  使用锁的注意事项
      • 少用锁,必要时用锁,使用了锁,多线程访问被锁的资源时,就成了串行,要么排队,要么争抢
      • 加锁时间越短越好,不需要就立即释放锁
      • 一定要避免死锁

     1、可重入锁Rlock

    • 可重入锁,是线程相关的锁,线程A获得可重复锁,并可以多次成功获取,不会阻塞,最后要在线程A中做和acquire次数相同的relea
    import threading
    import time
    
    lock = threading.RLock()
    print(lock.acquire())
    print('-------------------------')
    
    print(lock.acquire(blocking=False))
    print(lock.acquire())
    print(lock.acquire(timeout=3.55))
    print(lock.acquire(blocking=False))
    
    lock.release()
    lock.release()
    lock.release()
    lock.release()
    lock.release()


    四、Condition

    • 构造方法Condition(lock=None),可以传入一个Lock或RLock对象,默认是RLock
    • Conditon用于生产者,消费者模型,为了解决生产者消费者速度匹配问题
    •  acquire(*args) : 获取锁
    •  wait(self,time=None) : 等待或超市
    •  notify(n=1) : 唤醒至多指定数目个数的等待线程,没有等待的线程就没有任何操作
    •  notif_all() : 唤醒所有等待的线程
    举例1:  消费者消费速度大于生产者生成速度
    
    
        from threading import Thread, Lock, Event, Condition
        import time, random
        import logging
    
        FORAMT = '%(asctime)s %(threadName)s %(thread)d %(message)s'
        logging.basicConfig(format=FORAMT, level=logging.INFO)
    
        # 此例只是为了演示,不考虑线程安全问题
        class Dispatcher:
            def __init__(self):
                self.data = None
                self.event = Event()  #event只是为了使用方便,与逻辑无关
                self.cond = Condition()
    
            def produce(self, total):
                for _ in range(total):
                    data = random.randint(0,100)
                    with self.cond:
                        logging.info(data)
                        self.data = data
                        self.cond.notify_all()   #  通知消费者消费,启动所有线程
                    self.event.wait(1)  # 模拟产生数据速度
                self.event.set()
    
            def consume(self):
                while not self.event.is_set():
                    with self.cond:   # 消费者等待
                        self.cond.wait()
                        logging.info("received {}".format(self.data))
                        self.data = None
                    self.event.wait(0.5)  #模拟消费的速度
    
        d = Dispatcher()
        p = Thread(target=d.produce, args=(10,), name='prodcer')
        c = Thread(target=d.consume, name='consumer')  
        c.start()
        p.start()
    
        消费者等待数据,如果生产者准备好了会通知消费者消费,省得消费者反复来查看数据是否就绪
        
        Condition用于生产者消费者模型中,解决生产者消费者速度匹配的问题
        采用了通知机制,非常有效率
        使用方式,使用Condition,必须先acquire,用完了要release,因为内部使用了锁,默认使用了RLock锁,
        最好方式是使用with上下文管理
        消费者wait,等待通知,生产者生产好消息,对消费者发通知,可以使用notify或者notify_all方法


    五、线程同步Barrier

    • 有人翻译成栅栏,建设理解成屏障,可以想象成路障,道闸
    • Barrier(parties, action=None, timeout=None): 构建Barrier对象,指定参与方数目,timeout是wait方法未指定超时的默认值
    •  n_waiting : 当前在屏障中等待的线程数
    •  parties : 各方数,就是需要多少个等待
    •  wait(timeout=None) : 等待通过屏障,返回0到线程数-1的整数,每个线程返回不同,如果wait方法设置了超时,并超时发送,屏障将处于broken状态

    1、Barrier实例

    •  broken :如果屏障处于打破的状态,返回True
    •  abort() : 将屏障置于broken状态,等待中的线程或者调用等待方法的线程中都会抛出BrokenBarrierError异常,直到reset方法来恢复屏障
    •  reset(): 恢复屏障,重新开始拦截
    import threading
        import logging
    
        #输出格式定义
        FORMAT = '%(asctime)-15s	 [%(threadName)s, %(thread)8d] %(message)s'
        logging.basicConfig(level=logging.INFO, format=FORMAT)
    
        def worker(barrier:threading.Barrier):
            logging.info('waiting for {} threads'.format(barrier.n_waiting))
            try:
                barrier_id = barrier.wait()
                logging.info('after barrier {}'.format(barrier_id))
            except threading.BrokenBarrierError:
                logging.info('Broken Barrier')
    
        barrier = threading.Barrier(3)  #设置拦截线程数
    
        for x in range(3):
            threading.Thread(target=worker, name='worker-{}'.format(x), args=(barrier,)).start()
    
        logging.info('started')
    
    从运行结果看出:
        所有线程冲到了Barrier前等待,直到到达parties的数目,屏障打开,所有线程停止等待,继续执行
        再有线程wait,屏障就绪等到到达参数方数目
        举例 ,赛马比赛所有马匹就位,开闸
        
    举例2:
    
        import threading
        import logging
    
        #输出格式定义
        FORMAT = '%(asctime)-15s	 [%(threadName)s, %(thread)8d] %(message)s'
        logging.basicConfig(level=logging.INFO, format=FORMAT)
    
        def worker(barrier:threading.Barrier):
            logging.info('waiting for {} threads'.format(barrier.n_waiting))
            try:
                barrier_id = barrier.wait()
                logging.info('after barrier {}'.format(barrier_id))
            except threading.BrokenBarrierError:
                logging.info('Broken Barrier run')
    
        barrier = threading.Barrier(3)
    
        for x in range(0,9):
            if x == 2:
                barrier.abort()
            if x == 6:
                barrier.reset()
            threading.Event().wait(1)
            threading.Thread(target=worker, name='worker-{}'.format(x), args=(barrier,)).start()
    
        logging.info('started')
    
        上例中等待了2个,屏障就被break了,waiting的线程抛了BrokenBarrierError异常
        新wait的线程也抛异常,直到屏障恢复,才继续按照parties数目要求继续拦截线程
        


    2、wait方法超时实例

    • 如果wait方法超时发生,屏障将处于broken状态,直到reset
    import threading
        import logging
    
        #输出格式定义
        FORMAT = '%(asctime)-15s	 [%(threadName)s, %(thread)8d] %(message)s'
        logging.basicConfig(level=logging.INFO, format=FORMAT)
    
        def worker(barrier:threading.Barrier, i:int):
    
            logging.info('waiting for {} threads'.format(barrier.n_waiting))
            try:
                logging.info(barrier.broken)  # 是否broken
                if i < 3:
                    barrier_id = barrier.wait(1)  # 超时后,屏障broken
                else:
                    if i == 6:
                        barrier.reset() #恢复屏障
                    barrier_id = barrier.wait(1)
                logging.info('after barrier {}'.format(barrier_id))
            except threading.BrokenBarrierError:
                logging.info('Broken Barrier run')
    
        barrier = threading.Barrier(3)
    
        for x in range(0,9):
    
            threading.Event().wait(2)
            threading.Thread(target=worker, name='worker-{}'.format(x), args=(barrier,x)).start()
    
        logging.info('started')


    3、Barrier应用

    • 所有线程都必须初始化完成,才能继续工作,例如运行前加载数据,检查,如果这些工作没有完成,就开始运行,将不能正常工作
    • 10个线程做10种工作准备,每个线程负责一种工作,只有这10个线程都完成后,才能继续工作,先完成的要等待后完成的线程

    六、semaphore信号量

    • 和Lock很像,信号量对内部维护一个到计数器,每一次acquire都会减1,
    • 当acquire方法发现计数为0就阻塞请求的线程,直到其他线程对信号量release后,计数大于0,恢复阻塞线程
    • Semaphore(value=1): 构造方法,value小于0,抛出异常ValueError异常
    • acquire(blocking=True,time=None) : 获取信号量,计数器减1,获取成功返回True
    • release() : 释放信号量,计数器加1
    • 计数器永远不会低于0,因为acquire的时候,发现是0,都会被阻塞
    import threading
        import logging
        import time
    
        #输出格式定义
        FORMAT = '%(asctime)-15s	 [%(threadName)s, %(thread)8d] %(message)s'
        logging.basicConfig(level=logging.INFO, format=FORMAT)
    
        def worker(barrier:threading.Semaphore):
            logging.info('in sub thread')
            logging.info(s.acquire())
            logging.info('sub thread over')
    
        #信号量
        s = threading.Semaphore(3)
        logging.info(s.acquire())
        logging.info(s.acquire())
        logging.info(s.acquire())
    
        threading.Thread(target=worker, args=(s,)).start()
        time.sleep(2)
    
        logging.info(s.acquire(False))
        logging.info(s.acquire(timeout=3))   #阻塞
    
        #释放
        logging.info('released')
        s.release()
    
    
    
    
    应用举例:
        实现一个简单的连接池,连接池应该有容量,有一个工厂方法可以获取连接,能够被不要的连接返回,供其他调用者使用
    
    
        import threading
        import logging
        import random
    
        #输出格式定义
        FORMAT = '%(asctime)-15s	 [%(threadName)s, %(thread)8d] %(message)s'
        logging.basicConfig(level=logging.INFO, format=FORMAT)
    
        class Conn:
            def __init__(self, name):
                self.name = name
    
            def __repr__(self):
                return self.name
    
        class Pool:
            def __init__(self, count:int):
                self.count = count
                #池中是连接对象列表
                self.pool = [self._connect("conn-{}".format(x)) for x in range(self.count)]
                self.semaphore = threading.Semaphore(count)
    
            def _connect(self, conn_name):
                #返回一个名称
                return Conn(conn_name)
    
            def get_conn(self):
                #从池中拿走一个连接
                self.semaphore.acquire()
                conn = self.pool.pop()
                return conn
    
            def return_conn(self, conn:Conn):
                #向池中添加一个连接
                self.pool.append(conn)
                self.semaphore.release()
    
        # 连接池初始化
        pool = Pool(3)
    
        def worker(pool:Pool):
            conn = pool.get_conn()
            logging.info(conn)
            #模拟使用了一段时间
            threading.Event().wait(random.randint(1,4))
            pool.return_conn(conn)
    
        for i in range(6):
            threading.Thread(target=worker, name="worker-{}".format(i), args=(pool,)).start()
    
    
        上例中,使用信号量解决资源有限的问题
        如果池中有资源,请求者获取资源时信号量减1,拿走资源,当请求超过资源数,请求者只能等待,
        当使用者用完归还资源后信号量加1,等待线程拿到就可以唤醒拿走资源
        


    1、信号量和锁

    •  锁,只允许同一个时间一个线程独占资源,它是特殊的信号量,即信号量计数器初值为1
    •  信号量,允许多个线程访问共享资源,但是这个共享资源数量有限
    •  锁可以看做是特殊的信号量

    2、数据结构和GIL

    •  Queue :标准库queue模块,提供FIFO的Queue,LIFO的队列,优先队列
    •  Queue类是线程安全的,适用于多线程间安全的交换数据,内部使用了Lock和Condition

    3、GIL全局解释器锁

    • Cpython在解释器进程级别有一把锁,叫做GIL全局解释器
    • GIL保证CPython进程中,只有一个线程执行字节码,甚至是在多核CPU的情况下,也是如此
    •  CPython中,IO密集型,由于线程阻塞,就会调度其他线程,CPU密集型,当前线程可能会连续的获得GIL,导致其他线程几乎无法使用CPU
  • 相关阅读:
    Nodejs与ES6系列3:generator对象
    Nodejs与ES6系列2:Promise对象
    Nodejs与ES6系列1:变量声明
    Nodejs与ES6系列4:ES6中的类
    angular单元测试与自动化UI测试实践
    javascript 异步模块加载 简易实现
    javascript模式 (3)——工厂模式和装饰模式
    TP手册学习第四内置天
    TP手册学习第三天
    tp5命令行基础
  • 原文地址:https://www.cnblogs.com/jiangzuofenghua/p/11450668.html
Copyright © 2020-2023  润新知