博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
python线程同步机制
阅读量:7069 次
发布时间:2019-06-28

本文共 5906 字,大约阅读时间需要 19 分钟。

本文首发于

本文从线程同步的起因开始讲起,深入到各种同步机制,主要包括如下内容

  • 线程锁(线程同步、互斥锁)
  • GIL锁
  • 死锁
  • RLock(递归锁、可重入锁)

线程锁(线程同步、互斥锁)

在多进程中,每一个进程都拷贝了一份数据,而多线程的各个线程则共享相同的数据。这使多线程占用的资源更少,但是资源混用会导致一些错误,我们来看下面这个例子

import threading import time zero = 0 def change_zero(): global zero for i in range(1000000):         zero = zero + 1         zero = zero - 1 th1 = threading.Thread(target = change_zero) th2 = threading.Thread(target = change_zero) th1.start() th2.start() th1.join() th2.join() print(zero) 复制代码

change_zero函数会将zero变量加1再减1,按理说无论运行多少次,zero变量都应该是0,但是上面代码运行多次,总会出现不是0的情况(如果循环改为运行10000000次,则很难出现结果是0的情况了),说明不同线程一起修改zero变量出现了错乱,下面我们来看一下错乱的起因是什么样的。(参考)

zero = zero + 1在python中会先产生一个中间变量,比如x1 = zero + 1,然后再zero = x1

在不使用多线程时,运行两次change_zero函数是这样的

初始:zero = 0 th1: x1 = zero + 1  # x1 = 1 th1: zero = x1      # zero = 1 th1: x1 = zero - 1  # x1 = 0 th1: zero = x1      # zero = 0 th2: x2 = zero + 1  # x2 = 1 th2: zero = x2      # zero = 1 th2: x2 = zero - 1  # x2 = 0 th2: zero = x2      # zero = 0 结果:zero = 0 复制代码

使用多线程,可能出现这样的交叉影响

初始:zero = 0 th1: x1 = zero + 1  # x1 = 1 th2: x2 = zero + 1  # x2 = 1 th2: zero = x2      # zero = 1 th1: zero = x1      # zero = 1  问题出在这里,两次赋值,本来应该加2变成了加1 th1: x1 = zero - 1  # x1 = 0 th1: zero = x1      # zero = 0 th2: x2 = zero - 1  # x2 = -1 th2: zero = x2      # zero = -1 结果:zero = -1 复制代码

当循环次数非常多的时候就难免出现这样的乱象,从而导致结果的错误。threading模块提供了解决方法:线程锁。

创建出一个锁,在change_zero函数中首先要获得锁才能继续运行,最后释放锁。同一个锁同一时间只能被一个线程使用,所以当一个线程使用锁时,其他线程只能等着,等到锁被释放才能获得锁进行计算。代码改为

import threading import time zero = 0 lock = threading.Lock() def change_zero(): global zero for i in range(1000000): lock.acquire()         zero = zero + 1         zero = zero - 1 lock.release() th1 = threading.Thread(target = change_zero) th2 = threading.Thread(target = change_zero) th1.start() th2.start() th1.join() th2.join() print(zero) 复制代码

这样做返回的结果每次都是0

注意几点:

  • acquirerelease时可以使用try finally模式,保证锁一定被释放,否则可能有一些线程一直等着锁却等不到
  • 使用线程锁虽然能解决变量混用造成的错误,但是也降低了运行效率,因为一个线程使用锁运行时其他线程无法一起运行
  • 一般不要用acquire release包含整个运行函数部分,而是只包含可能导致错误的那一步即可,否则就和使用单线程没有区别了
  • 使用多个锁时可能两个线程各持有一个锁,并试图获得对方的锁,这会造成死锁,所有线程都耗在这里了,需要人为终止
  • 这个现象出现的情况:比如用多个线程读写同一份文件,要在读写整个过程前后加一个锁保证读写过程不被影响

另外,lock是有上下文管理形式的,上面的代码可以改写为

import threading import time zero = 0 lock = threading.Lock() def change_zero(): global zero for i in range(1000000): with lock:             zero = zero + 1             zero = zero - 1 th1 = threading.Thread(target = change_zero) th2 = threading.Thread(target = change_zero) th1.start() th2.start() th1.join() th2.join() print(zero) 复制代码

最后解释两个常见的概念(来自百度百科)

  • 线程同步:这里的同步指按预定的先后次序进行运行,“同”字应是指协同、协助、互相配合。所谓同步,就是在发出一个功能调用时,在没有得到结果之前,该调用就不返回,同时其它线程也不能调用这个方法。与异步相对。
  • 互斥锁:防止两条线程同时对同一公共资源(比如全局变量)进行读写的机制

GIL锁

GIL锁全称为全局解释锁(Global Interpreter Lock),任何python线程在执行之前都需要先获得GIL锁,然后每执行一部分代码,解释器就会自动释放GIL锁,其他线程就可以竞争这个锁,只有得到才能执行程序。

GIL锁是很多人说python多线程鸡肋的原因,但这其实是CPython解释器存在的问题(换个解释器可能就没有GIL锁了,不过当前绝大多数python程序都是用CPython解释器)。

首先要声明,GIL锁只影响CPU密集型程序的运行效率。对于IO密集型或者网页请求这种程序,多线程的效率还是很高的,因为它们主要消耗的时间在于等待。

对于CPU密集型程序,GIL锁的影响在于,有了它的存在,开启多线程无法利用多核优势,也就是只能用到一个核CPU来运行代码,要想用到多个核只能开启多进程(或者使用不带有GIL锁的解释器)。锁的存在使得一个时间只有一个线程在进行计算,所以即使开启多线程,也无法同时运算,而是线性地运算。因此有时开启多线程运行CPU密集型程序,反而会降低运行效率,因为多出了线程之间的切换与争夺锁所耗的时间。

死锁

锁利用不当可能造成死锁,我们来看下面一个例子

import threading import time lock1 = threading.Lock() lock2 = threading.Lock() class MyThread(threading.Thread): def print1(self):         lock1.acquire() # 获得第一个锁         print('print1 first ' + threading.current_thread().name)         time.sleep(1)         lock2.acquire() # 未释放第一个锁就请求第二个锁         print('print1 second ' + threading.current_thread().name)         lock2.release()         lock1.release() def print2(self):         lock2.acquire() # 获得第二个锁         print('print2 first ' + threading.current_thread().name)         time.sleep(1)         lock1.acquire() # 未释放第二个锁就请求第一个锁         print('print2 second ' + threading.current_thread().name)         lock1.release()         lock2.release() def run(self):         self.print1()         self.print2() th1 = MyThread() th2 = MyThread() th1.start() th1.join() th2.start() th2.join() print('finish') 复制代码

上面的代码是一个线程运行结束开始下一个线程,所以运行是不会有问题的,会输出结果

print1 first Thread-1 print1 second Thread-1 print2 first Thread-1 print2 second Thread-1 print1 first Thread-2 print1 second Thread-2 print2 first Thread-2 print2 second Thread-2 finish 复制代码

start join那部分改为

th1.start() th2.start() th1.join() th2.join() 复制代码

即两个线程会同时开启,程序打印出

print1 first Thread-1 print1 second Thread-1 print2 first Thread-1 print1 first Thread-2 复制代码

就会停止,陷入死锁状态,程序永远无法运行结束,根本原因在于:一个线程持有锁1同时在请求锁2,另一个线程持有锁2同时在请求锁1,二者不得到对方的锁都不会放开自己的锁,程序就这样僵持下去了。

下面来分析一下细节

  • 第一个线程先执行print1,获得了锁1,等待1秒。这时第二个线程已经开启,企图获得锁1,但是获取不到于是等待
  • 第一个线程等待时间结束,获得锁2,打印结束释放两把锁。之后马上开始执行print2,并获得锁2,等待1秒
  • 这时第二个线程可以获得锁1了,开始执行print1,也等待1秒
  • 等待时间结束,第一个线程持有锁2企图获得锁1,第一个线程持有锁1企图获得锁2,就陷入了僵局

(其实这个例子可以更简化一点,直接两个线程分别运行print1 print2即可)

我们在写多线程程序的时候,要注意避免死锁的发生。

RLock(递归锁、可重入锁)

上面我们初始化锁使用threading.Lock,这是一种最低级的线程同步指令。

现在来试一试另一种threading.RLock,它与Lock的不同在于

  • 同一个线程可以对RLock请求多次,而Lock只能一次,不过请求多次时,acquire的次数必须和release次数相同
  • Lock被一个线程acquire后,可以由另一个线程release,而RLock必须是本线程

我们来看下面例子

import threading import time lock = threading.RLock() def myprint():     print('start')     lock.acquire()     lock.acquire()     print('try rlock')     lock.release()     lock.release() myprint() 复制代码

这个代码会如期打印出

start try rlock 复制代码

如果我们用lock = threading.Lock(),则自动构成死锁,因为Lock只能被请求一次,所以第二次会一直等待下去。

RLock有什么用呢?

下面是上的一个例子

编写三个函数,他们之间有嵌套关系

def f():   g()   h() def g():   h()   do_something1() def h():   do_something2() 复制代码

现在想给这些函数上锁,用RLock可以这样写

import threading lock = threading.RLock() def f(): with lock:     g()     h() def g(): with lock:     h()     do_something1() def h(): with lock:     do_something2() 复制代码

但是Lock就需要这样写

import threading lock = threading.Lock() def f(): with lock:     _g()     _h() def g(): with lock:     _g() def _g():   _h()   do_something1() def h(): with lock:     _h() def _h():   do_something2() 复制代码

因为用Lock时,调用的f是上锁的,里面的g不能还是上锁的,就要多定义出一个_g来表示未上锁的函数。这就是可以被同一个线程获得多次锁的好处。

欢迎关注我的知乎专栏

专栏主页:

专栏目录:

版本说明:

转载地址:http://mhhll.baihongyu.com/

你可能感兴趣的文章
Linux条件测试
查看>>
阿兰•图灵与人工智能
查看>>
操作系统简单快捷安装方式
查看>>
微软MVA征文参赛作品_微软云计算,缔造新生活
查看>>
openshift 安装
查看>>
使用图形化工具Gitbook Editor编辑gitbook电子书
查看>>
SSH免密码登录原理
查看>>
我的友情链接
查看>>
我的友情链接
查看>>
Mbps与MB/s的区别
查看>>
eclipse 导入Maven项目的问题
查看>>
关于Java IO与NIO知识都在这里
查看>>
DEDE如何提取文章内容里面的第一张图片地址
查看>>
SQL Server的CONVERT() 函数介绍
查看>>
关于安装oracle数据库
查看>>
一句励志的英文短句,希望大家喜欢!
查看>>
org.hibernate.AssertionFailure: null id in xxx (don't flush the Session after an exception occurs)
查看>>
我的友情链接
查看>>
Android全局对话框
查看>>
awstats 分析nginx 日志
查看>>