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

时间:2016-10-16 16:54:30

标签: python multithreading

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

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

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

4 个答案:

答案 0 :(得分:17)

GIL保护Python interals。这意味着:

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

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

    self.some_number += 1
    

    这将读取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
    7. 您使用锁来强制执行此执行顺序:

      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
      7. 编辑:让我们用一些显示解释行为的代码完成这个答案:

        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不能保护您免受从不同线程并发访问的对象内部状态的修改的影响,这意味着如果不采取措施,您仍然会弄乱事情。

因此,尽管事实上两个线程可能不会在同一确切时间运行,但它们仍然可以尝试操纵对象的内部状态(一次,间歇地一次),并且如果不能避免的话发生这种情况(使用某种锁定机制),您的代码可能/将最终失败。

问候。