Python线程不安全示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
from multiprocessing import Process, Value
from threading import Thread

import time

"""
测试多线程、多进程、gevent协程下的线程安全问题(或者说是进程安全/协程安全)
再python3.7下测试, 运行前先安装gevent模块: pip install gevent

一次运行结果实例:
test_multithread: 194
test_multiprocess: 0
test_multiprocess_s: 192
test_gevent: 13

"""

class Foo:
def __init__(self):
self.i = 0

def inc(self):
tmp = self.i
tmp += 1
time.sleep(0.001)
self.i = tmp

def inc_2(self):
tmp = self.i
tmp += 1
self.i = tmp


def test_multithread():
"""
在上面的inc方法加上time.sleep(0.001)后,最后打印出来的值远远小于线程数2000;
如果是time.sleep(1),这里一般是打印出1;
如果没sleep, 通常打印出2000(即等于线程数),虽然这是线程不安全的。可能是由于CPU运行飞快、GIL的存在等原因,导致出现看似线程安全的结果
:return:
"""
foo = Foo()
thread_list = []
for i in range(2000):
thread = Thread(target=foo.inc)
thread_list.append(thread)
for thread in thread_list:
thread.start()
for thread in thread_list:
thread.join()
print("test_multithread:", foo.i)

def test_gevent():
"""
gevent使用协程来实现并发。协程只在发生I/O、调用sleep等的时候进行切换。一般不用考虑线程安全的问题
不过这里最后输出的将会远远小于2000。因为在遇到sleep时进行了协程切换,所以协程切换不能发生在应当是原子操作的代码之间
:return:
"""
# 下面这句patch会让test_multithread函数卡住无法返回(。 所以test_gevent函数需在测试完多线程后再执行
from gevent import monkey; monkey.patch_all()
import gevent

foo = Foo()
threads = [gevent.spawn(foo.inc) for i in range(2000)]
gevent.joinall(threads)
print("test_gevent:", foo.i)

def test_multiprocess():
"""
每个进程的foo变量各自独立,不共享,所以这里打印出0,在主进程的foo.i没被计算到,
需要将foo改为进程共享变量,才能观察其是 进程不安全 的,看下面的test_multiprocess_2函数
:return:
"""
foo = Foo()
process_list = []
for i in range(200):
process = Process(target=foo.inc)
process_list.append(process)
for process in process_list:
process.start()
for process in process_list:
process.join()
print("test_multiprocess:", foo.i)


def inc_s(i):
i.value += 1
# 加锁的写法:
# with i.get_lock():
# i.value += 1

def test_multiprocess_s():
"""
在+=操作不加锁的情况下,这里打印的将是小于进程数200。 这是个进程不安全的例子。
在multiprocessing模块官方文档有这么一句话说multiprocessing.Value是进程/线程安全的
> These shared objects will be process and thread-safe.
这句话是说在python字节代码层面上对该类型的读或写操作时是进程/线程安全的。但对于整个自增操作就不是了。
以下是自增变量的字节代码,并注释了Value加锁的位置,参考 https://eli.thegreenplace.net/2012/01/04/shared-counter-with-pythons-multiprocessing 这篇文章
0 LOAD_FAST 0 (val)
3 DUP_TOP
#<--- Value lock acquired
4 LOAD_ATTR 0 (value)
#<--- Value lock released
7 LOAD_CONST 1 (1)
10 INPLACE_ADD
11 ROT_TWO
#<--- Value lock acquired
12 STORE_ATTR 0 (value)
#<--- Value lock released
:return:
"""
shared_value = Value("i", 0)
process_list = []
for i in range(200):
process = Process(target=inc_s, args=(shared_value, ))
process_list.append(process)
for process in process_list:
process.start()
for process in process_list:
process.join()
print("test_multiprocess_s: ", shared_value.value)


if __name__ == '__main__':
test_multithread()
test_multiprocess()
test_multiprocess_s()
test_gevent()



本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!