python多线程共享全局变量及互斥锁问题解析源码

文章资讯 2019-10-17 10:12:13

python多线程共享全局变量及互斥锁问题解析源码

#!/usr/bin/env python3# -*- coding: utf-8 -*-
from threading import Thread
num = 0
def addnum():
    global num
    for i in range(1000):
        num += 1
    print(num)
if __name__ == '__main__':
    t1 = Thread(target=addnum)
    t2 = Thread(target=addnum)
    t1.start()
    t2.start()
    t1.join()
    t2.join()
    print('主进程结束,num的值为:',num)

结果为:

10002000
主进程结束,num的值为: 2000

可以看到,两个线程间是共享了num的值。虽说多线程是共享同一块内存空间的,但是由于每个线程的执行时间是不确定的,都是由CPU来分配的,这就造成 在处理全局变量的时候,有可能线程t1和t2都同时对全局变量num进程操作,比如num原来是10,同时进程加1之 后,由于他们获取到的num值都是10,因此同时进行加1的时候就只能让num的值变为11,假如是当有一个线程在 对num进程操作时,另一个线程等待之前的线程操作完成再去操作的话就不会出现上面的情况。

下面的代码演示了CPU时间片切换的明显现象:

#!/usr/bin/env python3# -*- coding: utf-8 -*-
from threading import Thread
import time
list1 = []
def addnum(seq):
    global list1
    for i in seq:
        time.sleep(0.1) # 通过时间等待,可以清楚的看到时间片切换的痕迹
        list1.append(i)
if __name__ == '__main__':
    nums = range(10)
    string = 'abcdefg'
    t1 = Thread(target=addnum, args=(nums,))
    t2 = Thread(target=addnum, args=(string,))
    t1.start()
    t2.start()
    t1.join()
    t2.join()
    print(list1)

结果为:

[0, 'a', 'b', 1, 2, 'c', 3, 'd', 'e', 4, 5, 'f', 6, 'g', 7, 8, 9]

可以看到,线程之间是由时间片来切换的。

2. 线程同步
线程同步说的就是上面多线程对全局变量进程操作的问题,因为后一个操作要依靠前一个操作的结果,因此 后面的操作必须等待前面的操作完成才能继续操作,否则就会造成数据缺失。

3. 互斥锁
互斥锁是python多线程为了解决多线程对相同资源竞争而提供的一种锁机制,互斥锁有两种状态:上锁/释放 锁。这两种状态是互斥的,也就是同一把锁一旦上锁就必须等待释放锁之后才能再次上锁。多线程中,同一 把锁一旦有一个线程上锁,其他的线程必须等待该锁释放之后才能继续上锁。就像排队买票一样,同一个窗 口,后面的人必须等待前面的人买完了才能上去买。
python多线程可以通过Lock类来实现互斥锁机制。

#!/usr/bin/env python3# -*- coding: utf-8 -*-
from threading import Thread,Lock
num = 0
mutex = Lock() #互斥锁实例对象
def addnum():
    global num
    for i in range(1000):
        mutex.acquire() # 上锁
        num += 1
        mutex.release() # 解锁
    print(num)
if __name__ == '__main__':
    t1 = Thread(target=addnum)
    t2 = Thread(target=addnum)
    t1.start()
    t2.start()
    t1.join()
    t2.join()
    print('主进程结束,num的值为:',num)

结果为:

1000
2000
主进程结束,num的值为: 2000

Lock 类实例有两个主要的方法,acquire以及release方法。

acquire()方法就是让线程获得锁,也就是上锁,从上锁到释放锁中间的代码都是线程安全的,只有获得锁的 线程才能执行,直到释放锁,其他的线程才能在获得锁之后再次对代码进行操作。
release()释放锁。解除当前线程对该锁的占用。

4. 线程死锁

线程试图对同一个互斥量A加锁两次

线程1拥有A锁,请求获得B锁;线程2拥有B锁,请求获得A锁
也就是说,两个线程分别各自获得一把锁,然后在没有释放自己拥有的锁的同时想要获得对方的锁。就像两 个人吵架,谁都不想先认错,互相僵持住了。

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from threading import Thread, Lock
import time
mutex1 = Lock() # 实例化锁A
mutex2 = Lock() # 实例化锁A  
def workA():
    mutex1.acquire()
    print("线程A获得锁A")
    time.sleep(1) # 延时操作,让线程B先获得锁B    
    mutex2.acquire()
    print("线程A获得锁B")
    mutex2.release()
    print("线程A释放锁B")
    mutex1.release()
    print("线程A释放锁A")
def workB():
    mutex2.acquire()
    print("线程B获得锁B")
    time.sleep(1) # 延时操作,让线程A先获得锁A     
    mutex1.acquire()
    print("线程B获得锁A")
    mutex1.release()
    print("线程B释放锁A")
    mutex2.release()
    print("线程B释放锁B")
if __name__ == "__main__":
    t1 = Thread(target=workA)
    t2 = Thread(target=workB)
    t1.start()
    t2.start()

结果为:

线程A获得锁A
线程B获得锁B

结果是程序就一直卡在接下来的过程那里,没办法执行下去了。

为了避免死锁,请不要让同一个线程,在同一个竟态资源内请求多把锁。一般我们的爬虫也只是用到一把锁而已。 用不到两把锁的。而且大部分时间是使用消息队列来实现共享资源处理的问题。

5. 守护线程
由于我们的线程是在同一个进程中的,因此当我们的主线程(主进程)退出的时候,那么所有的线程也会被 强行退出。默认情况下用threading模块创建出来的线程,如果子线程没有结束,但是主线程执行完了,那么 主线程会等待所有子线程执行完成之后才会退出程序(这种情况就是非守护的)。如果想要所有子线程在主 线程执行完成之后就马上退出,那么这个时候就需要将所有子线程设置为守护线程(也就是说明这个线程“不 重要”可以随着主线程一起退出)。即在调用start()方法之前调用setDaemon()方法将线程的daemon标志设为 True(setDaemon(True))。

非守护模式:(主线程执行完毕后,等待子线程结束后才一起退出程序)

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from threading import Thread,Lock
import time
num = 0
mutex = Lock() #互斥锁实例对象
def addnum():
    global num
    for i in range(10):
        time.sleep(1)
        mutex.acquire() # 上锁
        num += 1
        mutex.release() # 解锁
    print('子进程结束')
if __name__ == '__main__':
    t1 = Thread(target=addnum)
    t2 = Thread(target=addnum)
    t1.start()
    t2.start()
    print('主进程结束,num的值为:',num)

结果为:

主进程结束,num的值为: 0
子进程结束
子进程结束

守护模式(主线程结束后,不管子线程是否结束都一起退出程序):

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from threading import Thread,Lock
import time
num = 0
mutex = Lock() #互斥锁实例对象
def addnum():
    global num
    for i in range(10):
        time.sleep(1)
        mutex.acquire() # 上锁
        num += 1
        mutex.release() # 解锁
    print('子进程结束')
if __name__ == '__main__':
    t1 = Thread(target=addnum)
    t2 = Thread(target=addnum)
    t1.setDaemon(True)
    t2.setDaemon(True)
    t1.start()
    t2.start()
    print('主进程结束,num的值为:',num)

结果为:

主进程结束,num的值为: 0

守护线程在多线程爬虫中会经常使用。