Почему многопоточность увеличивает время обработки?

Я работал над базовой двумерной симуляцией DLA в режиме многозадачности. Диффузионная ограниченная агрегация (DLA) - это когда у вас есть частицы, выполняющие случайное блуждание и агрегирующие, когда они касаются текущего агрегата.

В моделировании у меня есть 10.000 частиц, движущихся в случайном направлении на каждом шаге. Я использую пул рабочих и очередь, чтобы кормить их. Я скармливаю им список частиц, и рабочий выполняет метод .updatePositionAndggregate() для каждой частицы.

Если у меня есть один рабочий, я кормлю его списком из 10.000 частиц, если у меня есть два рабочих, я кормлю их списком из 5.000 частиц каждый, если у меня 3 рабочих, я кормлю их списком из 3,333 частиц каждый, и т. д. и т. д.

Я покажу вам код для рабочего сейчас

class Worker(Thread):
    """
    The worker class is here to process a list of particles and try to aggregate
    them.
    """

    def __init__(self, name, particles):
        """
        Initialize the worker and its events.
        """
        Thread.__init__(self, name = name)
        self.daemon = True
        self.particles = particles
        self.start()

    def run(self):
        """
        The worker is started just after its creation and wait to be feed with a
        list of particles in order to process them.
        """

        while True:

            particles = self.particles.get()
            # print self.name + ': wake up with ' + str(len(self.particles)) + ' particles' + '\n'

            # Processing the particles that has been feed.
            for particle in particles:
                particle.updatePositionAndAggregate()

            self.particles.task_done()
            # print self.name + ': is done' + '\n'

И в основном потоке:

# Create the workers.
workerQueue = Queue(num_threads)
for i in range(0, num_threads):
    Worker("worker_" + str(i), workerQueue)

# We run the simulation until all the particle has been created
while some_condition():

    # Feed all the workers.
    startWorker = datetime.datetime.now()
    for i in range(0, num_threads):
        j = i * len(particles) / num_threads
        k = (i + 1) * len(particles) / num_threads

        # Feeding the worker thread.
        # print "main: feeding " + worker.name + ' ' + str(len(worker.particles)) + ' particles\n'
        workerQueue.put(particles[j:k])


    # Wait for all the workers
    workerQueue.join()

    workerDurations.append((datetime.datetime.now() - startWorker).total_seconds())
    print sum(workerDurations) / len(workerDurations)

Итак, я печатаю среднее время ожидания работниками завершения своих задач. Я поэкспериментировал с другим номером резьбы.

| num threads | average workers duration (s.) |
|-------------|-------------------------------|
| 1           | 0.147835636364                |
| 2           | 0.228585818182                |
| 3           | 0.258296454545                |
| 10          | 0.294294636364                |

Мне действительно интересно, почему добавление воркеров увеличивает время обработки, я думал, что, по крайней мере, наличие двух воркеров уменьшит время обработки, но оно резко увеличивается с 0,14 с. до 0,23 с. Вы можете мне объяснить почему?

РЕДАКТИРОВАТЬ: Итак, объяснение - это реализация потоковой передачи Python, есть ли способ, чтобы я мог иметь реальную многозадачность?


person Baptiste Pernet    schedule 17.03.2015    source источник
comment
Из-за GIL глобальная блокировка интерпретатора.   -  person ForceBru    schedule 17.03.2015
comment
точная единственная причина, по которой я не люблю питон. очень плохая мм и модель резьбы.   -  person Jason Hu    schedule 17.03.2015
comment
Стоит отметить, что некоторого параллелизма можно добиться, используя многопроцессорность. Затем количество потоков ограничивается количеством доступных процессоров / ядер (2 или 4 на большинстве ПК).   -  person Fran Borcic    schedule 17.03.2015


Ответы (4)


Это происходит потому, что потоки не выполняются одновременно, поскольку Python может выполнять только один поток за раз из-за GIL (Global Interpreter Lock).

Когда вы создаете новый поток, все замирает, кроме этого потока. Когда он останавливается, выполняется другой. Для создания потоков требуется много времени.

Проще говоря, код вообще не имеет значения, поскольку любой код, использующий 100 потоков, МЕДЛЕН, чем код, использующий 10 потоков в Python (если больше потоков означает большую эффективность и большую скорость, что не всегда верно) .

Вот точная цитата из документации Python:

Подробности реализации CPython:

В CPython из-за глобальной блокировки интерпретатора только один поток может выполнять код Python одновременно (даже если некоторые библиотеки, ориентированные на производительность, могут преодолеть это ограничение). Если вы хотите, чтобы ваше приложение лучше использовало вычислительные ресурсы многоядерных машин, рекомендуется использовать multiprocessing или concurrent.futures.ProcessPoolExecutor. Однако многопоточность по-прежнему является подходящей моделью, если вы хотите одновременно выполнять несколько задач, связанных с вводом-выводом.

Википедия о GIL

StackOverflow о GIL

person ForceBru    schedule 17.03.2015

Потоки в python (по крайней мере, в 2.7) НЕ выполняются одновременно из-за GIL: https://wiki.python.org/moin/GlobalInterpreterLock - они работают в одном процессе и совместно используют ЦП, поэтому вы не можете использовать потоки для ускорения вычислений.

Если вы хотите использовать параллельные вычисления для ускорения вычислений (по крайней мере, в python2.7), используйте процессы - пакет multiprocessing.

person Jan Spurny    schedule 17.03.2015
comment
Я пробовал с многопроцессорностью, но накладные расходы, добавленные для управления процессом и объектом общей памяти, стоят только для большого количества частиц - person Baptiste Pernet; 17.03.2015
comment
@BaptistePernet, потому что вам нужно изменить гораздо больше, чем одну библиотеку - параллельные вычисления совершенно разные, и обычно вам нужно изменить образ мышления - один из действительно хороших подходов - переписать вашу проблему, чтобы ее можно было использовать в map-reduce. Тогда вы получите максимум от параллельной архитектуры. Использование общих состояний приводит к все большему и большему количеству проблем. Я искренне верю, что в большинстве случаев следует избегать общих состояний. Кроме того, чтобы действительно получить выгоду от параллельной обработки, вам обычно требуется много процессоров, потому что, как вы, вероятно, заметили, всегда накладные расходы. - person Jan Spurny; 18.03.2015

Это связано с глобальной блокировкой интерпретатора Python. К сожалению, с GIL в Python потоки будут блокировать ввод-вывод и, как таковые, никогда не будут превышать использование 1 ядра ЦП. Загляните сюда, чтобы начать понимать GIL: https://wiki.python.org/moin/GlobalInterpreterLock

Проверьте свои запущенные процессы (например, диспетчер задач в Windows) и обратите внимание, что вашим приложением Python используется только одно ядро.

Я бы посоветовал взглянуть на многопроцессорность в Python, которой не препятствует GIL: https://docs.python.org/2/library/multiprocessing.html

person Christian Groleau    schedule 17.03.2015
comment
Я пробовал с многопроцессорностью, но накладные расходы, добавленные для управления процессом и объектом общей памяти, стоят только для большого количества частиц - person Baptiste Pernet; 17.03.2015

На создание другого потока и начало его обработки требуется время. Поскольку у нас нет контроля над планировщиком, я готов поспорить, что оба этих потока будут запланированы на одном ядре (поскольку работа настолько мала), поэтому вы добавляете время, необходимое для создания потока, а не выполняется параллельная обработка

person MobileMon    schedule 17.03.2015
comment
Как уже упоминалось, проблема в GIL, однако я оставлю это здесь для академических целей, потому что на языках, где существует настоящая многопоточность, это применимо. - person MobileMon; 18.03.2015