2013-03-03 4 views
0

У меня есть набор процессов, связанных с процессором, которые используют любое количество ядер до 100% использования, пока их единственная синхронизация получает задания из очереди.Python RLock IO-Bound?

Как только я добавляю RLock, чтобы избежать сценариев худшего случая при обновлении каталога в файловой системе, загрузка CPU/core падает до 60%, как если бы процессы стали IO-привязаны.

Какое объяснение?

Это не общая скорость. Речь идет о загрузке CPU/core, поэтому Python 2/3, Cython или PyPy не имеет значения.

Обновление: Я дал частичный ответ на свой вопрос. Окончательное решение для моего конкретного случая состояло в изменении способа доступа к файловой системе, поэтому никакая синхронизация не требовалась («вид» карты/сокращение).

ответ

0

Измерение указывает, что RLock не связан с I/O. Хотя код, который синхронизируется с RLock (или), кажется примерно в 6 раз медленнее кода без синхронизации, код, который делает простейший ввод-вывод, на несколько порядков медленнее.

Ниже приведен код одного процесса, который использовался для измерения RLock с использованием как Python, так и PyPy. Я до сих пор не понимаю, почему накладные расходы 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() 
+0

Синхронизация между независимыми объектами (то есть процессами или хостами) требует, чтобы они продолжали спрашивать друг друга, если что-либо изменилось. У этого есть много тонких затрат, таких как обходные кеши, чтобы гарантировать, что все сущности видят одни и те же данные. Доступ к неиспользуемой памяти убивает производительность! – RobM

+0

@RobM В приведенном примере используется один процесс, и нет конкуренции за ресурсы. – Apalala

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 или «Fast UserSpace мьютекс». Как следует из названия, это хороший выбор для синхронизации.

Когда процесс блокируется в системном вызове типа futex, процесс блокирует ввод-вывод для всех целей и задач.

Все это означает, что multiprocessing.RLock эффективен и выполняет то, что он предназначен для этого. Поэтому, если производительность вашего приложения меньше, чем вы ожидаете при использовании синхронизации, скорее всего, ваш алгоритм виноват.

+0

Кажется, вы не читали мой собственный ответ, прежде чем отвечать, но спасибо за анализ. «RLock» может иметь эффективную реализацию, но она такая же дорогая, как и в моей программе. – Apalala

Смежные вопросы