我认为这是一个愚蠢的问题,但我仍然找不到它。实际上最好将它分成两个问题:
1)我是对的,我们可以有很多线程但是因为GIL在一瞬间只有一个线程正在执行?
2)如果是这样,为什么我们还需要锁?我们使用锁来避免两个线程试图读/写一些共享对象的情况,因为GIL twi线程无法在一瞬间执行,可以吗?
答案 0 :(得分:17)
GIL保护Python interals。这意味着:
但GIL不保护您自己的代码。例如,如果您有此代码:
self.some_number += 1
这将读取self.some_number
的值,计算some_number+1
,然后将其写回self.some_number
。
如果你在两个线程中执行此操作,则一个线程和另一个线程的操作(读取,添加,写入)可能会混合,因此结果是错误的。
这可能是执行的顺序:
self.some_number
(0)self.some_number
(0)some_number+1
(1)some_number+1
(1)self.some_number
self.some_number
您使用锁来强制执行此执行顺序:
self.some_number
(0)some_number+1
(1)self.some_number
self.some_number
(1)some_number+1
(2)self.some_number
import threading
import time
total = 0
lock = threading.Lock()
def increment_n_times(n):
global total
for i in range(n):
total += 1
def safe_increment_n_times(n):
global total
for i in range(n):
lock.acquire()
total += 1
lock.release()
def increment_in_x_threads(x, func, n):
threads = [threading.Thread(target=func, args=(n,)) for i in range(x)]
global total
total = 0
begin = time.time()
for thread in threads:
thread.start()
for thread in threads:
thread.join()
print('finished in {}s.\ntotal: {}\nexpected: {}\ndifference: {} ({} %)'
.format(time.time()-begin, total, n*x, n*x-total, 100-total/n/x*100))
有两个实现增量的功能。一个使用锁,另一个不使用。
函数increment_in_x_threads
在许多线程中实现递增函数的并行执行。
现在使用足够多的线程运行它几乎可以确定会发生错误:
print('unsafe:')
increment_in_x_threads(70, increment_n_times, 100000)
print('\nwith locks:')
increment_in_x_threads(70, safe_increment_n_times, 100000)
就我而言,它印有:
unsafe:
finished in 0.9840562343597412s.
total: 4654584
expected: 7000000
difference: 2345416 (33.505942857142855 %)
with locks:
finished in 20.564176082611084s.
total: 7000000
expected: 7000000
difference: 0 (0.0 %)
所以没有锁,就会出现很多错误(33%的增量失败)。另一方面,使用锁定它的速度要慢20倍。
当然,这两个数字都被炸毁了,因为我使用了70个线程,但这显示了一般的想法。
答案 1 :(得分:3)
GIL阻止同时执行多个线程,但不能在所有情况下同时执行。
在线程执行的I / O操作期间临时释放GIL。这意味着,多个线程可以同时运行。这是你仍然需要锁定的一个原因。
我不知道我在哪里找到这个参考....在视频或其他内容 - 很难查找,但你可以自己进一步调查
答案 2 :(得分:3)
在任何时候,是的,只有一个线程正在执行Python代码(其他线程可能正在执行某些IO,NumPy,等等)。这大部分都是正确的。但是,这在任何单处理器系统上都是如此,但人们仍然需要锁定单处理器系统。
看看以下代码:
queue = []
def do_work():
while queue:
item = queue.pop(0)
process(item)
有一个帖子,一切都很好。使用两个线程,您可能会从queue.pop()
获得异常,因为另一个线程首先在最后一个项目上调用queue.pop()
。所以你需要以某种方式处理它。使用锁是一个简单的解决方案。您也可以使用适当的并发队列(如queue
模块中的那样) - 但如果查看queue
模块,您会发现Queue
对象有{{{ 1}}在里面。所以你要么使用锁。
在没有必要锁的情况下编写多线程代码是一个常见的新手错误。你查看代码并思考,“这样可以正常工作”,然后发现很多小时之后发生了一些真正奇怪的事情,因为线程没有正确同步。
或者简而言之,在多线程程序中有许多地方需要阻止另一个线程修改结构,直到您完成应用某些更改。这允许您维护数据的不变量,如果您不能维护不变量,那么编写正确的代码基本上是不可能的。
或者尽可能以最短的方式,“如果您不在乎代码是否正确,则不需要锁定。”
答案 3 :(得分:0)
GIL不能保护您免受从不同线程并发访问的对象内部状态的修改的影响,这意味着如果不采取措施,您仍然会弄乱事情。
因此,尽管事实上两个线程可能不会在同一确切时间运行,但它们仍然可以尝试操纵对象的内部状态(一次,间歇地一次),并且如果不能避免的话发生这种情况(使用某种锁定机制),您的代码可能/将最终失败。
问候。