python 64式: 第21式、python 的多线程安全问题

1 何谓线程安全?
对多线程共享的数据的操作的行为是确定的。即在多线程的条件下,程序的运行也是按照期望的行为工作。2 线程安全的问题到底出现在哪里?
以一个简单的例子来说明:

import threading
import time

G_VALUE = 0


def editValue():
    global G_VALUE
    for i in range(1000000):
        G_VALUE += 1
        G_VALUE -= 1


def process():
    thread1 = threading.Thread(target=editValue)
    thread2 = threading.Thread(target=editValue)
    thread1.start()
    thread2.start()
    thread1.join()
    thread2.join()
    print G_VALUE


if __name__ == "__main__":
    process()

分析:
上述代码中有这样两步
G_VALUE += 1
G_VALUE -= 1
在python中
G_VALUE += 1
等同于
x = G_VALUE + 1
G_VALUE = x

存在这样的执行序列: 
第一个线程先执行了
x1 = G_VALUE + 1
然后第二个线程得到GIL锁后,也执行了
x2 = G_VALUE + 1
然后第一个线程继续执行
G_VALUE = x1
然后第二个线程继续执行
G_VALUE = x2
可以看到原本程序的预期是:
假设G_VALUE值为0, 线程1先对G_VALUE进行累加,G_VALUE值应该变为1,
然后线程2对G_VALUE进行累加,G_VALUE值应该变为2,但是最终经过上述分析
却变成了1。

所以上述代码才会再运行很多次的情况下,最终结果不是0。
问题的根本原因就是脏数据。一个线程用的并不是另一个线程修改后的数据,
而是两个线程都是基于原始数据的操作。
带来脏数据的原因就是无法保证:一个代码块(至少几行代码)能够被一个线程作为
完整的部分来执行,即无法保证代码块执行的原子性。

3 python内置的dict, list等是线程安全的吗?
这里是最容易混淆的地方,需要重点解释。

3.1 全局解释器锁
GIL是全局解释器锁,GIL锁确保了python多线程中同一个时间只有一个线程可以运行代码。
这带来的问题就是,python的多线程实际是一个伪多线程,即使采用
futures.ThreadPoolExecutor这种线程池想来提高提高python多线程的性能,提升的性能也并不大。
提升的性能应该就是节省了new多个线程的开销。
所以很多其他python项目中使用eventlet(即协程),通过eventlet中重写相关网络库,采用
select.epoll等高性能网络库来提高性能,做到并发,这点在openstack项目中应用
地很多。
那么是不是意味着GIL锁就可以保证锁有python程序都是线程安全的,反正最多同一个时间也只有一个线程运行?
答案是否定的,如果你的多线程程序中存在对共享资源的非原子操作,可能导致对这个共享资源修改到一半,GIL锁被分配给了另一个线程,另一个线程也对共享资源修改,带来脏数据,具体参见第2部分的分析。


3.2 dict,list线程是否安全分析
的确dict, list内置的list[0]=1, dict[1]='a'
这种操作是线程安全,
但是类似 list[0] += list[0] + 1 这种操作不是线程安全的。
因为
list[0] += list[0] + 1
这个等同于 
x = list[0] + 1
list[0] = x
这样的话,这个操作不是原子操作,就有可能第一个线程执行到
x = list[0] + 1
的时候,还没有来得及对list[0]进行修改,
第二个线程得到GIL锁,也对list[0]进行修改,导致第一个
线程的执行结果不是基于第二个线程的结果的基础上进行修改的。
即: 参见上述第2部分的分析,脏数据产生了。

3.3 原子操作才会线程安全。
加锁后,锁内部的程序要么不执行,要执行就会执行结束才会切换到其他线程,这其实相当于实现了一种“人工原子操作”,一整块代码当成一个整体运行,不会被打断。
这样做就可以防止资源修改的冲突。
如果程序本身就是原子操作,就自动实现了线程安全。
例如只对dict字典中进行设置键值对的操作
dict['a'] = 1
这个是原子操作,线程安全。
亦或是对dict进行删除dict键值对的操作
del dict['a']
这个是原子操作,线程安全。
但是dict['a'] += 1 这种是非原子操作,不是线程安全的。

3.4 原子操作列举
L.append(x)
L1.extend(L2)
x = L[i]
x = L.pop()
L1[i:j] = L2
L.sort()
x = y
x.field = y
D[x] = y
D1.update(D2)
D.keys()

3.4 非原子操作列举
i = i+1
L.append(L[-1])
L[i] = L[j]
D[x] = D[x] + 1


4 如何避免python多线程的不安全问题?
判断多线程对共享数据修改代码是否是原子操作,如果不是原子操作,则必须加锁。


参考:
[1] https://docs.python.org/3/glossary.html#term-global-interpreter-lock
[2] https://www.jianshu.com/p/a15acf790fab
[3] https://blog.csdn.net/LoveL_T/article/details/84337698
[4] https://blog.csdn.net/ywk_hax/article/details/82505181
[5] http://effbot.org/pyfaq/what-kinds-of-global-value-mutation-are-thread-safe.htm
[6] https://blog.csdn.net/u010649766/article/details/79740873
[7] http://www.aboutyun.com/thread-20085-1-1.html

猜你喜欢

转载自blog.csdn.net/qingyuanluofeng/article/details/87899974
今日推荐