一篇文章理清Python多线程同步锁,死锁和递归锁

简介: 公众号:pythonislover前面说到过python多线程的基本使用,大概的内容有几点1.创建线程对象t1 = threading.Thread(target=say,args=('tony',))2.启动线程t1.start()后面又说了两个点就是join和守护线程的概念但是不知道大家有没有注意到一点就是前面说的两个功能是相互独立的,相互不干涉的,不会用到同享的资源或者数据,如果我们多个线程要用到相同的数据,那么就会存在资源争用和锁的问题,不管在什么语言中,这个都是不能避免的。

公众号:pythonislover

前面说到过python多线程的基本使用,大概的内容有几点

1.创建线程对象
t1 = threading.Thread(target=say,args=('tony',))
2.启动线程
t1.start()
后面又说了两个点就是join和守护线程的概念

但是不知道大家有没有注意到一点就是前面说的两个功能是相互独立的,相互不干涉的,不会用到同享的资源或者数据,如果我们多个线程要用到相同的数据,那么就会存在资源争用和锁的问题,不管在什么语言中,这个都是不能避免的。对数据库属性的同学应该也了解,数据库中也存在锁的概念。

今天这篇文章我们说说python多线程中的同步锁,死锁和递归锁的使用。

  1. Python同步锁

锁通常被用来实现对共享资源的同步访问。为每一个共享资源创建一个Lock对象,当你需要访问该资源时,调用acquire方法来获取锁对象(如果其它线程已经获得了该锁,则当前线程需等待其被释放),待资源访问完后,再调用release方法释放锁。

下面我们来举个例子说明如果多线程在没有同步锁的情况下访问公共资源会导致什么情况

import threading
import time

num = 100

def fun_sub():
    global num
    # num -= 1
    num2 = num
    time.sleep(0.001)
    num = num2-1

if __name__ == '__main__':
    print('开始测试同步锁 at %s' % time.ctime())

    thread_list = []
    for thread in range(100):
        t = threading.Thread(target=fun_sub)
        t.start()
        thread_list.append(t)

    for t in thread_list:
        t.join()
    print('num is %d' % num)
    print('结束测试同步锁 at %s' % time.ctime())

上面的例子其实很简单就是创建100的线程,然后每个线程去从公共资源num变量去执行减1操作,按照正常情况下面,等到代码执行结束,打印num变量,应该得到的是0,因为100个线程都去执行了一次减1的操作。

但是结果却不是我们想想的,我们看看结果

开始测试同步锁 at Sun Apr 28 09:56:45 2019
num is 91
结束测试同步锁 at Sun Apr 28 09:56:45 2019

我们会发现,每次执行的结果num值都不是一样的,上面显示的是91,那就存在问题了,为什么结果不是0呢?

我们来看看上面代码的执行流程。
1.因为GIL,只有一个线程(假设线程1)拿到了num这个资源,然后把变量赋值给num2,sleep 0.001秒,这时候num=100
2.当第一个线程sleep 0.001秒这个期间,这个线程会做yield操作,就是把cpu切换给别的线程执行(假设线程2拿到个GIL,获得cpu使用权),线程2也和线程1一样也拿到num,返回赋值给num2,然sleep,这时候,其实num还是=100.
3.线程2 sleep时候,又要yield操作,假设线程3拿到num,执行上面的操作,其实num有可能还是100
4.等到后面cpu重新切换给线程1,线程2,线程3上执行的时候,他们执行减1操作后,其实等到的num其实都是99,而不是顺序递减的。
5.其他剩余的线程操作如上

大家应该发现问题了,结果和我们想想的不一样,那我们怎么才能等到我们想要的结果呢?就是100个线程操作num变量得到最后结果为0?

这里就要借助于python的同步锁了,也就是同一时间只能放一个线程来操作num变量,减1之后,后面的线程操作来操作num变量。看看下面我们怎么实现。

import threading
import time

num = 100

def fun_sub():
    global num
    lock.acquire()
    print('----加锁----')
    print('现在操作共享资源的线程名字是:',t.name)
    num2 = num
    time.sleep(0.001)
    num = num2-1
    lock.release()
    print('----释放锁----')

if __name__ == '__main__':
    print('开始测试同步锁 at %s' % time.ctime())

    lock = threading.Lock() #创建一把同步锁

    thread_list = []
    for thread in range(100):
        t = threading.Thread(target=fun_sub)
        t.start()
        thread_list.append(t)

    for t in thread_list:
        t.join()
    print('num is %d' % num)
    print('结束测试同步锁 at %s' % time.ctime())

看到上面我们给中间的减1代码块,加个一把同步锁,这样,我们就可以得到我们想要的结果了,这就是同步锁的作用,一次只有一个线程操作同享资源。

看看上面代码执行的结果:

.......
----加锁----
现在操作共享资源的线程名字是: Thread-98
----释放锁----
----加锁----
现在操作共享资源的线程名字是: Thread-100
----释放锁----
num is 0
结束测试同步锁 at Sun Apr 28 12:08:27 2019
  1. Python死锁

死锁的这个概念在很多地方都存在,比较在数据中,大概介绍下私有是怎么产生的
1.A拿了一个苹果
2.B拿了一个香蕉
3.A现在想再拿个香蕉,就在等待B释放这个香蕉
4.B同时想要再拿个苹果,这时候就等待A释放苹果
5.这样就是陷入了僵局,这就是生活中的死锁

python中在线程间共享多个资源的时候,如果两个线程分别占有一部分资源并且同时等待对方的资源,就会造成死锁,因为系统判断这部分资源都正在使用,所有这两个线程在无外力作用下将一直等待下去。下面是一个死锁的例子:

import threading
import time

lock_apple = threading.Lock()
lock_banana = threading.Lock()

class MyThread(threading.Thread):

    def __init__(self):
        threading.Thread.__init__(self)

    def run(self):
        self.fun1()
        self.fun2()

    def fun1(self):

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

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

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


    def fun2(self):

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

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

        lock_banana.release()

if __name__ == "__main__":
    for i in range(0, 10):  #建立10个线程
        my_thread = MyThread()  #类继承法是python多线程的另外一种实现方式
        my_thread.start()
代码执行hung住,死锁了

线程 Thread-1 , 想拿: 苹果--Sun Apr 28 12:21:06 2019
线程 Thread-1 , 想拿: 香蕉--Sun Apr 28 12:21:06 2019
线程 Thread-1 , 想拿: 香蕉--Sun Apr 28 12:21:06 2019
线程 Thread-2 , 想拿: 苹果--Sun Apr 28 12:21:06 2019

Process finished with exit code -1

上面的代码其实就是描述了苹果和香蕉的故事。大家可以仔细看看过程。下面我们看看执行流程

1.fun1中,线程1先拿了苹果,然后拿了香蕉,然后释放香蕉和苹果,然后再在fun2中又拿了香蕉,sleep 0.1秒。
2.在线程1的执行过程中,线程2进入了,因为苹果被线程1释放了,线程2这时候获得了苹果,然后想拿香蕉
3.这时候就出现问题了,线程一拿完香蕉之后想拿苹果,返现苹果被线程2拿到了,线程2拿到苹果执行,想拿香蕉,发现香蕉被线程1持有了
4.双向等待,出现死锁,代码执行不下去了

上面就是大概的执行流程和死锁出现的原因。在这种情况下就是在同一线程中多次请求同一资源时候出现的问题。

  1. Python递归锁RLock

为了支持在同一线程中多次请求同一资源,python提供了"递归锁":threading.RLock。RLock内部维护着一个Lock和一个counter变量,counter记录了acquire的次数,从而使得资源可以被多次acquire。直到一个线程所有的acquire都被release,其他的线程才能获得资源。

下面我们用递归锁RLock解决上面的死锁问题

import threading
import time

lock = threading.RLock()  #递归锁


class MyThread(threading.Thread):

    def __init__(self):
        threading.Thread.__init__(self)

    def run(self):
        self.fun1()
        self.fun2()

    def fun1(self):

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

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

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


    def fun2(self):

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

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

        lock.release()

if __name__ == "__main__":
    for i in range(0, 10):  #建立10个线程
        my_thread = MyThread()  #类继承法是python多线程的另外一种实现方式
        my_thread.start()

上面我们用一把递归锁,就解决了多个同步锁导致的死锁问题。大家可以把RLock理解为大锁中还有小锁,只有等到内部所有的小锁,都没有了,其他的线程才能进入这个公共资源。

另外一点前面没有就算用类继承的方法实现python多线程,这个大家可以查下,就算继承Thread类,然后重新run方法来实现。

最后大家可能还有个疑问,就算如果我们都加锁了,也就是单线程了,那我们还要开多线程有什么用呢?这里解释下,在访问共享资源的时候,锁是一定要存在了,但是我们的代码中不是总是在访问公共资源的,还有一些其他的逻辑可以使用多线程,所以我们在代码里面加锁的时候,要注意在什么地方加,对性能的影响最小,这个就靠对逻辑的理解了。

好了今天就说到这里,个人意见,望指教。

文章推荐:
Python基础-不一样的切片操作 - https://mp.weixin.qq.com/s/3pute7-xrdhAt2Yk41nw9w
Python那些有用的小知识点1 - https://mp.weixin.qq.com/s/JXnBrYj0TEAo9LNrVv2cPA
Python日志模块之你还在用PRINT打印日志吗 - https://mp.weixin.qq.com/s/P_SDxmbxkK1ei3-sq5Qhlw
90%的人说Python程序慢,5大神招让你的代码像赛车一样跑起来 - https://mp.weixin.qq.com/s/XZcqzwodPjvjltPRNRTpVQ
110道python面试题 - https://mp.weixin.qq.com/s/SyC_LLQL8AU3i6wYNlOdNQ
Python你想知道的时间格式Time都在这里 - https://mp.weixin.qq.com/s/fXv9leS8xsKUEVRvo8sKdA
深度辨析 Python 的 eval() 与 exec() - https://mp.weixin.qq.com/s/Dkn6TXUSfEk_Lg4SF0m81g
为什么range不是迭代器?range到底是什么类型? - https://mp.weixin.qq.com/s/9FdWqAuQgv-t-yLJXwiHTg
6种方法让Python遍历多个列表 - https://mp.weixin.qq.com/s/z1wuXp8x08YZQMmKIMQHQg
Python之sorted,max,min内置函数用法 - https://mp.weixin.qq.com/s/KDRedycdz3qAGsaFMrqJBg
愉快地迁移到Python3 - https://mp.weixin.qq.com/s/RPdx2G36kDIQ3_G_XPk9tQ

目录
相关文章
|
2月前
|
数据采集 存储 数据处理
Python中的多线程编程及其在数据处理中的应用
本文深入探讨了Python中多线程编程的概念、原理和实现方法,并详细介绍了其在数据处理领域的应用。通过对比单线程与多线程的性能差异,展示了多线程编程在提升程序运行效率方面的显著优势。文章还提供了实际案例,帮助读者更好地理解和掌握多线程编程技术。
|
2月前
|
并行计算 数据处理 调度
Python中的并发编程:探索多线程与多进程的奥秘####
本文深入探讨了Python中并发编程的两种主要方式——多线程与多进程,通过对比分析它们的工作原理、适用场景及性能差异,揭示了在不同应用需求下如何合理选择并发模型。文章首先简述了并发编程的基本概念,随后详细阐述了Python中多线程与多进程的实现机制,包括GIL(全局解释器锁)对多线程的影响以及多进程的独立内存空间特性。最后,通过实例演示了如何在Python项目中有效利用多线程和多进程提升程序性能。 ####
|
2月前
|
算法 Python
在Python编程中,分治法、贪心算法和动态规划是三种重要的算法。分治法通过将大问题分解为小问题,递归解决后合并结果
在Python编程中,分治法、贪心算法和动态规划是三种重要的算法。分治法通过将大问题分解为小问题,递归解决后合并结果;贪心算法在每一步选择局部最优解,追求全局最优;动态规划通过保存子问题的解,避免重复计算,确保全局最优。这三种算法各具特色,适用于不同类型的问题,合理选择能显著提升编程效率。
70 2
|
2月前
|
Java Unix 调度
python多线程!
本文介绍了线程的基本概念、多线程技术、线程的创建与管理、线程间的通信与同步机制,以及线程池和队列模块的使用。文章详细讲解了如何使用 `_thread` 和 `threading` 模块创建和管理线程,介绍了线程锁 `Lock` 的作用和使用方法,解决了多线程环境下的数据共享问题。此外,还介绍了 `Timer` 定时器和 `ThreadPoolExecutor` 线程池的使用,最后通过一个具体的案例展示了如何使用多线程爬取电影票房数据。文章还对比了进程和线程的优缺点,并讨论了计算密集型和IO密集型任务的适用场景。
123 4
|
2月前
|
监控 JavaScript 前端开发
python中的线程和进程(一文带你了解)
欢迎来到瑞雨溪的博客,这里是一位热爱JavaScript和Vue的大一学生分享技术心得的地方。如果你从我的文章中有所收获,欢迎关注我,我将持续更新更多优质内容,你的支持是我前进的动力!🎉🎉🎉
33 0
|
2月前
|
数据采集 Java Python
爬取小说资源的Python实践:从单线程到多线程的效率飞跃
本文介绍了一种使用Python从笔趣阁网站爬取小说内容的方法,并通过引入多线程技术大幅提高了下载效率。文章首先概述了环境准备,包括所需安装的库,然后详细描述了爬虫程序的设计与实现过程,包括发送HTTP请求、解析HTML文档、提取章节链接及多线程下载等步骤。最后,强调了性能优化的重要性,并提醒读者遵守相关法律法规。
77 0
|
1月前
|
人工智能 数据可视化 数据挖掘
探索Python编程:从基础到高级
在这篇文章中,我们将一起深入探索Python编程的世界。无论你是初学者还是有经验的程序员,都可以从中获得新的知识和技能。我们将从Python的基础语法开始,然后逐步过渡到更复杂的主题,如面向对象编程、异常处理和模块使用。最后,我们将通过一些实际的代码示例,来展示如何应用这些知识解决实际问题。让我们一起开启Python编程的旅程吧!
|
1月前
|
存储 数据采集 人工智能
Python编程入门:从零基础到实战应用
本文是一篇面向初学者的Python编程教程,旨在帮助读者从零开始学习Python编程语言。文章首先介绍了Python的基本概念和特点,然后通过一个简单的例子展示了如何编写Python代码。接下来,文章详细介绍了Python的数据类型、变量、运算符、控制结构、函数等基本语法知识。最后,文章通过一个实战项目——制作一个简单的计算器程序,帮助读者巩固所学知识并提高编程技能。
|
25天前
|
Unix Linux 程序员
[oeasy]python053_学编程为什么从hello_world_开始
视频介绍了“Hello World”程序的由来及其在编程中的重要性。从贝尔实验室诞生的Unix系统和C语言说起,讲述了“Hello World”作为经典示例的起源和流传过程。文章还探讨了C语言对其他编程语言的影响,以及它在系统编程中的地位。最后总结了“Hello World”、print、小括号和双引号等编程概念的来源。
107 80
|
14天前
|
Python
[oeasy]python055_python编程_容易出现的问题_函数名的重新赋值_print_int
本文介绍了Python编程中容易出现的问题,特别是函数名、类名和模块名的重新赋值。通过具体示例展示了将内建函数(如`print`、`int`、`max`)或模块名(如`os`)重新赋值为其他类型后,会导致原有功能失效。例如,将`print`赋值为整数后,无法再用其输出内容;将`int`赋值为整数后,无法再进行类型转换。重新赋值后,这些名称失去了原有的功能,可能导致程序错误。总结指出,已有的函数名、类名和模块名不适合覆盖赋新值,否则会失去原有功能。如果需要使用类似的变量名,建议采用其他命名方式以避免冲突。
34 14