Python:无锁机制下多个线程并发访问函数

4 投票
4 回答
3682 浏览
提问于 2025-04-17 19:03

当多个线程同时访问同一个函数时,我们是否需要明确地实现机制呢?

我有一个使用线程的程序。这个程序里有两个线程,t1t2t1 用于执行 add1(),而 t2 用于执行 subtract1()。这两个线程同时访问同一个函数 myfunction(caller,num)

1. 我在这个程序中定义了一个简单的锁机制,使用了一个变量 functionLock。这个做法可靠吗?还是说我们需要修改它?

import time, threading

functionLock = '' # blank means lock is open        

def myfunction(caller,num):
    global functionLock
    while functionLock!='': # check and wait until the lock is open
        print "locked by "+ str(functionLock)
        time.sleep(1)

    functionLock = caller # apply lock

    total=0
    if caller=='add1':
        total+=num
        print"1. addition finish with Total:"+str(total)
        time.sleep(2)
        total+=num
        print"2. addition finish with Total:"+str(total)
        time.sleep(2)
        total+=num
        print"3. addition finish with Total:"+str(total)

    else:
        time.sleep(1)
        total-=num
        print"\nSubtraction finish with Total:"+str(total)

    print '\n For '+caller+'() Total: '+str(total)

    functionLock='' # release the lock


def add1(arg1, arg2):

    print '\n START add'
    myfunction('add1',10)
    print '\n END add'        


def subtract1():

  print '\n START Sub'  
  myfunction('sub1',100)   
  print '\n END Sub'


def main():

    t1 = threading.Thread(target=add1, args=('arg1','arg2'))
    t2 = threading.Thread(target=subtract1)
    t1.start()
    t2.start()


if __name__ == "__main__":
  main()

程序的输出结果如下:

START add
START Sub
1. addition finish with Total:10
locked by add1
locked by add1
2. addition finish with Total:20
locked by add1
locked by add1
3. addition finish with Total:30 
locked by add1
 For add1() Total: 30
 END add
Subtraction finish with Total:-100
 For sub1() Total: -100
 END Sub

2. 如果我们不使用锁,是否可以?

即使我不使用上面程序中定义的锁机制,两个线程 t1t2 的结果也是一样的。这是否意味着 Python 在多个线程访问同一个函数时会自动实现锁?

不使用锁 functionLock 的程序输出结果:

START add
START Sub
1. addition finish with Total:10
Subtraction finish with Total:-100
For sub1() Total: -100
END Sub
2. addition finish with Total:20
3. addition finish with Total:30
For add1() Total: 30
END add

谢谢!

4 个回答

1

虽然我对Python了解不多,但我觉得这跟其他语言差不多:

只要函数里面没有用到在外面声明的变量(这些变量可以被多个线程共享),那么就不需要使用锁。看起来你的函数是符合这个条件的。

不过,输出到控制台的内容可能会出现混乱。

2
  1. 在你的代码里,你自己实现了一个叫做 自旋锁 的东西。虽然这样做是可以的,但我觉得在Python中不太推荐,因为可能会导致性能问题。

  2. 我用一个大家都知道的搜索引擎(以 G 开头)搜索了“python lock”。其中一个前几条结果就是这个:Python中的线程同步机制。这看起来是个不错的入门文章。

  3. 关于代码本身:当你对一个共享资源进行的操作不是原子操作时,就应该加锁。目前看起来你的代码中没有这样的资源。

1

除了这个讨论串中关于忙等待变量的其他评论外,我想指出的是,你没有使用任何原子交换操作,这可能会导致并发错误。即使你的测试执行没有出现这些问题,但如果在不同的时间重复执行多次,可能会出现以下情况:

线程 #1 执行 while functionLock!='',结果得到 False。然后,线程 #1 被中断(被抢占去执行其他任务),接着线程 #2 执行同样的代码 while functionLock!='',也得到 False。在这个例子中,两个线程都进入了关键区域,这显然不是你想要的。特别是在任何修改 total 的代码行中,结果可能不是你预期的,因为两个线程可以同时在这个区域。看看下面的例子:

total 的初始值是 10。为了简单起见,假设 num 始终是 1。线程 #1 执行 total+=num,这个操作实际上分为三个步骤:(i) 读取 total 的值,(ii) 将它与 num 相加,(iii) 将结果存回 total。如果在步骤 (i) 后,线程 #1 被抢占,线程 #2 执行 total-=num,那么 total 就变成了 9。然后,线程 #1 恢复执行,但它已经读取了 total = 10,所以它加上 1,结果将 11 存入 total 变量。这实际上让线程 #2 的减法操作变成了无效操作。

注意,在 @ron-klein 提到的维基百科文章中,代码使用了 xchg 操作,这个操作可以原子性地交换一个寄存器和一个变量。这对于锁的正确性是至关重要的。总之,如果你想避免难以调试的并发错误,绝不要自己实现锁来替代原子操作。

[编辑] 我刚注意到实际上 total 是你代码中的一个局部变量,所以这种情况不可能发生。然而,我相信你并不知道这是你代码正常工作的原因,因为你说“这是否意味着当多个线程访问同一个函数时,Python 会自动实现锁”,这并不正确。请尝试在 myfunction 的开头添加 global total,然后多次执行线程,你应该会在输出中看到错误。[/编辑]

撰写回答