【问题标题】:Is `with` statement __enter__ and __exit__ thread safe?`with` 语句 __enter__ 和 __exit__ 线程安全吗?
【发布时间】:2016-10-31 12:54:57
【问题描述】:

假设:

class A(object):
  def __init__(self):
    self.cnt = 0
  def __enter__(self):
    self.cnt += 1
  def __exit__(self, exc_type, exc_value, traceback)
    self.cnt -= 1
  1. 多线程时self.cnt += 1会不会被执行两次?
  2. 是否有可能对于同一个上下文管理器实例,在多线程中,__enter__ 被调用两次,__exit__ 只被调用一次,所以self.cnt 最终结果是1

【问题讨论】:

  • 多线程中的同一个实例是什么意思?无论您是否为每个线程提供自己可能使用的任何上下文管理器的实例,这都不相关。如果访问的数据在线程之间共享,则应使用互斥锁/锁来管理对它们的访问。
  • @metatoaster 我同意,每个线程都有自己的上下文管理器,但是我的同事说__enter__ 反正只是一个实例方法,它可能会被调用两次,所以它不是线程安全的,所以同一个上下文管理器实例的 __enter__ 不是线程安全的。我感到迷茫
  • 如果每个线程都有自己的实例,那么就没有共享数据。
  • 被叫两次是什么意思?你的意思是两个不同的线程每个调用一次,因此它被调用了两次?

标签: python thread-safety with-statement


【解决方案1】:

不,线程安全只能通过锁来保证。

多线程时self.cnt += 1会不会被执行两次?

如果你有两个线程运行它,它将被执行两次。三个线程,三次等。我不确定您的真正意思是什么,也许可以向我们展示您如何构建/执行与上下文管理器相关的这些线程。

是否有可能对于同一个上下文管理器实例,在多线程中,__enter__ 被调用两次,__exit__ 只被调用一次,所以 self.cnt 最终结果是 1?

是的,最终结果可以是非零的,但不是通过您假设的非对称调用进入和退出的机制。如果您跨多个线程使用相同的上下文管理器实例,您可以构建一个可以重现错误的简单示例,如下所示:

from threading import Thread

class Context(object):
    def __init__(self):
        self.cnt = 0
    def __enter__(self):
        self.cnt += 1
    def __exit__(self, exc_type, exc_value, traceback):
        self.cnt -= 1

shared_context = Context()

def run(thread_id):
    with shared_context:
        print('enter: shared_context.cnt = %d, thread_id = %d' % (
            shared_context.cnt, thread_id))
        print('exit: shared_context.cnt = %d, thread_id = %d' % (
            shared_context.cnt, thread_id))

threads = [Thread(target=run, args=(i,)) for i in range(1000)]

# Start all threads
for t in threads:
    t.start()

# Wait for all threads to finish before printing the final cnt
for t in threads:
    t.join()

print(shared_context.cnt)

你会不可避免地发现最终的shared_context.cnt 通常不会回到0,即使所有线程都以完全相同的代码启动和结束,即使进入和退出都被调用了更多成对或更少:

enter: shared_context.cnt = 3, thread_id = 998
exit: shared_context.cnt = 3, thread_id = 998
enter: shared_context.cnt = 3, thread_id = 999
exit: shared_context.cnt = 3, thread_id = 999
2
...
enter: shared_context.cnt = 0, thread_id = 998
exit: shared_context.cnt = 0, thread_id = 998
 enter: shared_context.cnt = 1, thread_id = 999
exit: shared_context.cnt = 0, thread_id = 999
-1

这主要是由于+= 运算符被解析为四个操作码,并且只有通过 GIL 才能保证单个操作码是安全的。更多细节可以在这个问题中找到:Is the += operator thread-safe in Python?

【讨论】:

  • 如果不使用共享上下文,把with shared_context:改成with Context(),cnt还能比1大吗?
  • 另一个问题是您的示例代码,shared_context.cnt 的最终结果(我的意思是 joinall())是否仍保证为零?
  • @est:您可以更改它并自己查看。如果您使用了with Context(),那么在手头的代码中,线程之间将没有共享数据。没有joinall() 调用(除非你的意思是所有线程,加入它们),但我包含的shared_context.cnt 的结果表明它不能保证为零 - 它可能是任何东西.
  • 我不认为.cnt 可以是任何东西,因为__enter____exit__ 对称的,最终在所有线程完成后,.cnt. 将等于完全为零,对吧?
  • 好吧,运行我的代码(多次),看看你是否不相信我的示例输出。由于没有锁保护以这种非线程安全方式修改 .cnt 的操作,因此它总是有可能无法反映现实,其中一个结果是它在正确的时间不为零 -其中一个线程在另一个线程读取但未分配其值后将其新值分配给.cnt
猜你喜欢
  • 1970-01-01
  • 2010-12-31
  • 1970-01-01
  • 2017-09-11
  • 2016-03-08
  • 1970-01-01
  • 2019-02-07
  • 2017-03-25
相关资源
最近更新 更多