线程同步和锁

线程同步

概念
线程同步,线程间协同,通过某种技术,让一个线程访问某些数据时,其他线程不能访问这些数据,直到该线程完成对数据的操作

Event

Event事件,是线程间通信机制中最简单的实现,使用一个内部的标记falg,通过flag的True或False的变化来进行操作

名称含义
set()标记设置为True
clear()标记设置为False
is_set()标记是否为True
wait(timeout=None)设置等待标记为True的时长,None为无限等待,等到返回True,未等到超时了返回False

练习
老板雇佣了一个工人,让他生产杯子,老板一直等着这个工人,直到生产了10个杯子

from threading import Event,Thread
import logging
import time

FORMAT = '%(asctime)s %(threadName)s %(thread)s %(message)s'
logging.basicConfig(format=FORMAT, level=logging.INFO)

def boss(event:Event):
    logging.info('boss')
    event.wait()
    logging.info('good')

def worker(event:Event,count=10):
    logging.info('working')
    cups = []
    while True:
        logging.info('make 1 cup')
        time.sleep(0.1)
        cups.append(1)
        if len(cups) >= count:
            event.set()
            break
    logging.info(cups)

event = Event()

b = Thread(target=boss, name='boss', args=(event,))
w = Thread(target=worker, name='worker', args=(event,))
b.start()
w.start()

总结
使用同一个Event对象标记flag
谁wait就是等到flag变为True,或等到超时返回False.不限制等待的个数

wait的使用

from threading import Event,Thread
import logging

FORMAT = '%(asctime)s %(threadName)s %(thread)s %(message)s'
logging.basicConfig(format=FORMAT, level=logging.INFO)

def worker(event:Event,interval):
    while not event.wait(interval):
        logging.info('do sth')

event = Event()

Thread(target=worker, name='worker', args=(event,3))

event.wait(5)
event.set()

print('===end===')

定时器Timer/延迟执行
threading.Timer继承自Thread,这个类用来定义延迟多久后执行一个函数

class threading.Timer(interval,function,args=None,kwargs=None)
start方法执行之后,Timer对象会处于等待状态,等待了interval秒之后,开始执行function函数的

import threading
import logging
import time

FORMAT = '%(asctime)s %(threadName)s %(thread)s %(message)s'
logging.basicConfig(format=FORMAT, level=logging.INFO)

def worker():
    logging.info('in worker')
    time.sleep(2)

t = threading.Timer(3,worker)
t.setName('timer')

#t.cancel()
t.start()
#t.cancel()

while True:
    print(threading.enumerate())
    time.sleep(1)

上例代码工作线程早就启动了,只不过是在工作线程中掩饰4秒才执行worker函数

Timer是线程Tread的子类,Timer实例内部提供了一个finshed属性,该属性是Event对象.cancal方法,本质上是在worker函数执行前对finshed属性set方法操作,从而跳过了worker函数执行,达到了取消的效果

总结
Timer是线程Thread的子类,就是线程类,具有线程的能力和特征。
它的实例是能够延时执行目标函数的线程,在真正执行目标函数之前,都可以cancel它。
cancel方法本质使用Event类实现。这并不是说,线程提供了取消的方法。

Lock

Lock
锁,一旦线程获得锁,其它试图获取锁的线程将被阻塞
锁:凡是存在共享资源争抢的地方都可以使用锁,从而保证只有一个使用者可以完全使用这个资源

名称含义
acquire(blocking=True,timeout=1默认阻塞,阻塞可以设置超时时间.非阻塞时,timeout禁止设置.成功获取锁,返回True,否则返回False
release()释放锁.可以从任何线程调用释放.以上锁的锁,会被重置为unlocked未上锁的锁上调用,抛RuntimeError异常

锁的基本使用

import threading

lock = threading.Lock()

lock.acquire()
print('-'*30)

# threading.Timer(5, lambda l: l.release(), (lock,)).start()
# lock.release()

def worker():
    print('worker start', threading.current_thread().name)
    lock.acquire()
    print('worker over', threading.current_thread().name)

for i in range(10):
    threading.Thread(target=worker,name='{}'.format(i)).start()

print('-'*30)

while True:
    cmd = input('>>').strip()
    if cmd =='r':
        lock.release()
    elif cmd == 'quit':
        break
    else:
        print(threading.enumerate())

上例可以看出不管在哪一个线程中,只要对一个已经上锁的锁阻塞请求,该线程就会阻塞

练习
订单要求生产1000个杯子,组织10个工人生产,请忽略老板,关注工人生成杯子

from threading import Event,Thread
import logging
import time

FORMAT = '%(asctime)s %(threadName)s %(thread)s %(message)s'
logging.basicConfig(format=FORMAT, level=logging.INFO)

cups = []

def worker(count=10):
    logging.info('working')
    while len(cups) < count:
        time.sleep(0.001)
        cups.append(1)
    logging.info('I finished my job. cups = {}'.format(len(cups)))

for i in range(1,11):
    t = Thread(target=worker,name='worker-{}'.format(i),args=(100,))
    t.start()

从上例的运行结果看出,多线程调度,导致了判断失效

上例的锁实现

from threading import Thread,Lock
import threading
import time

FORMAT = '%(asctime)s %(threadName)s %(thread)s %(message)s'
logging.basicConfig(format=FORMAT, level=logging.INFO)

cups = []
lock = Lock()

def worker(count=10):
    logging.info('working')
    flag = False
    while True:
        lock.acquire() #获取锁

        if len(cups) >= count:
            flag = True
        #lock.release() # 1

        time.sleep(0.001) #为了看出线程切换效果
        if not flag:
            cups.append(1)
        lock.release() # 2 这里释放锁

        if flag:
            break
        #lock.release() # 3

    logging.info('I finshed. cups = {}'.format(len(cups)))

for _ in range(10):
    Thread(target=worker,args=(1000,)).start()

思考
上面代码中,共有3处可以释放锁.请问,放在何处合适?
假设位置1的lock.release()合适,分析如下:
有一个时刻,在某一个线程中len(cups)正好是999,flag=True,释放锁,正好线程被打断。另一个线程判断发现 也是999,flag=True,可能线程被打断。可能另外一个线程也判断是999,flag也设置为True。这三个线程只要继 续执行到cups.append(1),一定会导致cups的长度超过1000的.

假设位置2的lock.release()合适,分析如下:
在某一个时刻len(cups),正好是999,flag=True,其它线程试图访问这段代码的线程都阻塞获取不到锁,直到当 前线程安全的增加了一个数据,然后释放锁。其它线程有一个抢到锁,但发现已经1000了,只好break打印退出。 再其它线程都一样,发现已经1000了,都退出了。 所以位置2 释放锁 是正确的。 但是我们发现锁保证了数据完整性,但是性能下降很多。

上例中位置3,if flag:break是为了保证位置2的release方法被执行,否则,就出现了死锁,得到锁的永远没有释放锁.

计算器类,可以加 可以减

from threading import Thread,Lock
import threading
import time

class Counter:
    def __init__(self):
        self._val = 0

    @property
    def value(self):
        return self._val

    def inc(self):
        self._val += 1

    def dec(self):
        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 = 10 
for i in range(c1):
    Thread(target=run,args=(c,c2)).start()

print(c.value)

c1取10 100 1000看看
c2取10 100 1000看看

self._val += 1或self._val-=1在线程执行的时候,有可能被打断

加锁,解锁
一般来说,加锁就需要解锁,但是加锁后解锁前,还要有一些代码执行,就有可能抛异常,一旦出现异常,锁无法释放,但是当前线程可能因为这个异常被终止了,这也产生了死锁

加锁 解锁常用语句:

  1. 使用try…finally语句保证锁的释放
  2. with上下文管理,锁对象支持上下文管理

改造Couter类,如下

from threading import Thread,Lock
import threading
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 = 1000
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)
        break
    else:
        print(threading.enumerate())

print(c.value)这一句在主线程中,很早就执行了,退出条件是,只剩下主线程的时候
这样的改造后,代码可以保证最后得到的value值一定是0

锁的应用场景

锁适用于访问和修改同一个共享资源的时候,即读写同一个资源的时候
如果全部都是读取同一个共享资源需要锁吗?
不需要。因为这时可以认为共享资源是不可变的,每一次读取它都是一样的值,所以不用加锁

使用锁的注意事项:

  • 少用锁,必要时用锁。使用了锁,多线程访问被锁的资源时,就成了串行,要么排队执行,要么争抢执行
    举例,高速公路上车并行跑,可是到了省界只开放了一个收费口,过了这个口,车辆依然可以在多车道 上一起跑。过收费口的时候,如果排队一辆辆过,加不加锁一样效率相当,但是一旦出现争抢,就必须 加锁一辆辆过。注意,不管加不加锁,只要是一辆辆过,效率就下降了。

  • 加锁时间越短越好,不需要就立即释放锁

  • 一定要避免死锁

不使用锁,有了效率,但是结果是错的
使用了锁,效率但是结果是对的
我们还是要正确的结果

非阻塞锁使用

import threading
import logging
import time

FORMAT = '%(asctime)s %(threadName)s %(thread)-10d %(message)s'
logging.basicConfig(level=logging.INFO, format=FORMAT)

lock = threading.Lock()

def worker(l:threading.Lock):
    while True:
        flag = l.acquire(False)
        if flag:
            logging.info('do something') #为了显示效果,没有释放锁
        else:
            logging.info('try again')
            time.sleep(1)

for i in range(5):
    threading.Thread(target=worker, name='worker={}'.format(i),args=(lock,)).start()

可重入锁RLock
可重入锁,是线程相关的锁
线程A获得可重复锁,并可以多次获取,不会阻塞.最后要在线程A中做和acquire次数相同的release

import threading
import logging
import time

FORMAT = '%(asctime)s %(threadName)s %(thread)-10d %(message)s'
logging.basicConfig(level=logging.INFO, format=FORMAT)

lock = threading.RLock()


print(lock.acquire())
print(lock.acquire())
print(lock.acquire())
print(lock.acquire())


lock.release()
lock.release()
lock.release()
lock.release()
# lock.release() #release多了抛异常

print('main thread {}'.format(threading.main_thread().ident))
print('lock in main thread {}'.format(lock))

print(lock.acquire(blocking=False))
lock.release()
print('-'*30)

print(lock.acquire())

def sub(l):
    print('{}: {}'.format(threading.current_thread(), l.acquire())) # 阻塞
    print('{}: {}'.format(threading.current_thread(), l.acquire()))
    print('lock in sub thread {}'.format(lock))
    l.release()
    print('release in sub 1')
    l.release()
    print('release in sub 2')

threading.Timer(2,sub,(lock,)).start()

print('in main thread, {}'.format(lock.acquire())) # count = 2
lock.release()
time.sleep(5)
print('release lock in main thread~~~~~~~~', end='\n\n')
lock.release() # count = 0

可重入锁

  • 与线程相关,可在一个线程中获取锁,并可继续在同一线程中不阻塞多次获取锁
  • 当锁未释放完,其它线程获取锁就会阻塞,直到当前持有锁的线程释放完锁
  • 锁都应该使用完后释放。可重入锁也是锁,应该acquire多少次,就release多少次

Condition
构造方法Condtion(lock=None),可以传入一个Lock或RLock对象,默认是RLock

acquire(*args)获取锁
wait(self,timeout=None)等待或超时
notify(n=1)唤醒至多指定数目的等待的线程,没有等待的线程就没有任何操作
notify_all()唤醒所有等待的线程

Condition基本使用

import logging
import threading
import time

FORMAT = '%(asctime)s %(threadName)s %(thread)-10d %(message)s'
logging.basicConfig(level=logging.INFO, format=FORMAT)

def boss(c:threading.Condition):
    logging.info("I'm waiting for U")
    with c:
        c.wait()
    logging.info("Good Job")

def worker(c:threading.Condition,count=1000):
    print("I'm working {}".format(threading.current_thread().name))
    with c:
        cups = []
        while len(cups) < count:
            time.sleep(0.001)
            cups.append(1)

        print("finishd.{} cups = {}".format(threading.current_thread().name,len(cups)))
        c.notify_all()
        c.notify(1)

cond = threading.Condition()

b1 = threading.Thread(target=boss, args=(cond,), name='boss1')
b2 = threading.Thread(target=boss, args=(cond,), name='boss2')
b1.start()
b2.start()

w = threading.Thread(target=worker, args=(cond,), name='worker')
w.start()

Condition用于生产者 消费者模型,为了解决生产者消费者速度匹配问题
先看一个例子,消费者消费速度大于生产者生产速度

Condtion总结

Condition用于生产者消费者模型中,解决生产者消费者速度匹配的问题。
采用了通知机制,非常有效率。

使用方式
使用Condition,必须先acquire,用完了要release,因为内部使用了锁,默认使用RLock锁,最好的方式是使用with上下文
消费者wait,等待通知
生产者生产好消息,对消费者发通知,可以使用notify活着notify_all方法.

semaphore信号量
和Lock很像,信号量对象内部维护一个倒计数器,每一次acquire都会减1,当acquire方法发现计数为0就阻塞请求 的线程,直到其它线程对信号量release后,计数大于0,恢复阻塞的线程。

名称含义
Semaphore(value=1)构造方法.value小于0,抛ValueError异常
acquire(blocking=True,timeout=None)获取信号量,计数器减1,获取成功返回True
release()释放信号量,计数器加1

计数器永远不会低于0,因为acquire的时候,发现是0,都会被阻塞

import logging
from threading import Thread,Semaphore
import time

FORMAT = '%(asctime)s %(threadName)s %(thread)-10d %(message)s'
logging.basicConfig(level=logging.INFO, format=FORMAT)

def worker(s:Semaphore):
    logging.info("in worker thread")
    logging.info(s.acquire())
    logging.info('worker thread over')


#信号量
s = Semaphore(3)
logging.info(s.acquire())
print(s._value)
logging.info(s.acquire())
print(s._value)
logging.info(s.acquire())
print(s._value)


Thread(target=worker,args=(s,)).start()

time.sleep(2)

logging.info(s.acquire(False))
logging.info(s.acquire(timeout=3))

logging.info("abc")
s.release()

release方法超界

import logging
import threading

sema = threading.Semaphore(3)
logging.warning(sema.__dict__)

for i in range(3):
    sema.acquire()
    logging.warning('~~~~~~~~')
    logging.warning(sema.__dict__)

for i in range(4):
    sema.release()
    logging.warning(sema.__dict__)

for i in range(3):
    sema.acquire()
    logging.warning('~~~~~~~~')
    logging.warning(sema.__dict__)
    sema.acquire()
    logging.warning('~~~~~~~~')
    logging.warning(sema.__dict__)

从上例输出结果可以看出,竟然内置计数器达到了4,这样实际上超出我们的最大值,需要解决这个问题.

BoundedSemaphore类
有界的信号量,不允许使用release超出初始值的范围,否则,抛出ValueError异常。
将上例的信号量改成有界的信号量试一试

应用举例
连接池
因为资源有限,且开启一个连接成本高,所以,使用连接池
一个简单的连接池
连接池应该有容量(总数),有一个工厂方法可以获取连接,能够把不用的连接返回,供其他调用者使用.

class Conn:
    def __init__(self,name):
        self.name = name

class Pool:
    def __init__(self,count:int):
        self.count = count
        self.pool = [self._connect('conn-{}'.format(i)) for i in range(count)]

    def _connect(self,conn_name):
        return Conn(conn_name)

    def get_conn(self):
        if len(self.pool)>0:
            return self.pool.pop()

    def return_conn(self,conn:Conn):
        self.pool.append(conn)

真正的连接池的实现比上面的例子要复杂的多,这里只是简单的一个功能的实现.
本例中,get_conn()方法在多线程的时候有线程安全问题。
假设池中正好有一个连接,有可能多个线程判断池的长度是大于0的,当一个线程拿走了连接对象,其他线程再来 pop就会抛异常的。如何解决?

加锁,在读写的地方加锁
使用信号量Semaphore

import random
import threading
import logging
import time

FORMAT = '%(asctime)s %(threadName)s %(thread)-10d %(message)s'
logging.basicConfig(level=logging.INFO, format=FORMAT)

class Conn:
    def __init__(self,name):
        self.name = name

class Pool:
    def __init__(self,count:int):
        self.count = count
        #池中提前放着备用
        self.pool = [self._connect('conn-{}'.format(i)) for i in range(count)]
        self.semaphore = threading.Semaphore(count)

    def _connect(self,conn_name):
        #创建连接的方法,返回一个连接对象
        return Conn(conn_name)

    def get_conn(self):
        #从池中拿走一个连接
        logging.info('get---------')
        self.semaphore.acquire()
        logging.info('------------')
        return self.pool.pop()

    def return_conn(self,conn:Conn):
        #向池中返回一个连接对象
        logging.info('return------------')
        self.pool.append(conn)
        self.semaphore.release()
#初始化连接池
pool = Pool(3)

def worker(pool:Pool):
    conn = pool.get_conn()
    logging.info(conn)
    #模拟使用了一段时间
    time.sleep(random.randint(1,5))
    pool.return_conn(conn)

for i in range(6):
    threading.Thread(target=worker, name='worker-{}'.format(i), args=(pool,)).start()

上例中,使用信号量解决资源有限的问题。
如果池中有资源,请求者获取资源时信号量减1,拿走资源。当请求超过资源数,请求者只能等待。当使用者用完 归还资源后信号量加1,等待线程就可以被唤醒拿走资源。
注意:这个连接池的例子不能用到生成环境,只是为了说明信号量使用的例子,连接池还有很多未完成功能

问题

self.conns.append(conn) 这一句有哪些问题考虑?
一. 边界问题分析
return_conn方法可以单独执行,有可能多归还连接,也就是会多release,所以,要用有界信号量 BoundedSemaphore类。
这样用有界信号量修改源代码,保证如果多return_conn就会抛异常

self.pool.append(conn)
self.semaphore.release()

假设一种极端情况,计数器还差1就归还满了,有三个线程A、B、C都执行了第一句,都没有来得及release,这时 候轮到线程A release,正常的release,然后轮到线程C先release,一定出问题,超界了,直接抛异常。 因此信号量,可以保证,一定不能多归还。
如果归还了同一个连接多次怎么办,重复很容易判断。
这个程序还不能判断这些连接是不是原来自己创建的,这不是生成环境用的代码,只是简单演示。

二.正常使用分析
正常使用信号量,都会先获取信号量,然后用完归还。
创建很多线程,都去获取信号量,没有获得信号量的线程都阻塞。能归还的线程都是前面获取到信号量的线程,其 他没有获得线程都阻塞着。非阻塞的线程append后才release,这时候等待的线程被唤醒,才能pop,也就是没有 获取信号量就不能pop,这是安全的。
经过上面的分析,信号量比计算列表长度好,线程安全.

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值