Почему CPU bound выполняется за одно и тоже время в потоках и процессах?
Задание 1, 2, 3 - нужно сделать 10 запросов на сайт синхронно, потоками и процессами. На скрине видно что потоки справляются лучше всех, как и предполагалось.
Задание 4, 5, 6 - нужно 10 раз запустить цикл (range(5_000_000)). По идее процессы должны дать лучший результат, потому что проходят цикл одновременно. Но на скрине видно что у всех способов примерно одинаковое время выполнения.
Вывод на моей машине (Процессор 2 ядра 4 потока):
Задание 4 Time: 3.2805144786834717
Задание 5 Time: 3.3142642974853516
Задание 6 Time: 3.6553516387939453
Вопрос: что я сделал не так, и почему результат не такой как предполагалось???
import time
import requests
from threading import Thread
from multiprocessing import Process
# Замеряет время выполнения
def measure_time(func):
def measure():
begin = time.time()
n = func()
print("Задание", n)
print("Time:", time.time() - begin, end="\n\n")
return measure
# Для IO bound
def t():
res = requests.get('https://google.com')
# Для CPU bound
def countdown():
i = 0
while i < 5_000_000:
i += 1
# Задание 1
@measure_time
def one():
res = list()
for i in range(10):
res.append(requests.get('https://google.com'))
return 1
# Задание 2
@measure_time
def two():
threads = list()
for i in range(10):
threads.append(Thread(target=t))
for thrd in threads:
thrd.start()
for thrd in threads:
thrd.join()
return 2
# Задание 3
@measure_time
def three():
processes = list()
for i in range(10):
processes.append(Process(target=t))
for prc in processes:
prc.start()
for prc in processes:
prc.join()
return 3
# Задание 4
@measure_time
def four():
for i in range(10):
countdown()
return 4
# Задание 5
@measure_time
def five():
threads = list()
for i in range(10):
threads.append(Thread(target=countdown))
for thrd in threads:
thrd.start()
for thrd in threads:
thrd.join()
return 5
# Задание 6
@measure_time
def six():
processes = list()
for i in range(10):
processes.append(Process(target=countdown))
for prc in processes:
prc.start()
for prc in processes:
prc.join()
return 6
if __name__ == "__main__":
# IO bound
one()
two()
three()
# CPU bound
four()
five()
six()
Ответы (2 шт):
Вы все сделали почти правильно, на мой взгляд.Вот результаты на моей машине. Выигрыш при потоках более 40%.
Задание 4 Time: 2.719468355178833
Задание 5 Time: 2.493485450744629
Задание 6 Time: 1.762448787689209
Но можно улучшить - зачем вам join()? Выигрыш более двух раз. (Обновил: эта идея с устранением join() неверна. См.комментарии)
Задание 4 Time: 2.6073203086853027
Задание 5 Time: 2.0994372367858887
Задание 6 Time: 0.7468335628509521
И обратите внимание, чтобы другие программы не мешали в этот момент. Запустил сейчас, когда вентилятор зашумел (какая-то программа "проснулась") - разницы между 5 и 6 почти не стало.
В общем, я провёл некоторые эксперименты, почитал всякое... Короче, всё упирается в GIL как обычно. Почему мультипроцессинг тоже в него упирается, хотя вроде бы не должен - я пока не разобрался. Но в общем если отпускать GIL, например, с помощью time.sleep, то получаются уже другие результаты - параллельная версия работает до 10 раз быстрее, чем последовательная... Но выгоды всё-равно никакой нет, потому что time.sleep съедает всю выгоду. В итоге параллельные версии работают столько же, сколько раньше работали все все версии, а последовательная до 10 раз дольше, чем раньше.
В общем, в случае чисто питоновского кода из-за GIL нет почти никакого смысла в мультипоточности и мультипроцессности, увы.
Содержимое цикла для экспериментов:
# Для CPU bound
def countdown(j):
#print(j, 'start', flush=True)
i = 0
n = 5_000_000
m = 10_000
k = n // m
for _ in range(m):
for __ in range(k):
i += 1
time.sleep(0.00001)
#print(j, 'end', flush=True)
return s
Можно побаловаться цифрами, делая больше внутренний цикл либо внешний, но ничего путного в итоге у меня не получилось на таких вычислениях. Может просто потому, что в Google Colab, кажется, всего два ядра CPU. Может быть если ядер будет больше, то какой-то выигрыш всё же будет.