在烧瓶中使用全局变量安全吗



假设我想让一个flask服务器以相同的全局状态运行,该状态可能会通过请求进行修改。也就是说,让初始state由数字5表示。通过调用/getn,返回状态,并通过/inc增加状态。

from flask import Flask
from flask import request
import time
import os
app = Flask(__name__)
state = 5
@app.route('/inc')
def inc():
global state
print("sleep")
time.sleep(5)
state += 1
return 'done'
@app.route('/getn')
def getn():
global state
return f"{state}"

if __name__ == '__main__':
app.run(threaded=True, host="0.0.0.0")

现在我同时从两个不同的终端呼叫curl http://127.0.0.1:5000/inc。在阅读了相同的博客后,我预计在两次调用完成后,/getn会给我一个数字6,因为全局变量在flask中被认为是不安全的。但是,返回的状态等于7

s.o.能解释一下吗?此外,执行这项任务的正确方式是什么?

谢谢!

据我所知,当您运行Flash线程化时,语句state += 1可能存在问题,其中state是全局的,因为该语句在转换为字节码时变为:

0 LOAD_GLOBAL              0 (state)
2 LOAD_CONST               1 (1)
4 INPLACE_ADD
6 STORE_GLOBAL             0 (state)

假设state最初为5。现在,如果第一个线程执行上面的前三条指令并计算一个新值6,但在它有机会用第四条指令将这个新值存储回state之前,线程失去了对第二个线程的控制,会发生什么?因此,第二个线程从state加载相同的值5,重新计算state的新值,即6,并将其存储出来。现在,当第一个线程重新获得控制权时,它可以执行第四个字节码指令,并再次存储6。

因此,问题是+=运算符不是原子,即它被实现为一系列Python字节码指令,如果线程在第3条和第4条指令之间被中断,而另一个线程正在执行完成的相同操作,则其中一个更新将丢失。但是,在线程中发生这种情况的概率并没有那么高,因为多个线程不会并行执行字节码,因为它们必须锁定全局解释器锁(GIL(,并且只有当它们的时间片过期或进入I/O等待时才会失去对另一个线程的控制,这在这里不适用。

但为了完全安全,您可以也应该在锁的控制下进行更新。

from flask import Flask
from flask import request
import time
import os
from threading import Lock

app = Flask(__name__)
state_lock = Lock()
state = 5
@app.route('/inc')
def inc():
global state
print("sleep")
time.sleep(5)
with state_lock:
state += 1
return 'done'
@app.route('/getn')
def getn():
# The global statement is not really required
# since state is read/only, but it's okay:
global state
return f"{state}"

if __name__ == '__main__':
app.run(threaded=True, host="0.0.0.0")

这里有一个更好的例子来说明递增全局不是线程安全的。如果同时运行/inc端点和调用/getn端点的两个调用,则输出应为200000000,但现在线程很有可能会在重要的时候丢失一次或多次时间片,结果将不正确。然后在with state_lock:块内进行增量重试。

from flask import Flask
from threading import Lock

app = Flask(__name__)
state_lock = Lock()
state = 0
@app.route('/inc')
def inc():
global state
for _ in range(100_000_000):
state += 1
return 'done'
@app.route('/getn')
def getn():
return f"{state}"

if __name__ == '__main__':
app.run(threaded=True, host="0.0.0.0")

最新更新