从不同线程修改Python字典

16 投票
3 回答
16871 浏览
提问于 2025-04-16 09:06

谈到多线程的时候,我知道你得确保在一个线程修改某个变量的时候,另一个线程不能同时也在修改这个变量,因为这样你的修改可能会丢失(比如在增加一个计数器的时候)。

那字典也是这样吗?还是说字典其实是一堆变量的集合?

如果每个线程都要锁住整个字典,那程序的运行速度会大大变慢,而每个线程其实只需要对字典里自己那一小部分有写的权限。

如果这样做不行,Python里有没有类似PHP那样的可变变量?

3 个回答

0

你需要做的是,不让多个线程直接访问共享的数据结构,而是用一些东西把访问包裹起来,这样可以确保互斥,也就是同一时间只有一个线程可以访问,比如用一个叫做 互斥锁

让访问原始结构的方式看起来一样(比如 shared[id] = value)需要多花一点功夫,但其实也没那么复杂。

26

字典也是这样吗?还是说字典其实是一种变量的集合?

我们来更一般地说:

什么是“原子操作”?

根据维基百科的解释:

在并发编程中,一个操作(或一组操作)被称为原子、线性化、不可分割或不可中断,如果它在系统的其他部分看起来是瞬间完成的。原子性保证了与并发进程的隔离。

那么在Python中这意味着什么呢?

这意味着每条字节码指令都是原子的(至少对于Python <3.2版本来说,在新的全局解释器锁(GIL)出现之前)。

为什么会这样呢??

因为Python(CPython)使用了全局解释器锁(GIL)。CPython解释器使用锁来确保同一时间只有一个线程在解释器中运行,并使用一个“检查间隔”(见sys.getcheckinterval())来知道在切换线程之前要执行多少条字节码指令(默认设置为100)。

那么这又意味着什么呢??

这意味着可以用一条字节码指令表示的操作是原子的。例如,增加一个变量的值并不是原子的,因为这个操作需要三条字节码指令来完成:

>>> import dis

>>> def f(a):
        a += 1

>>> dis.dis(f)
  2           0 LOAD_FAST                0 (a)
              3 LOAD_CONST               1 (1)      <<<<<<<<<<<< Operation 1 Load
              6 INPLACE_ADD                         <<<<<<<<<<<< Operation 2 iadd
              7 STORE_FAST               0 (a)      <<<<<<<<<<<< Operation 3 store
             10 LOAD_CONST               0 (None)
             13 RETURN_VALUE        

那字典呢??

有些操作是原子的;例如,这个操作就是原子的:

d[x] = y
d.update(d2)
d.keys()

你可以自己看看:

>>> def f(d):
        x = 1
        y = 1
        d[x] = y

>>> dis.dis(f)
  2           0 LOAD_CONST               1 (1)
              3 STORE_FAST               1 (x)

  3           6 LOAD_CONST               1 (1)
              9 STORE_FAST               2 (y)

  4          12 LOAD_FAST                2 (y)
             15 LOAD_FAST                0 (d)
             18 LOAD_FAST                1 (x)
             21 STORE_SUBSCR                      <<<<<<<<<<< One operation 
             22 LOAD_CONST               0 (None)
             25 RETURN_VALUE   

想了解STORE_SUBSCR的作用,可以查看这里

但是如你所见,这并不是完全正确,因为这个操作:

             ...
  4          12 LOAD_FAST                2 (y)
             15 LOAD_FAST                0 (d)
             18 LOAD_FAST                1 (x)
             ...

可能会导致整个操作变得不原子。为什么呢?假设变量x也可能被另一个线程修改……或者你希望另一个线程清空你的字典……我们可以列举很多可能出错的情况,所以这很复杂!在这里我们可以应用墨菲定律: “任何可能出错的事情,都会出错”。

那么现在该怎么办呢?

如果你仍然想在多个线程之间共享变量,使用锁:

import threading

mylock = threading.RLock()

def atomic_operation():
    with mylock:
        print "operation are now atomic"
8

我觉得你对线程安全这个话题有些误解。其实,这并不是单纯关于变量的问题(或者说可变变量——这些东西本身就很糟糕,在这里和其他地方一样毫无意义,甚至可能有害),而是关于线程如何同时访问可变数据的问题。举个例子,线程出错的方式有很多,主要都是因为多个线程在重叠的时间内访问同一个可变数据。具体来说,就是这样的过程:

  • 线程N从某个地方获取数据(可能是内存中的某个位置、磁盘上的文件、字典中的一个槽位,基本上任何可变的数据)
  • 线程M也从同一个地方获取数据
  • 线程N修改了这份数据
  • 线程M也修改了这份数据
  • 线程N用修改后的数据覆盖了源数据
  • 线程M也用修改后的数据覆盖了源数据
  • 结果:线程N的修改被丢失了,新的共享值没有考虑到线程N的修改

这个问题同样适用于字典和可变变量(可变变量其实就是一种糟糕的语言实现,它只允许用字符串作为键的字典)。解决这个问题的办法就是一开始就不要使用共享状态(函数式编程语言通过不鼓励甚至完全禁止可变性来做到这一点,这对它们来说效果很好),或者给所有共享的数据加上某种锁(这很难做到,但如果做对了,至少能确保它正确工作)。如果没有两个线程共享字典中的任何东西,那就没问题了——但你应该把所有东西分开,以更好地确保它们真的没有共享任何内容。

撰写回答