Python. Как работает GIL в 3.8.5 и есть ли он?
На 4-х ядерном процессоре запускаю код по сложению списка на 50 10е6. Вариант с четырмя потоками из Threading занимает 7 сек, а c четырмя процессами из Pool 8 секунд. Что, в общем-то, одинаково.
import threading
import time
from multiprocessing import Pool
def thread_job(index):
global result, A
summ = A[index[0]]
j = index[0]+1
while j < index[1]:
summ += A[j]
j += 1
result += summ
def pool_job(x, y):
global A
summ = A[x]
j = x + 1
while j < y:
summ += A[j]
j += 1
return summ
def counter(b, n):
intervals = []
delta = int(b/n)
x, y = 0, delta
for i in range(n):
intervals.append((x, y))
x, y = x + delta, y + delta
return intervals
if __name__ == '__main__':
# Calculate with threads
b = 50000000
n = 4
A = [x for x in range(b)]
intervals = counter(b, n)
threads = [threading.Thread(target=thread_job, args=(i, )) for i in intervals]
time_start = time.time()
result = 0
for thread in threads:
thread.start()
for thread in threads:
thread.join()
print('First variant threads', time.time() - time_start, 'result', result)
# Calculate with multiprocessing
result = 0
time_start = time.time()
pool = Pool(processes=n)
r1 = pool.apply(pool_job, intervals[0])
r2 = pool.apply(pool_job, intervals[1])
r3 = pool.apply(pool_job, intervals[2])
r4 = pool.apply(pool_job, intervals[3])
Ответы (2 шт):
pool.apply работает синхронно. Для честного сравнения нужно сделать как-то так:
r1 = pool.apply_async(pool_job, intervals[0])
r2 = pool.apply_async(pool_job, intervals[1])
r3 = pool.apply_async(pool_job, intervals[2])
r4 = pool.apply_async(pool_job, intervals[3])
for r in [r1, r2, r3, r4]:
r.wait()
В варианте с последовательным выполнением, вы получаете результат практически такой же как и с многопоточностью. Что неудивительно и демонстрирует наличие и работу GIL в этом сценарии, который CPU-bound.
Небольшая разница во времени, а именно то, что в многопоточном варианте задачи выполняются 7 секунд против 8 в мнопроцессном варианте, можно объяснить тем, что для запуска задачи в других процессах, нужно передать массивы, которые обрабатываются от главного процесса к процессами обработчикам. В многопоточном варианте, это не нужно, данные разделяются между потоками одного процесса.
Кроме того у вас запуск процессов (Pool(processes=n)), тоже учитывается, а это дополнительное время.
Хотя вполне правильный ответ уже есть, хочу всё же акцентировать, что основная разница между вариантами кода видимо и правда в том, что в одном случае используются потоки, а в другом процессы. Как написано прямо в заголовках документации по этим библиотекам:
threading— Thread-based parallelismmultiprocessing— Process-based parallelism
Создание процессов - более трудоёмкая задача, чем создание потоков. Она требует сериализации данных порождающего процесса, для создания их полной копии для каждого порождённого процесса. Именно поэтому, кстати, вы и вынуждены во втором случае в явном виде возвращать посчитанные данные - изменять в этом случае глобальную переменную без толку, она у каждого процесса будет своя.
Поскольку у процессов своя копия данных, то GIL в случае процессов мешать вообще не должен, но при этом тратится время на копирование данных в порождённый процесс и передачу результата в порождающий процесс.
А вот в случае потоков как-раз GIL скорость работы кода и ограничивает. В итоге получается примерно одно и то же по скорости, хотя механика происходящего несколько разная. Думаю, что в зависимости от конкретики задач может получиться разница в скорости и в пользу процессов, если потоками будут слишком часто использоваться одни и те же данные и GIL в этом случае им будет очень сильно мешать.