以python为例来理解多线程中的同步锁死锁递归锁

news/2024/5/20 14:52:50 标签: 多线程, python, 死锁, 递归锁, 同步锁

同步锁

首先,我们先了解在使用多线程的时候,什么情况下需要加锁?
那是因为在不同的线程,我们可能会需要对同一个变量进行修改,这个时候就会出现资源抢占的问题,比如在线程A中,对变量X的修改还未执行完毕时,这个时候线程B也要对X进行修改,此时线程B就会把X抢占过来,那么线程A中对X的修改就会被中断,导致修改不成功。
比如,我们下面这个例子,初设化n=0,我们启动10个线程,每个线程都对n进行1000000次+1的操作,那么预期的结果n应该等于10000000

python">from threading import Thread


def add_n():
    global n
    for i in range(1000000):
        n += 1

# 开始添加线程
threads = []
# target:函数,args:函数的参数,以元组形式
for i in range(10):
    threads.append(Thread(target=add_n))

n = 0

for t in threads:
    # 设置为守护线程:主线程运行完毕之后,子线程仍然继续运行
    t.setDaemon(True)
    t.start()  # 所有子线程同时启动
# 等待所有子线程运行完毕之后,再运行主程序
for t in threads:
    t.join()
    
print("n = ", n)

但是,可以看到,结果并不是1000000,这就是多线程资源抢占造成的后果

n =  3434881

那么问题来了,如何解决呢?这个时候就需要用到同步锁了,在对n进行+1时,加上一把同步锁,等操作完成后再释放锁,这样就保证在加锁期间,只有一个线程在执行n+=1操作,其他线程只能等待锁的释放之后才能执行该操作。

python">from threading import Thread, Lock

lock = Lock()

def add_n():
    global n
    for i in range(1000000):
        lock.acquire()  # 
        n += 1
        lock.release()

# 开始添加线程
threads = []
# target:函数,args:函数的参数,以元组形式
for i in range(10):
    threads.append(Thread(target=add_n))

n = 0

for t in threads:
    # 设置为守护线程:主线程运行完毕之后,子线程仍然继续运行
    t.setDaemon(True)
    t.start()  # 所有子线程同时启动
# 等待所有子线程运行完毕之后,再运行主程序
for t in threads:
    t.join()
    
print("n = ", n)

这个时候,加上同步锁之后,得到的n就如我们预期一样了。

n =  10000000

死锁

在给多线程加锁的时候,需要特别注意一点就是防止出现死锁
在下面的例子中,就出现了死锁的情况,我们来分析一下为什么出现了死锁

  1. A拿了一个苹果,并把苹果锁住了;
  2. B拿了一个香蕉,并把香蕉锁住了;
  3. A现在想再拿个香蕉,但必须得等待B释放这个香蕉;
  4. B同时想要再拿个苹果,这时候它也得等待A释放苹果;
  5. 这样就是陷入了僵局,出现了死锁
python">from threading import Thread, Lock
import time

lock_apple = Lock()
lock_banana = Lock()

def fun1():

    lock_apple.acquire()  # 如果锁被占用,则阻塞在这里,等待锁的释放

    print ("线程 %s , 想拿: %s--%s" %(t.name, "苹果",time.ctime()))

    lock_banana.acquire()
    print ("线程 %s , 想拿: %s--%s" %(t.name, "香蕉",time.ctime()))
    lock_banana.release()
    lock_apple.release()


def fun2():

    lock_banana.acquire()
    print ("线程 %s , 想拿: %s--%s" %(t.name, "香蕉",time.ctime()))
    time.sleep(0.1)

    lock_apple.acquire()
    print ("线程 %s , 想拿: %s--%s" %(t.name, "苹果",time.ctime()))
    lock_apple.release()

    lock_banana.release()

def run():
    fun1()
    fun2()

# 开始添加线程
threads = []
# target:函数,args:函数的参数,以元组形式
for i in range(10):
    threads.append(Thread(target=run))

for t in threads:
    # 设置为守护线程:主线程运行完毕之后,子线程仍然继续运行
    t.setDaemon(True)
    t.start()  # 所有子线程同时启动
for t in threads:
    t.join()

程序会一直卡在这里不动了

线程 Thread-1 , 想拿: 苹果--Wed Aug 28 12:19:40 2019
线程 Thread-1 , 想拿: 香蕉--Wed Aug 28 12:19:40 2019
线程 Thread-2 , 想拿: 香蕉--Wed Aug 28 12:19:40 2019
线程 Thread-2 , 想拿: 苹果--Wed Aug 28 12:19:40 2019

递归锁

解决死锁,就需要用到递归锁了。RLock内部维护着一个Lock和一个counter变量,counter记录了acquire的次数,从而使得资源可以被多次acquire。直到一个线程所有的acquire都被release,其他的线程才能获得资源。

python">from threading import Thread, RLock
import time

lock = RLock()

def fun1():

    lock.acquire()  # 如果锁被占用,则阻塞在这里,等待锁的释放

    print ("线程 %s , 想拿: %s--%s" %(t.name, "苹果",time.ctime()))

    lock.acquire()
    print ("线程 %s , 想拿: %s--%s" %(t.name, "香蕉",time.ctime()))
    lock.release()
    lock.release()


def fun2():

    lock.acquire()
    print ("线程 %s , 想拿: %s--%s" %(t.name, "香蕉",time.ctime()))
    time.sleep(0.1)

    lock.acquire()
    print ("线程 %s , 想拿: %s--%s" %(t.name, "苹果",time.ctime()))
    lock.release()

    lock.release()

def run():
    fun1()
    fun2()

# 开始添加线程
threads = []
# target:函数,args:函数的参数,以元组形式
for i in range(10):
    threads.append(Thread(target=run))

for t in threads:
    # 设置为守护线程:主线程运行完毕之后,子线程仍然继续运行
    t.setDaemon(True)
    t.start()  # 所有子线程同时启动
for t in threads:
    t.join()

欢迎关注同名公众号:“我就算饿死也不做程序员”。
交个朋友,一起交流,一起学习,一起进步。在这里插入图片描述


http://www.niftyadmin.cn/n/1693530.html

相关文章

maven工程读取resources配置文件的正确姿势

我们maven项目结构如下: 使用相对路径来读取resources目录下的资源文件 InputStream in new InputStream(new File(“src/main/resources/car.txt”));这样在本地运行的时候,是能正常读取到的,不会报错,但是如果打成jar包&…

spark读取HBase数据的一次坑爹经历

首先,在这里说明一下,我遇到的错误如下: org.apache.hadoop.hbase.DoNotRetryIOException: /192.168.x.x:16020 is unable to read call parameter from client 10.47.x.x 然后,你在spark中读取HBase的方式也与我一样&…

通过Git安装和配置自己的GitHub仓库

目录安装WindowsMacOSUbuntu配置你的GitHub账号安装 Windows 直接在百度搜索git,到官网下载git.exe安装包,然后像其他软件一样案安装即可 MacOS 打开终端,输入git,如果你之前已经安装了,会显示以下信息&#xff1b…

Windows安装mysql8教程

目录下载安装初设化和启动注意事项mysql服务启动失败卸载mysql远程登录失败下载 首先,去MySQL官网下载免费的社区版 安装 将下载后的zip文件进行解压,放在没有中文和空格的目录下;在你的mysql根目录下,新建my.ini文件和data文…

spark sql和jdbc将数据写入mysql的对比

目录jdbcspark sql引用的库类效率对比连接mysql错误jdbc public static void jdbc() {// test为数据库名,spark为表名final String url "jdbc:mysql://localhost:3306/test";final String username "root";final String password "123…

数据结构:队列的java实现以及优化

队列 队列是一种先进先出,后进后出的数据结构。 如果用数组的基本数据结构实现的话,普通的队列只能入列的元素数量为数组的大小,不管是否已经出列。 /*** 队列:先入先出,后入后出* 最多只能入列maxSize个元素&#x…

二叉排序树的创建和节点删除

二叉排序树-BST: (Binary Sort(Search) Tree), 对于二叉排序树的任何一个非叶子节点,要求左子节点的值比当前节点的值小,右子节点的值比当前节点的值大。 例如对于一批数据 (7, 3, 10, 12, 5, 1, 9) 对应的二叉排序树如下: 二叉排序树的创建…

平衡二叉树(AVL)的原理和java实现

目录什么是AVL左旋转右旋转双旋转java实现什么是AVL 平衡二叉树(AVL:下面我们都统称AVL)也是一种二叉排序树,但是它的左子树和右子树的高度差不超过1,可以认为是二叉排序树优化之后的一种数据结构。 (如果…