python - Python 2.X 中的 `print` 内置函数是原子函数吗?

标签 python multithreading python-2.7 atomic

本周我一直在探索 Python 中线程的内部实现。令人惊奇的是,我每天都对自己的无知感到惊讶;不知道我想知道什么,这就是让我发痒的原因。

我注意到我在 Python 2.7 下作为多线程应用程序运行的一段代码中有一些奇怪的地方。我们都知道Python 2.7默认在100条虚拟指令后进行线程切换。调用函数就是一条虚拟指令,例如:

>>> from __future__ import print_function
>>> def x(): print('a')
... 
>>> dis.dis(x)
  1           0 LOAD_GLOBAL              0 (print)
              3 LOAD_CONST               1 ('a')
              6 CALL_FUNCTION            1
              9 POP_TOP             
             10 LOAD_CONST               0 (None)
             13 RETURN_VALUE        

如您所见,在加载全局 print 和加载常量 a 后,函数被调用。因此,调用函数是原子的,因为它是通过一条指令完成的。因此,在多线程程序中,要么函数(此处为 print)运行,要么“正在运行”的线程在函数获得要运行的更改之前被中断。也就是说,如果 LOAD_GLOBALLOAD_CONST 之间发生上下文切换,指令 CALL_FUNCTION 将不会运行。

请记住,在上面的代码中,我使用的是 from __future__ import print_function,我现在实际上是在调用内置函数,而不是 print 语句。让我们看一下函数 x 的字节码,但这次使用 print 语句:

>>> def x(): print "a"          # print stmt
... 
>>> dis.dis(x)
  1           0 LOAD_CONST               1 ('a')
              3 PRINT_ITEM          
              4 PRINT_NEWLINE       
              5 LOAD_CONST               0 (None)
              8 RETURN_VALUE 

在这种情况下,LOAD_CONSTPRINT_ITEM 之间很可能发生线程上下文切换,从而有效地阻止 PRINT_NEWLINE 指令执行。因此,如果您有一个像这样的多线程程序(借用《Python 编程》第 4 版并稍作修改):

def counter(myId, count):
    for i in range(count):
        time.sleep(1)
        print ('[%s] => %s' % (myId, i)) #print (stmt) 2.X 

for i in range(5):
    thread.start_new_thread(counter, (i, 5))

time.sleep(6)  # don't quit early so other threads don't die

输出可能会或可能不会如下所示,具体取决于线程的切换方式:

[0] => 0
[3] => 0[1] => 0
[4] => 0
[2] => 0
...many more...

使用print语句就可以了。

如果我们使用内置 print 函数更改 print 语句,会发生什么?让我们看看:

from __future__ import print_function
def counter(myId, count):
    for i in range(count):
        time.sleep(1)

        print('[%s] => %s' % (myId, i))  #print builtin (func)

for i in range(5):
    thread.start_new_thread(counter, (i, 5))

time.sleep(6) 

如果您多次运行此脚本足够长的时间,您将看到如下内容:

[4] => 0
[3] => 0[1] => 0
[2] => 0
[0] => 0
...many more...

鉴于上述所有解释,这怎么可能? print 现在是一个函数,为什么它打印传入的字符串而不是新行? print 在打印字符串的末尾打印 end 的值,默认设置为 \n。本质上,对函数的调用是原子的,它到底是怎么被中断的?

让我们大吃一惊:

def counter(myId, count):
    for i in range(count):
        time.sleep(1)
        #sys.stdout.write('[%s] => %s\n' % (myId, i))
        print('[%s] => %s\n' % (myId, i), end='')

for i in range(5):
    thread.start_new_thread(counter, (i, 5))

time.sleep(6) 

现在总是打印新行,不再出现困惑的输出:

[1] => 0
[2] => 0
[0] => 0
[4] => 0
...many more...

在字符串中添加 \n 现在显然证明 print 函数不是原子函数(即使它是一个函数),本质上它只是充当print 语句。然而,dis.dis 语无伦次或愚蠢地告诉我们它是一个简单的函数,因此是一个原子操作?!

注意:我从不依赖线程的顺序或时间来保证应用程序正常工作。坦率地说,这仅用于测试目的,适合像我这样的极客。

最佳答案

你的问题是基于中心前提

Calling a function therefore is atomic as it's done with a single instruction.

这是完全错误的。

首先,执行CALL_FUNCTION操作码可能涉及执行额外的字节码。最明显的情况是执行的函数是用 Python 编写的,但即使是内置函数也可以自由调用其他可能用 Python 编写的代码。例如,print 调用 __str__write 方法。

其次,即使在 C 代码中间,Python 也可以自由地释放 GIL。它通常对 I/O 和其他可能需要一段时间而不需要执行 Python API 调用的操作执行此操作。 Python 2.7 file object implementation 中有 23 次使用 FILE_BEGIN_ALLOW_THREADSPy_BEGIN_ALLOW_THREADS 宏。单独的一个,包括 file.write 实现中的一个,print 依赖于该实现。

关于python - Python 2.X 中的 `print` 内置函数是原子函数吗?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/45225311/

相关文章:

从不正确的线程访问 Java android Realm

python - 有没有办法用 matplotlib 或任何其他库绘制 3D 饼图?

python - python2 存在 site-packages 文件夹,python3 不存在

python - 我能在python中找到哪些参数是 "pre-assigned"吗?

java - 安卓 : Thread not passing data to Handler

multithreading - Clojure:pvalues 与 pcalls

mysql - 如何在 Pandas 数据框连接中将表 A 中的 A 列乘以表 B 中的 B 列?

python - 将时间戳列表从 timedelta 转换为可读字符串格式

python - 使用 Freebusy 到 'primary' 以外的其他日历

python - 调试斜纹异常错误