Python RLock IO-Bound?

时间:2013-03-03 01:44:52

标签: python synchronization multiprocessing cpu-usage

我有一组CPU绑定的进程,只要他们的同步是从队列中获取作业,就可以将任意数量的内核用于100%利用率。

一旦我添加RLock以避免在更新文件系统中的目录时出现最坏情况,CPU /核心利用率就会下降到60%,就像进程已经成为IO绑定一样。

解释是什么?

这不是关于整体速度。它与CPU /核心利用率有关,因此Python 2/3,Cython或PyPy无关紧要。

更新:我对自己的问题给出了部分答案。我的特定案例的最终解决方案包括修改文件系统的访问方式,因此不需要同步(“一种”地图/缩小)。

2 个答案:

答案 0 :(得分:1)

这完全取决于multiprocessing实施RLock的方式。我知道多处理可以跨主机工作,这意味着同步原语可以跨套接字工作。如果这是真的,它会引入很多(可变的)延迟。

所以我做了一个实验。

以下是多个进程使用RLock的一个简单示例(以防止所有锁在同一进程中的任何快速路径):

#!/usr/bin/env python
import multiprocessing
from time import sleep

lock = multiprocessing.RLock()

def noop(myname):
    # nonlocal lock
    sleep(0.5)
    print myname, "acquiring lock"
    with lock:
        print myname, "has lock"
        sleep(0.5)
    print myname, "released lock"

sProc1 = multiprocessing.Process(target=noop, args=('alice',))
sProc2 = multiprocessing.Process(target=noop, args=('bob',))

sProc1.start()
sProc2.start()

sProc1.join()
sProc2.join()

运行此命令时,其输出如下所示:

alice acquiring lock
alice has lock
bob acquiring lock
alice released lock
bob has lock
bob released lock

很好,现在通过strace系统调用跟踪来运行它。

在下面的命令中,-ff选项告诉工具“跟随fork()”调用,即跟踪主要启动的任何进程。出于简洁的原因,我还使用-e trace=futex,write,它根据我在发布之前做出的结论来过滤输出。通常情况下,您将在没有-e选项的情况下运行,并使用文本编辑器/ grep来探索事后发生的事情。

# strace -ff -e trace=futex,write ./traceme.py
futex(0x7fffeafe29bc, FUTEX_WAIT_BITSET_PRIVATE|FUTEX_CLOCK_REALTIME, 1, NULL, 7fb92ac6c700) = -1 EAGAIN (Resource temporarily unavailable)
futex(0x7fb92a8540b0, FUTEX_WAKE_PRIVATE, 2147483647) = 0
futex(0x7fb92aa7131c, FUTEX_WAKE_PRIVATE, 2147483647) = 0
write(3, "\1\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0", 32) = 32
Process 25873 attached
Process 25874 attached
Process 25872 suspended
[pid 25873] write(1, "alice acquiring lock\n", 21alice acquiring lock
) = 21
[pid 25873] write(1, "alice has lock\n", 15alice has lock
) = 15
[pid 25874] write(1, "bob acquiring lock\n", 19bob acquiring lock
) = 19
[pid 25874] futex(0x7fb92ac91000, FUTEX_WAIT, 0, NULL <unfinished ...>
[pid 25873] futex(0x7fb92ac91000, FUTEX_WAKE, 1 <unfinished ...>
[pid 25874] <... futex resumed> )       = 0
[pid 25873] <... futex resumed> )       = 1
[pid 25874] write(1, "bob has lock\n", 13 <unfinished ...>
bob has lock
[pid 25873] write(1, "alice released lock\n", 20 <unfinished ...>
alice released lock
[pid 25874] <... write resumed> )       = 13
[pid 25873] <... write resumed> )       = 20
Process 25872 resumed
Process 25873 detached
[pid 25872] --- SIGCHLD (Child exited) @ 0 (0) ---
Process 25872 suspended
[pid 25874] write(1, "bob released lock\n", 18bob released lock
) = 18
Process 25872 resumed
Process 25874 detached
--- SIGCHLD (Child exited) @ 0 (0) ---

根据打印(write())消息和futex调用阻止并稍后恢复的模式,RLock使用futex或“快速”实现显而易见用户空间互斥体“。顾名思义,这是同步的一个很好的选择。

如果在futex之类的系统调用中阻止某个进程,则该进程会阻止所有意图和目的的I / O.

所有这些都意味着multiprocessing.RLock是有效率的并且正在做它的目的。因此,如果您的应用程序的性能低于使用同步时的预期,那么您的算法可能会受到责备。

答案 1 :(得分:0)

测量表明RLock不受I / O限制。虽然与RLock(或Semaphore)同步的代码似乎比没有同步的代码慢大约6倍,但执行最简单I / O的代码要慢几个数量级。

下面是我用Python和PyPy测量RLock开销的单进程代码。我仍然不明白为什么RLock的开销在单个进程中如此之高,或者为什么开销不能保持CPU /核心利用率,但结果表明使用标准同步原语比使用标准同步原语更有效试图自己动手。

# lock.py
import sys
import os
import timeit
from random import random
from multiprocessing import RLock, Semaphore

N=8*1024*1024

lock = RLock()
semaphore = Semaphore()

def touch(fname, times=None):
    if not os.path.isfile(fname):
        open(fname, 'wa').close()
    else:
        with file(fname, 'a'):
            os.utime(fname, times)

def wolock():
    return random()

def wlock():
    with lock:
        return random()

def wsem():
    with semaphore:
        return random()

def wfile():
   os.path.isfile('lock')
   touch('lock') 
   try:
        return random()
   finally:
        os.unlink('lock')

def run(get):
    result = []
    for i in xrange(N):
        result.append(get())
    return result

def main():
    t0 = timeit.timeit('lock.wolock()', setup='import lock', number=N)
    print '%8.3f %8.2f%% %s' % (t0, 100, 'unsynchronized')
    t1 = timeit.timeit('lock.wlock()', setup='import lock', number=N)
    print '%8.3f %8.2f%% %s' % (t1, 100*t1/t0, 'rlock')
    t2 = timeit.timeit('lock.wsem()', setup='import lock', number=N)
    print '%8.3f %8.2f%% %s' % (t2, 100*t2/t0, 'semaphore')
    t = timeit.timeit('lock.wfile()', setup='import lock', number=N)
    print '%8.3f %s' % (t, 'file system')

if __name__ == '__main__':
    main()