Python中线程同步与线程锁「建议收藏」

Python中线程同步与线程锁「建议收藏」文章目录Python中线程同步与线程锁线程同步threading.Event对象threading.Timer定时器,延迟执行threading.Lock锁可重入锁RLockCondition条件锁,等待通知therading.Semaphore信号量threading.BoundedSemaphore有界信号量Python中线程同步与线程锁线程同步概念*线程同步,线程间协同,通过某种技…

大家好,又见面了,我是你们的朋友全栈君。

线程同步与线程锁

线程同步

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

1.threading.Event对象

  • Event事件,是线程间通信机制中最简单的实现,使用一个内部的标记flag,通过flag的True或False的变化 来进行操作
名称 含义
event.set() 标记设置为True
event.clear() 标记设置为False
event.is_set() 标记是否为True
event.wait(timeout=None) 设置等待标记为True的时长,None为无限等待。等到返回True,未等到超时了返回False
  • 老板雇佣了一个工人,让他生产杯子,老板一直等着这个工人,直到生产了10个杯子
import threading
import time
import logging

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

def worker(event:threading.Event,count = 10):
    logging.info("我是worker工作线程")
    cups = []
    while True:
        logging.info("制作了一个 cup")
        time.sleep(0.2)
        cups.append(1)
        if len(cups)>=count:
            event.set()
            break
    logging.info("制作完成:{}".format(cups))

def boss(event:threading.Event):
    logging.info("我是boss")
    event.wait()
    logging.info("Good Job")

event = threading.Event()
b = threading.Thread(target=boss,args=(event,))
w = threading.Thread(target=worker,args=(event,))
b.start()
w.start()

threading2_001

  • 使用同一个Event对象的标记flag。

  • 谁wait就是等到flag变为True,或等到超时返回False。不限制等待的个数。

  • wait的使用

from threading import Thread,Event
import logging

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

def worker(event:Event,interval:int):
    while not event.wait(interval):
        logging.info("没有等到。。")

e = Event()
Thread(target=worker,args=(e,1)).start()

e.wait(5)
e.set()

print("======end========")

threading2_002

2.threading.Timer定时器,延迟执行

方法 含义
Timer.cancel() 取消定时器,(定时器为执行函数时可以取消,在函数执行中无法取消)
Time.start() 启动定时器
  • threading.Timer继承自Thread,这个类用来定义延迟多久后执行一个函数。
  • class threading.Timer(interval, function, args=None, kwargs=None)
    1. interval #多少时间后执行function函数
    2. function #需要执行的函数
  • start方法执行之后,Timer对象会处于等待状态,等待了interval秒之后,开始执行function函数的。
  • Timer是线程Thread的子类,Timer实例内部提供了一个finished属性,该属性是Event对象。
  • cancel方法,本质上 是在worker函数执行前对finished属性set方法操作,从而跳过了worker函数执行,达到了取消的效果。
from threading import Timer
import logging
import time

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

def worker():
    logging.info("in worker")
    time.sleep(5)
    logging.info("end in worker")

t = Timer(2,worker)
t.setName("timer1") #设置线程名称
# t.cancel() #取消定时器后,定时器不在执行
t.start()
# t.cancel() #取消定时器后,定时器不在执行
time.sleep(4) #等待4秒后,定时器已经开始执行
t.cancel() #当定时器执行后,无法取消

print("======end========")

threading2_003

3.threading.Lock锁

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

名称 含义
Lock.acquire(blocking=True,timeout=-1) 获取锁,获取成功返回True,否则返回False
当获取不到锁时,默认进入阻塞状态,直到获取到锁,后才继续。阻塞可以设置超时时间。非阻塞时,timeout禁止设置。如果超时依旧未获取到锁,返回False。
Lock.rease() 释放锁,可以从任何线程调用释放。
已上锁的锁,会被设置为unlocked。如果未上锁调用,会抛出RuntimeError异常。
import threading
import sys
import time

def print(*args):
    sys.stdout.write(" ".join(map(str,args))+"\n")

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

lock = threading.Lock()
lock.acquire()
print(" -"*30)
for i in range(5):
    threading.Thread(target=worker,args=(lock,),name="w{}".format(i)).start()

print("- "* 30)
while True:
    time.sleep(0.1)
    cmd = input(">>").strip()
    if cmd == "r":
        lock.release()
    elif cmd == "q":
        break
    else:
        print(threading.enumerate())

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

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

  • 加锁、解锁常用语句:

    1. 使用try…finally语句保证锁的释放
    2. with上下文管理,锁对象支持上下文管理
  • 计数器类,可加,可减。

import threading
import sys
import time

def print(*args):
    sys.stdout.write(" ".join(map(str,args))+"\n")

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

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

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

    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):
    threading.Thread(target=run,args=(c,c2)).start()

for k in threading.enumerate():
    if k.name != "MainThread":
        k.join()

print(c.value)
  • 锁的应用场景
    锁适用于访问和修改同一个共享资源的时候,即读写同一个资源的时候。

  • 使用锁的注意事项:

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

import threading
import sys
import time

def print(*args):
    sys.stdout.write(" ".join(map(str,args))+"\n")

def worker(lock:threading.Lock):
    while True:
        if lock.acquire(False):
            print("do something.")
            time.sleep(1)
            lock.release()
            break
        else:
            print("try again")
            time.sleep(1)

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

threading2_005

4.可重入锁RLock

  • 可重入锁,是线程相关的锁
  • 线程A获得可重复锁,并可以多次成功获取,不会阻塞。最后要在线程A中做和acquire次数相同的release
import threading
import sys
import time

def print(*args):
    sys.stdout.write(" ".join(map(str,args))+"\n")

def fib(num,rlock:threading.RLock):
    with rlock:
        if num<3:
            return 1
        return fib(num-1,rlock)+fib(num-2,rlock)

def work(num,rlock):
    print(fib(num,rlock))

rlock = threading.RLock()
for i in range(1,10):
    threading.Thread(target=work,args=(i,rlock)).start()

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

5.Condition条件锁,等待通知

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

名称 含义
Condition.acquire(self,*args) 获取锁
Condition.wait(self,timeout=None) 等待通知,timeout设置超时时间
Condition.notify(self,n=1) 唤醒至多指定数目个数的等待的线程,没有等待的线程就没有任何操作
Condition.notify_all(self) 唤醒所有等待的线程
  • 每个线程都可以通过Condition获取已把属于自己的锁,在锁中可以等待其他进程的同级锁的通知。当获取到同级锁的通知后,会停止等待。

  • 当使用Condition(lock=Lock())初始化锁时,锁只能一级等待,不能出现多级等待。

  • 简单示例:

import threading
import time

def work(cond:threading.Condition):
    with cond:
        print("开始等待")
        cond.wait()
        print("等到了")

cond = threading.Condition()
# cond = threading.Condition(threading.Lock())
threading.Thread(target=work,args=(cond,)).start()
threading.Thread(target=work,args=(cond,)).start()

with cond:
    with cond:
        time.sleep(1)
        print("开始释放二级等待")
        print(cond.notifyAll())
    time.sleep(2)
    print("开始释放一级等待")
    cond.notifyAll()

threading2_006

  • 广播模式示例:
from threading import Thread,Condition,Lock
import time
import logging
import random

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

class Dispachter:
    def __init__(self):
        self.data = None
        self.cond = Condition(lock=Lock())

    #生成者
    def produce(self,total):
        for _ in range(total):
            data = random.randint(1,100)
            with self.cond:
                logging.info("生产了一个数据:{}".format(data))
                self.data = data
                self.cond.notify(1)
            time.sleep(1) #模拟生成数据需要耗时1秒

    #消费者
    def consume(self):
        while True:
            with self.cond:
                self.cond.wait() #等待
                data = self.data
                logging.info("消费了一个数据 {}".format(data))
                self.data = None

d = Dispachter()
p = Thread(target=d.produce,name="producer",args=(10,))

# 增加消费者
for i in range(5):
    c = Thread(target=d.consume,name="consumer{}".format(i))
    c.start()

p.start()

threading2_007

上面例子中演示了生产者生产一个数据,就通知一个消费者消费。

  • Condition总结
    Condition用于生产者消费者模型中,解决生产者消费者速度匹配的问题。采用了通知机制,非常有效率。
  • 使用方式
    使用Condition,必须先acquire,用完了要release,因为内部使用了锁,默认使用RLock锁,最好的方式是使用 with上下文。
    消费者wait,等待通知。
    生产者生产好消息,对消费者发通知,可以使用notify或者notify_all方法。

6.therading.Semaphore信号量

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

名称 含义
Semaphore(value=1) 构造方法。value为初始信号量。value小于0,抛出ValueError异常
Semaphore.acquire(self,blocking=True,timeout=None) 获取信号量,技术器减1,即_value的值减少1。如果_value的值为0会变成阻塞状态。获取成功返回True
Semaphore.release(self) 释放信号量,计数器加1。即_value的值加1
Semaphore._value 信号量,当前信号量
  • 注意
    1. 计数器永远不会低于0,因为acquire的时候,发现是0,都会被阻塞。
    2. 信号量没有做超界限制
from threading import Semaphore

s =Semaphore(3)
print(s._value)
s.release() #会增加信号量
print(s._value) #可以看出没有做信号量上线控制
print("----------------")
print(s.acquire())
print(s._value)
print(s.acquire())
print(s._value)
print(s.acquire())
print(s._value)
print(s.acquire())
print(s._value)
print(s.acquire()) #当信号量为0再次acquire会被阻塞
print("~~~~~~阻塞了吗?")
print(s._value)

threading2_008

  • 跨线程使用演示
from threading import Thread,Semaphore
import time
import logging

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

#定义获取信号量
def worker(s:Semaphore):
    while s.acquire():
        logging.info("被执行了一次,获取一个信号量 _value={}".format(s._value))

#释放信号量
def cunn(s:Semaphore):
    while True:
        logging.info("释放一个信号量")
        s.release()
        time.sleep(1)

s = Semaphore(3)
#创建3个线程获取信号量
for i in range(3):
    Thread(target=worker,args=(s,),name="w{}".format(i)).start()

#开启一个线程释放信号量
Thread(target=cunn,args=(s,)).start()

threading2_009

7.threading.BoundedSemaphore有界信号量

  • 有界信号量,不允许使用release超出初始值的范围,否则,抛出ValueError异常
名称 含义
BoundedSemaphore(value=1) 构造方法。value为初始信号量。value小于0,抛出ValueError异常
BoundedSemaphore.acquire(self,blocking=True,timeout=None) 获取信号量,技术器减1,即_value的值减少1。如果_value的值为0会变成阻塞状态。获取成功返回True
BoundedSemaphore.release(self) 释放信号量,计数器加1。即_value的值加1,超过初始化值会抛出异常ValueError。
BoundedSemaphore._value 信号量,当前信号量
from threading import BoundedSemaphore

bs = BoundedSemaphore(3)
print(bs._value)
bs.acquire()
bs.acquire()
bs.acquire()
print(bs._value)
bs.release()
bs.release()
bs.release()
print(bs._value)
bs.release()

threading2_010

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

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

#链接类
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.bsemaphore = BoundedSemaphore(count)

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

    #获取一个链接
    def get_conn(self):
        self.bsemaphore.acquire()
        self.pool.pop()
        logging.info("从连接池拿走了一个连接~~~~~~~")

    #归还一个连接
    def return_conn(self,conn:Conn):
        logging.info("归还了一个连接----------")
        self.pool.append(conn)
        self.bsemaphore.release()

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

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

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

  • 问题分析
  1. 边界问题
    • 假设一种极端情况,计数器还差1就归还满了,有三个线程A、B、C都执行了第一句,都没有来得及release,这时 候轮到线程A release,正常的release,然后轮到线程C先release,一定出问题,超界了,直接抛异常。 因此信号量,可以保证,一定不能多归还。
  2. 正常使用分析
    • 正常使用信号量,都会先获取信号量,然后用完归还。
    • 创建很多线程,都去获取信号量,没有获得信号量的线程都阻塞。能归还的线程都是前面获取到信号量的线程,其 他没有获得线程都阻塞着。非阻塞的线程append后才release,这时候等待的线程被唤醒,才能pop,也就是没有 获取信号量就不能pop,这是安全的。
    • 经过上面的分析,信号量比计算列表长度好,线程安全。
  • 信号量和锁
    1. 信号量,可以多个线程访问共享资源,但这个共享资源数量有限。
    2. 锁,可以看做特殊的信号量,即信号量计数器初值为1。只允许同一个时间一个线程独占资源。

总结

threading模块中的类

常用方法 含义
Event set(self) 将标记设置为True
clear(self) 将标记设置为False
is_set() 判断当前标记是否为True,是True返回True,否则返回False
相当于为返回当前标记
wait(self,timeout=None) 如果当前标记为True,立即返回True,如果当前标记为False,会产生一个阻塞,直到标记为True时返回True。timeout等待超时时间,默认为None表示无限等待。未等到超时了返回False
Time定时器,延迟执行 Timer(interval, function, args=None, kwargs=None) 实例化构造方法
1. interval #多少时间后执行function函数
2. function #需要执行的函数
cancel(self) 取消定时器
(定时器为执行函数时可以取消,在函数执行中无法取消)
start() 启动定时器
Lock锁 acquire(self,blocking=True,timeout=-1) 获取锁,获取成功返回True,否则返回False
当获取不到锁时,默认进入阻塞状态,直到获取到锁,后才继续。
阻塞可以设置超时时间。
非阻塞时,timeout禁止设置。如果超时依旧未获取到锁,返回False。
rease(self) 释放锁,可以从任何线程调用释放。
已上锁的锁,会被设置为unlocked。如果未上锁调用,会抛出RuntimeError异常。
RLock可重入锁 和Lock类似但是:
线程A获得可重复锁,并可以多次成功获取,不会阻塞。最后要在线程A中做和acquire次数相同的release
Condition Condition(self,lock=None) 构造方法,lock默认值为None表示使用RLock()锁,也可以自己传入为Lock()
acquire(self,*args) 获取锁
rease(self) 释放锁
wait(self,timeout=None) 等待通知,timeout设置超时时间。
注意:必须获取锁后才能等待通知,notify或notify_all可以发通知
notify(self,n=1) 唤醒至多指定数目个数的等待的线程,没有等待的线程就没有任何操作
notify_all(self) 唤醒所有等待的线程
Semaphore信号量 Semaphore(value=1) 构造方法。value为初始信号量。value小于0,抛出ValueError异常
acquire(self,blocking=True,timeout=None) 获取信号量,技术器减1,即_value的值减少1。
如果_value的值为0会变成阻塞状态。获取成功返回True
release(self) 释放信号量,计数器加1。即_value的值加1
`_value`属性 信号量,当前信号量
BoundedSemaphore有界信号量 BoundedSemaphore(value=1) 构造方法。value为初始信号量。value小于0,抛出ValueError异常
acquire(self,blocking=True,timeout=None) 获取信号量,技术器减1,即_value的值减少1。
如果_value的值为0会变成阻塞状态。获取成功返回True
release(self) 释放信号量,计数器加1。即_value的值加1,超过初始化值会抛出异常ValueError。
_value 信号量,当前信号量
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/152057.html原文链接:https://javaforall.cn

【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛

【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...

(0)


相关推荐

  • MySQL 如何实现递归查询?「建议收藏」

    MySQL 如何实现递归查询?「建议收藏」点击上方IT牧场,选择置顶或者星标技术干货每日送达!前言最近在做的业务场景涉及到了数据库的递归查询。我们公司用的Oracle,众所周知,Oracle自带有递归查询的功能,所以…

  • java怎么删除数组中的某个元素_js数组删除元素的方法

    java怎么删除数组中的某个元素_js数组删除元素的方法问题在Java开发中,可能会碰到需要删除数组中某个元素的场景。解决方案Javaapi中,数组虽然是一个对象,但是其并没有提供add()或者remove()等操作元素的方法,要删除元素的话,可以通过将数组对象转换成List再进行remove(),这个方法今天不在这里展开,这里介绍的是另外一种方法,直接通过Java的操作对数组元素进行移除。流程如下:要删除一个数组中index位置的元素,使…

    2022年10月25日
  • 独立样本与配对样本t检验

    独立样本与配对样本t检验spss操作全力推荐医咖会这个医学网站,上面讲解非常齐全,下附链接:https://www.mediecogroup.com组间均值是否存在差异,使用t检验独立样本t检验1、需满足条件①、数据满足独立性、②、样本均数服从正态分布、③、两个总体方差齐,可选用两个样本均数比较的t检验2、进行判断①、逻辑上判断是否满足独立,②、数据量比较大时,一般可认为数据满足正态分布③、方差齐次…

  • Redis–各个数据类型最大存储量

    Redis–各个数据类型最大存储量

  • acwing-1172. 祖孙询问(最近公共祖先)「建议收藏」

    acwing-1172. 祖孙询问(最近公共祖先)「建议收藏」原题链接给定一棵包含 n 个节点的有根无向树,节点编号互不相同,但不一定是 1∼n。有 m 个询问,每个询问给出了一对节点的编号 x 和 y,询问 x 与 y 的祖孙关系。输入格式输入第一行包括一个整数 表示节点个数;接下来 n 行每行一对整数 a 和 b,表示 a 和 b 之间有一条无向边。如果 b 是 −1,那么 a 就是树的根;第 n+2 行是一个整数 m 表示询问个数;接下来 m 行,每行两个不同的正整数 x 和 y,表示一个询问。输出格式对于每一个询问,若 x 是 y 的祖先则输

  • 云服务器CentOS7安装图形界面与远程连接,超简单

    云服务器CentOS7安装图形界面与远程连接,超简单安装图形界面1.安装图形用户界面接口XWindowSystemyumgroupinstall”XWindowSystem”2.安装图形用界面gnomeyumgroupinstall”GNOMEDesktop”完成以上操作,我们还要借助vnc工具来远程连接桌面1.安装服务端vncvncserver安装yum-yinstalltigervnc-server…

发表回复

您的电子邮箱地址不会被公开。

关注全栈程序员社区公众号