Введение в gil и новый gil
DESCRIPTION
Автор: Андрей Нехайчик (Wargaming.net | COOO «Гейм Стрим») — Треды, отличия от потоков. — Как использовать треды. — Тестирование производительности (и облом). — Представление GIL, как он работает. — Освобождение по I/O, 100 тиков. — Зачем нужен GIL. — Проблемы переключения потоков (медленный захват). — Проблема 100 тиков. — Проблема отсутствия приоритетов и их типов. — Новый GIL, 5 миллисекунд, drop_request. — Когда drop_request не работает. — Соревнование CPU и I/O тредов. — Как борются с GIL: тезисы о numpy, Jython, multiprocessing.TRANSCRIPT
Введение в потокиGIL и новый GIL
Андрей НехайчикWargaming.net
Из чего сделана эта презентация
3 доклада David Beazley:● Inside the Python GIL, июнь 2009● Inside the New GIL, январь 2010● Understanding the Python GIL,
февраль 2010
- Специфический внутряк для разработчиков ядра питона
+ Свежие тесты
http://www.dabeaz.com/GIL/
Немного о потоках
● для распараллеливания однотипных задач
● используется pthread
Отличия от процессов● поток – наименьшая единица обработки● поток – составной элемент процесса● потоки работают в едином адресном
пространстве● потоки “дешевле” и, обычно, ОС проще
ими манипулировать
Как работают потоки в идеале1 ядро
Как работают потоки в идеале3 ядра
Пример
def count(n): while n > 0: n -= 1
t1 = Thread(target=count, args=(100000000, ))t2 = Thread(target=count, args=(100000000, ))
t1.start()t2.start()t1.join()t2.join()
Пример
def count(n): while n > 0: n -= 1
t1 = Thread(target=count, args=(100000000, ))t2 = Thread(target=count, args=(100000000, ))
t1.start()t2.start()t1.join()t2.join()
прекратите тиражировать
бесполезный пример
Пример: поиск простых чиселdef is_odd_prime(num): for multiplier in range(3, num, 2): if num % multiplier == 0: return False return True
def store_odd_primes(storage, max_number, start_number=3): for num in range(start_number, max_number + 1, 2): if is_odd_prime(num): storage.append(num)
numbers = [2]store_odd_primes(numbers, 20000)
store_odd_primes(numbers, 40000, start_number=20001)
print len(numbers)
Пример: поиск простых чиселfrom threading import Thread
...
numbers = [2]
thread1=Thread(target=store_odd_primes, args=(numbers, 20000, ))
thread1.start()
thread2=Thread(target=store_odd_primes, args=(numbers, 40000, ), kwargs={'start_number': 20001})
thread2.start()
thread1.join()
thread2.join()
print len(numbers)
Результаты
Поиск простых чисел до 40000 неоптимальным алгоритмом
Окружение Последовательноевыполнение
2 потока(1 ядро)
2 потока(2 ядра)
python 2.7.5(2GHz, Gentoo)
5.40 5.41 5.92 (+10%)
python 3.2.5 8.80 9.02 12.11 (+40%)
python 3.3.3 8.90 8.90 12.20 (+40%)
python 2.7.3(1GHz, iOS 7, iPad 3)
23.90 - 25.30 (+5%)
python 2.7.2(2.5GHz, OS X 10.8)
3.56 - 3.90 (+10%)
Потоки в Python
● Существует GIL и гарантирует последовательное выполнение байткода
● Каждые 100 тиков: освобождение и захват GIL
● 1 тик – одна или более инструкций байткода
● IO освобождает GIL● Си-модули могут освобождать GIL● Освобождение и захват GIL –
дополнительные накладные расходы
Потоки в Python
Обработка сигналов
Профилирование однопоточной программы
python -m cProfile prime_seq.py # 10000
ncalls tottime filename:lineno(function) 1 0.001 prime_seq.py:1(<module>)
1 0.015 prime_seq.py:3
(get_prime_list)
9999 0.797 prime_seq.py:5(is_prime)1229 0.001 {method 'append' of 'list' objects}
1 0.000 {method 'disable' of
'_lsprof.Profiler' objects}
Профилирование многопоточной программыncalls tottime percall cumtime percall filename:lineno(function)
………
2 0.000 0.000 0.886 0.443 threading.py:909(join)
………
25 0.886 0.035 0.886 0.035 {method 'acquire' of 'thread.lock'}
97 0.000 0.000 0.000 0.000 {method 'append' of 'list'}
1 0.000 0.000 0.000 0.000 {method 'disable' of
'_lsprof.Profiler'}
2 0.000 0.000 0.000 0.000 {method 'extend' of 'list'}
2 0.000 0.000 0.000 0.000 {method 'get' of 'dict'}
1 0.000 0.000 0.000 0.000 {method 'insert' of 'list'}
2 0.000 0.000 0.000 0.000 {method 'items' of 'dict'}
1 0.000 0.000 0.000 0.000 {method 'lower' of 'str'}
12 0.000 0.000 0.000 0.000 {method 'release' of 'thread.lock'}
1 0.000 0.000 0.000 0.000 {method 'rfind' of 'str'}
2 0.000 0.000 0.000 0.000 {method 'setter' of 'property'}
6 0.000 0.000 0.000 0.000 {method 'write' of 'file'}
Вернёмся к начальному примеру
def count(n): while n > 0: n -= 1
Последовательное 2 потока(2 ядра)
Разница
python 2.7.5,Gentoo, 2Ghz
20.0 27.7 x1.4
python 3.2.5 19.7 34.5 x1.8
David BeazleyOS X, 2GHz
24.6 45.5 x1.8
python 2.7.2OS X 10.8, 2.5 Ghz i5
15.2 23.7 x1.6
Почему такие большие накладные расходы (1 ядро)?
Почему такие большие накладные расходы (2 ядра)?
Визуализация попыток захвата
Промежуточные итоги
● Код параллельно не выполняется● Но IO (всегда) и CPython расширения
(некоторые) освобождают GIL● Нет планировщика потоков● Сигналы обрабатываются в главном
потоке● Потоки с интенсивным использованием
CPU передерживают GIL● Провальные попытки захвата GIL
Зачем нужен GIL?
● Защита операций работы с памятью в ядре
● Упрощение кода● Скорость выше, если код проще
GIL в python 3.2
● Первое серьёзное изменение со времён 1992 года
● Вместо счётчика тиков - gil_drop_request● Также добавлен таймаут в 5мс
Как это работает?
Проблемы с таймаутом
Проблемы из-за отсутствия планировщика
Замедление IO
Какие планы по GIL
Его заменят, если новый подход будет:● простым● увеличит скорость для многопоточных
программ● не изменит скорость для однопоточных● будет совместим с текущим API ядра● оставит такое же поведение GC
Как обойти GIL
● Для IO этого делать не надо● Использовать специализированные
библиотеки:https://wiki.python.org/moin/ParallelProcessing (например scipy)
● Использовать другие интерпретаторы● Использовать multiprocessing
Использовать другие интерпретаторы
Последовательное 2 потока(2 ядра)
4 потока(4 ядра)
jython 2.5.3(Gentoo, i3 3Ghz)
10.70 8.65 (x1.24) 6.85 (x1.56)
pypy 2.0.2(Gentoo, i3 3Ghz)
2.75 2.85 2.90
Multiprocessingfrom multiprocessing import Process, Pipe
…
def store_odd_primes(pipe_conn, max_number, start_number=3):
storage = []
for num in range(start_number, max_number + 1, 2):
if is_odd_prime(num):
storage.append(num)
pipe_conn.send(storage)
numbers = [2]
parent_conn, child_conn = Pipe()
proc1 = Process(target=store_odd_primes, args=(child_conn, 20000, ))
proc2 = Process(target=store_odd_primes, args=(child_conn, 40000, ), kwargs={'start_number': 20001})
proc1.start();proc2.start();proc1.join();proc2.join()
numbers += parent_conn.recv()
numbers += parent_conn.recv()
print len(numbers)
Multiprocessing
Последовательное 2 процесса(2 ядра)
4 потока(4 ядра)
python 2.6.8(Gentoo, i3 3Ghz)
3.80 2.80 (x1.35) 2.08 (x1.83)
python 3.2.5(Gentoo, i3 3Ghz)
5.05 3.68 (x1.37) 3.10 (x1.63)
pypy 2.0.2(Gentoo, i3 3Ghz)
0.50 0.42 (x1.20) 0.33 (x1.55)
Выводы
● Параллельной многопоточности в Python по умолчанию нет
● Она частично существует для специализированных расширений и операций IO
● GIL почти никогда не мешает● А когда мешает мы знаем как с этим
бороться
Спасибо.