一篇文章理清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

目录
相关文章
|
3月前
|
存储 数据采集 自然语言处理
Python爬取公众号文章并实现关键词分析
Python爬取公众号文章并实现关键词分析
|
3月前
|
数据采集 存储 JSON
Python爬取知乎评论:多线程与异步爬虫的性能优化
Python爬取知乎评论:多线程与异步爬虫的性能优化
|
3月前
|
人工智能 安全 调度
Python并发编程之线程同步详解
并发编程在Python中至关重要,线程同步确保多线程程序正确运行。本文详解线程同步机制,包括互斥锁、信号量、事件、条件变量和队列,探讨全局解释器锁(GIL)的影响及解决线程同步问题的最佳实践,如避免全局变量、使用线程安全数据结构、精细化锁的使用等。通过示例代码帮助开发者理解并提升多线程程序的性能与可靠性。
116 0
|
3月前
|
数据采集 监控 调度
干货分享“用 多线程 爬取数据”:单线程 + 协程的效率反超 3 倍,这才是 Python 异步的正确打开方式
在 Python 爬虫中,多线程因 GIL 和切换开销效率低下,而协程通过用户态调度实现高并发,大幅提升爬取效率。本文详解协程原理、实战对比多线程性能,并提供最佳实践,助你掌握异步爬虫核心技术。
|
4月前
|
JSON 算法 Java
打造终端里的下载利器:Python实现可恢复式多线程下载器
在数字时代,大文件下载已成为日常需求。本文教你用Python打造专业级下载器,支持断点续传、多线程加速、速度限制等功能,显著提升终端下载体验。内容涵盖智能续传、多线程分块下载、限速控制及Rich库构建现代终端界面,助你从零构建高效下载工具。
249 1
|
3月前
|
数据采集 存储 Java
多线程Python爬虫:加速大规模学术文献采集
多线程Python爬虫:加速大规模学术文献采集
|
4月前
|
数据采集 网络协议 前端开发
Python多线程爬虫模板:从原理到实战的完整指南
多线程爬虫通过并发请求大幅提升数据采集效率,适用于大规模网页抓取。本文详解其原理与实现,涵盖任务队列、线程池、会话保持、异常处理、反爬对抗等核心技术,并提供可扩展的Python模板代码,助力高效稳定的数据采集实践。
194 0
|
安全 Java 数据处理
Python网络编程基础(Socket编程)多线程/多进程服务器编程
【4月更文挑战第11天】在网络编程中,随着客户端数量的增加,服务器的处理能力成为了一个重要的考量因素。为了处理多个客户端的并发请求,我们通常需要采用多线程或多进程的方式。在本章中,我们将探讨多线程/多进程服务器编程的概念,并通过一个多线程服务器的示例来演示其实现。
|
数据采集 数据库 C++
python并发编程:并发编程中是选择多线程呢?还是多进程呢?还是多协程呢?
python并发编程:并发编程中是选择多线程呢?还是多进程呢?还是多协程呢?
203 0
|
8月前
|
数据采集 Java 数据处理
Python实用技巧:轻松驾驭多线程与多进程,加速任务执行
在Python编程中,多线程和多进程是提升程序效率的关键工具。多线程适用于I/O密集型任务,如文件读写、网络请求;多进程则适合CPU密集型任务,如科学计算、图像处理。本文详细介绍这两种并发编程方式的基本用法及应用场景,并通过实例代码展示如何使用threading、multiprocessing模块及线程池、进程池来优化程序性能。结合实际案例,帮助读者掌握并发编程技巧,提高程序执行速度和资源利用率。
360 0

热门文章

最新文章

推荐镜像

更多