众所周知,多线程中的所有数据全部是由所有线程共享的,在这一点上它和多进程有很大的不同,多进程的内存空间相互独立,互不影响,即使是同一个变量,在多个进程中存储的也是它的拷贝,但多线程却与之相反,这样虽然也有好处,但却也存在着安全隐患。我们先看一个例子:

示例:

import threading
result = 100
def text(n):
    global result
    for i in range(1000000):
        result += n
        result -= n
t1 = threading.Thread(target=text,args=(100,))
t2 = threading.Thread(target=text,args=(-200,))
t1.start()
t2.start()
t1.join()
t2.join()
print(result)

<分析>

这个程序的运行结果会是什么样子呢?
如果仅仅是按照这段程序来推测,那么最后的result应该依然输出100才对,因为我们对result的处理实质上并没有修改它的值。可是当我们运行程序发现,有不小的可能我们得到的结果并不是100,它可能是别的值,这个可能性与range(n)中的值有一定关系,循环的次数越多,得到错误结果的概率也会随之变大。

这说明了什么?
很显然,这说明多线程编程存在一定的安全隐患。为什么呢?因为多线程编程中所有数据都是全线程共享的,倘使同时有多个线程对一个数据进行修改,那么很有可能数据就会因此紊乱,这种可能性在小规模运算量下虽然不大,但一旦出错可能就会造成严重的后果。

Lock

1.lock类

为了解决上述问题,通常需要在关键代码段加上一把锁,确保该段代码在一段时间内只可能由一个线程执行,加锁的目的就在于此。被加锁的代码段不会被多进程同时修改,进程一旦执行加锁的代码段,就只能将它执行完毕,不能中途挂起。当执行完毕后,再将锁释放,由下一个执行线程获得该锁,这样,就避免了同一个数据被不同线程篡改的问题。

怎么实现这种功能呢?我们可以通过使用threading模块提供的 Lock类

Lock类提供的方法:

  • acquire(blocking=True,timeout=-1):请求对Lock或者Rlock对象加锁。block用于指定是否是非堵塞锁,timeout指定加锁多长时间。
  • release():释放锁。

示例:

import threading
lock = threading.Lock() #创建锁实例
result = 100
def text(n):
    global result
    for i in range(1000000):
        lock.acquire() #加锁
        try:
            result += n
            result -= n
        finally:
            lock.release() #释放锁
t1 = threading.Thread(target=text,args=(100,))
t2 = threading.Thread(target=text,args=(-200,))
t1.start()
t2.start()
t1.join()
t2.join()
print(result)

<分析>

如上,我们在需要对关键数据进行修改的代码段加上了锁,此时当多个线程同时运行到lock.acquire()语句时,只会有一个线程能成功获取锁并继续执行下去,其它线程则被阻塞。

因为线程在用完锁之后一定要释放锁,否则其它等待该锁的线程将永远等待下去,进而成为 死线程。为了避免这种情况,我们又用了 try…finally语句来确保锁一定会被释放。

锁的好处就在于此,它能够确保关键代码被完整执行。但它也有坏处,首先就是阻止了并发执行,因为加锁的代码段只有一个线程,不可能并发,这就大大降低了程序执行任务的效率。另外,由于可以存在多个锁,不同的线程持有不同的锁并试图获取对方的锁时,可能会造成 死锁,这会导致多个线程全部挂起,既不能执行,也无法结束,只能由操作系统强行终止。

2.RLock类

除了Lock类之外,threading模块下的RLock类也可以提供加锁和释放锁的功能,相比于Lock类而言,RLock类的锁代表着可重入锁(Reentrant Lock)。

什么是可重入锁呢?

在同一个线程中可以对它进行多次锁定,也可以多次释放的锁就是可重入锁。如果使用RLock,那么acquire()与release()方法必须成对出现,即如果线程调用了n次acquire()加锁,就必须调用n次release()释放锁。

3.RLock类与Lock类的区别:

在说明它们两个的区别之前,先介绍一下 死锁 的概念:

3_1.死锁:

什么是死锁?

当两个线程相互等待对方释放同步监视器时就会发生死锁。
什么意思呢?就是假使有两个锁,你有一个锁,我有一个锁,我想要你的锁,而你又想要我的锁,我们两个互相监视互相等待,但谁都不会释放自己的锁,因为我们都只会在对方释放了对方的锁之后我们才会释放我们的锁。这会造成什么结果呢?
很明显,我们两个将一直僵持下去,对于线程来说,即两个线程都将一直处于阻塞状态。

Python解释器没有监测,也没有独有的措施来处理死锁情况,并且一旦出现死锁,整个程序既不会发出异常,也不会给出提示,因此在进行多线程编程时应该考虑到采取措施避免死锁。

采取什么措施呢?这就回到了我们一开始的问题上,RLock与Lock的一个显著区别就是:RLock可以在一定程度上避免死锁的发生,即对一个RLock锁线程可以多次获取,且线程不会阻塞,而如果对Lock多次获取,就会发生死锁

示例1:

import threading
lock = threading.Lock()
rol = lock.acquire()
print(rol)
rel = lock.acquire()
print(rel)

运行结果:

True
(阻塞)

<分析>

第一次用acquire()方法获取Lock对象,获取成功,返回True,第二次获取失败,因为Lock对象已经被获取,因此主线程被阻塞。

示例2:

import threading
lock = threading.Lock()
lock.acquire()
rol = lock.acquire(False)
print(rol)

运行结果:

False

<分析>

使用非阻塞锁然后获取,虽然获取失败,但主线程不会被阻塞,并返回False。

示例3:

import threading
lock = threading.RLock()
rol = lock.acquire()
print(rol)
rel = lock.acquire()
print(rel)
lock.release()
lock.release()

运行结果:

True
True

<分析>

用RLock可重入锁,即使第一个锁未释放,第二次依然能获得锁。

示例4:

import threading
lock = threading.Lock()
def text():
    lock.acquire()
    try:
        text_1()
    finally:
        lock.release()
def text_1():
    lock.acquire()
    try:
        text_2()
    finally:
        lock.release()
def text_2():
    print('Lock,Lock,biubiubiu.')
threading.Thread(target=text).start()
threading.Thread(target=text).start()
threading.Thread(target=text).start()

运行结果:

(阻塞)

把Lock改为Rlock:

Lock,Lock,biubiubiu.
Lock,Lock,biubiubiu.
Lock,Lock,biubiubiu.

<分析>

在示例四中,我们创建了三个线程,并让它们都运行text()函数,当使用Lock锁时,线程全部被阻塞,这是因为Lock锁不仅要在text()函数中获取,还要在text_1()函数中获取,这两个函数获取的锁显然是同一把锁,因为用Lock锁尝试多次获取是行不通的,所以结果就是死锁。
改用RLock,可以看到成功输出。