你为什么要锁定线程?

10 投票
2 回答
7599 浏览
提问于 2025-04-16 19:48

我看了很多关于线程锁定的例子,但为什么我们需要锁定线程呢?

根据我的理解,当你启动线程但不让它们加入主线程时,它们会和主线程以及其他线程争夺资源,然后执行,有时会同时执行,有时不会。

锁定线程是否能确保它们不会同时执行呢?

另外,线程同时执行有什么问题吗?这样不是更好吗?(整体执行速度更快)

当你锁定线程时,是锁定所有线程,还是可以选择要锁定哪些线程?(不管锁定到底是干什么的…)

顺便提一下,我指的是在线程模块中使用像lock()和acquire这样的锁定函数…

2 个回答

12

首先,锁的作用是保护资源;线程并不是“锁定”或“解锁”,而是“获取”一个锁(在某个资源上)和“释放”一个锁(在某个资源上)。

你说得对,线程应该尽可能同时运行,但我们来看看这个情况:

y=10

def doStuff( x ):
    global y
    a = 2 * y
    b = y / 5
    y = a + b + x
    print y

t1 = threading.Thread( target=doStuff, args=(8,) )
t2 = threading.Thread( target=doStuff, args=(8,) )
t1.start()
t2.start()
t1.join()
t2.join()

现在,你可能知道这两个线程中的任何一个都可能先完成并打印结果。你会期待看到两个结果都是30。

但实际上,它们可能不会。

y是一个共享资源,在这个情况下,读取和写入y的操作属于一个叫做“关键区”的部分,这部分应该被锁保护。原因是你无法控制工作单位:任何一个线程都可能随时获得CPU的使用权。

想象一下:

t1正在顺利执行代码,突然遇到

a = 2 * y

此时t1的a = 20,并且暂时停止执行。t2变得活跃起来,而t1在等待更多的CPU时间。t2执行:

a = 2 * y
b = y / 5
y = a + b + x

此时全局变量y = 30

t2暂停了一会儿,t1又开始执行。它执行:

b = y / 5
y = a + b + x

因为在设置b的时候y是30,所以b = 6,y现在被设置为34。

打印的顺序是不可预测的,你可能先看到30,也可能先看到34。

如果使用锁,我们会得到:

global l
l = threading.Lock()
def doStuff( x ):
    global y
    global l
    l.acquire()
    a = 2 * y
    b = y / 5
    y = a + b + x
    print y
    l.release()

这就意味着这段代码是线性的——一次只能有一个线程在执行。但如果你的整个程序都是顺序执行的,那就不应该使用线程。使用线程的目的是为了提高速度,特别是当你有一部分代码可以在没有锁的情况下并行执行时。这也是为什么在一个双核系统上使用线程并不会让所有事情的性能翻倍的原因之一。

锁本身也是一个共享资源,但它必须是这样的:一旦一个线程获取了锁,所有其他尝试获取同一个锁的线程都会被阻塞,直到这个锁被释放。一旦释放,第一个继续前进并获取锁的线程会阻止所有其他等待的线程。

希望这些信息对你有帮助!

18

锁的作用是让多个线程一次只能有一个线程访问某个资源,而不是让所有线程同时去访问。

通常情况下,我们确实希望线程能够同时执行。但是想象一下,如果有两个线程同时往同一个文件写东西,它们的内容就会混在一起,结果两个线程都无法成功把自己想写的内容放进文件里。

这种问题可能不会经常出现。大多数时候,线程不会同时写文件。但有时候,可能在千次运行中就会出现一次。这样你可能会遇到一些看起来随机发生的错误,难以重现,也就难以修复。真让人头疼!

或者说…我曾经在一家公司遇到过这样的情况…你可能有这样的错误,但你并不知道,因为如果你的电脑只有几个CPU,这种错误发生的频率非常低,而几乎没有客户的电脑超过4个CPU。然后他们开始购买16个CPU的电脑…而你的软件会根据CPU核心数运行相应数量的线程,这样突然间你就会频繁崩溃或者得到错误的结果。

回到文件的问题。为了防止线程互相干扰,每个线程在写文件之前必须先获取文件的锁。一次只能有一个线程持有这个锁,所以一次只能有一个线程写文件。线程在完成写入后释放锁,这样其他线程就可以使用这个文件了。

如果线程写的是不同的文件,这个问题就不会出现。所以一个解决办法是让线程写不同的文件,必要时再把它们合并。但这并不总是可行的;有时候,只有一个资源。

这不一定是文件。假设你想统计多个文件中字母“A”的出现次数,每个文件一个线程。你可能会想,当然可以让所有线程每次看到“A”时都增加同一个内存位置的计数。但是!当你去增加这个计数变量时,计算机会先把这个变量读到一个寄存器中,增加寄存器的值,然后再把值存回去。如果两个线程同时读取这个值,同时增加它,然后同时存回去呢?它们都从10开始,增加到11,然后存回去。结果计数器是11,而应该是12:你丢失了一个计数。

获取锁可能会很耗时,因为你必须等到其他正在使用这个资源的人完成后才能使用。这就是为什么Python的全局解释器锁会成为性能瓶颈。所以你可能决定完全避免使用共享资源。比如,不再使用一个内存位置来保存文件中“A”的数量,而是让每个线程自己保存自己的计数,最后再把它们加起来(这和我之前提到的文件解决方案有点相似,挺有趣的)。

撰写回答