如果我们有GIL,为什么我们需要线程锁?

Pau*_*aul 20 python multithreading

我相信这是一个愚蠢的问题,但我仍然找不到它.实际上最好将它分成两个问题:

1)我是对的,我们可以有很多线程但是因为GIL在一瞬间只有一个线程正在执行?

2)如果是这样,为什么我们还需要锁?我们使用锁来避免两个线程试图读/写某些共享对象的情况,因为GIL twi线程无法在一瞬间执行,可以吗?

zvo*_*one 22

GIL保护Python内部.这意味着:

  1. 因为多线程,你不必担心解释器出错了
  2. 大多数事情并不真正并行运行,因为python代码是由GIL顺序执行的

但GIL不保护您自己的代码.例如,如果您有此代码:

self.some_number += 1
Run Code Online (Sandbox Code Playgroud)

那将是读取self.some_number,计算some_number+1,然后写回来的价值self.some_number.

如果在两个线程中执行此操作,则一个线程和另一个线程的操作(读取,添加,写入)可能会混合,因此结果是错误的.

这可能是执行的顺序:

  1. thread1读取self.some_number(0)
  2. thread2读取self.some_number(0)
  3. thread1计算some_number+1(1)
  4. thread2计算some_number+1(1)
  5. thread1写1到 self.some_number
  6. thread2将1写入 self.some_number

您使用锁来强制执行此执行顺序:

  1. thread1读取self.some_number(0)
  2. thread1计算some_number+1(1)
  3. thread1写1到 self.some_number
  4. thread2读取self.some_number(1)
  5. thread2计算some_number+1(2)
  6. thread2写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))
Run Code Online (Sandbox Code Playgroud)

有两个实现增量的功能.一个使用锁,另一个不使用.

功能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)
Run Code Online (Sandbox Code Playgroud)

在我的情况下,它打印:

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 %)
Run Code Online (Sandbox Code Playgroud)

所以没有锁,就会出现很多错误(33%的增量失败).另一方面,使用锁定它的速度要慢20倍.

当然,这两个数字都被炸毁,因为我使用了70个线程,但这显示了一般的想法.

  • 好例子@zvone。**此外,仅需注意一些有关GIL工作原理的信息:线程保持对SINGLE执行上下文(解释器)的控制,直到任一A)它们调用了一个阻塞但具有线程意识的函数(即“ time.sleep”会暂停)或B)GIL强制将控制权从连续运行了太多Python操作码的线程中**(默认数量为100,我认为...有一种检查方法)。在您的示例代码中,情况B正在发生。它们循环执行,直到GIL强行重新获得控制权并将其交给另一个线程,即使在操作码中间! (2认同)

Die*_*Epp 5

在任何时候,是的,只有一个线程正在执行 Python 代码(其他线程可能正在执行一些 IO、NumPy 等)。这大部分是正确的。然而,这在任何单处理器系统上都是微不足道的,但人们仍然需要在单处理器系统上加锁。

看看下面的代码:

queue = []
def do_work():
    while queue:
        item = queue.pop(0)
        process(item)
Run Code Online (Sandbox Code Playgroud)

一个线程,一切都很好。对于两个线程,您可能会收到异常,queue.pop()因为另一个线程首先调用queue.pop()了最后一项。所以你需要以某种方式处理它。使用锁是一个简单的解决方案。您还可以使用适当的并发队列(如在queue模块中)——但是如果您查看queue模块内部,您会发现该Queue对象threading.Lock()内部有一个。因此,无论哪种方式,您都在使用锁。

在没有必要的锁的情况下编写多线程代码是一个常见的新手错误。您查看代码并认为“这会正常工作”,然后在几个小时后发现确实发生了一些奇怪的事情,因为线程没有正确同步。

或者简而言之,在多线程程序中有很多地方需要防止另一个线程修改结构,直到您完成一些更改。这允许您维护数据上的不变量,如果您不能维护不变量,那么基本上不可能编写正确的代码。

或者用尽可能最短的方式,“如果你不在乎你的代码是否正确,你就不需要锁。”