Конкурентность и Параллелизм

в Python

Игорь Кальницкий

Конкурентность

Время

Задача А

Задача Б

Задача В

t + 0

исполняется

t + 1

исполняется

t + 2

исполняется

t + 3

исполняется

t + 4

исполняется

t + 5

исполняется

Параллелизм

2 вычислителя

Время

Задача А

Задача Б

Задача В

t + 0

исполняется

исполняется

t + 1

исполняется

исполняется

t + 2

исполняется

исполняется

t + 3

исполняется

исполняется

t + 4

исполнятеся

исполняется

t + 5

исполняется

Потоки

from threading import Thread

def sqr(x):
    return x * x

Thread(target=sqr, args=(42, )).start()
  • Нельзя получить результат работы sqr(42).
  • Удобно использовать для создания фоновых задач.
from concurrent.futures import ThreadPoolExecutor

def sqr(x):
    return x * x

with ThreadPoolExecutor(max_workers=3) as executor:
    result = list(executor.map(sqr, [1, 2, 3, 4, 5]))
  • Нельзя организовать фоновую работу.
  • Удобно использовать для распределения задач на выполнение и получения результата.

А как же GIL?

Global Interpreter Lock (GIL)

from concurrent.futures import ThreadPoolExecutor

def sqr(x):
    return x * x

with ThreadPoolExecutor(max_workers=3) as executor:
    result = list(executor.map(sqr, [1, 2, 3, 4, 5]))
  • Только один поток будет вычислять квадрат принятого числа в единицу времени.
  • Общая производительность будет хуже, чем при последовательных вызовах из-за экстра работы по созданию и коммуникации потоков.

Процессы

import os
os.fork()

Multiprocessing the Hard Way :)

from multiprocessing import Process

def sqr(x):
    return x * x

Process(target=sqr, args=(42, )).start()
  • Нельзя получить результат работы sqr(42).
  • Удобно использовать для создания фоновых задач, неограниченных GIL.
from concurrent.futures import ProcessPoolExecutor

def sqr(x):
    return x * x

with ProcessPoolExecutor(max_workers=3) as executor:
    result = list(executor.map(sqr, [1, 2, 3, 4, 5]))
  • В отличии от версии с потоками, вычисление может выполняться параллельно при доступной аппаратной составляющей.
  • Так как создание процесса – дорогостоящая операция, отдавать не трудоемкие задачи на выполнение имеет мало смысла.

Процессы или Потоки?

Характеристика

Потоки

Процессы

Ограничение GIL

да

нет

Время создания

меньше

больше

Время коммуникации

меньше

больше

Потребление памяти

меньше

больше

Зеленые потоки

import eventlet; eventlet.monkey_patch()

from threading import Thread

def sqr(x):
    return x * x

Thread(target=sqr, args=(42, )).start()
  • Event Loop использует технологии ядра для массовой обработки сокетов. Например, под Linux используется epoll для ожидание события на одном из сокетов.
  • Когда событие пришло, вызывается зеленый поток, который связан с этим сокетом.

Зеленые потоки или Потоки?

Зеленые потоки

Потоки

Подходят только для I/O приложений.

Подходят для любого рода приложений.

Экономят системные ресурсы.

Хотя и не будут исполняться параллельно.

asyncio

import asyncio

async def sqr(x):
    return x * x

loop = asyncio.get_event_loop()
result = loop.run_until_complete(sqr(42))

Зеленые потоки или asyncio?

Зеленые потоки

asyncio

Совместимы с обычным многопоточным кодом.

Требует использование специального синтаксиса, несовместимого с классическим кодом.

Благодаря костылям, работают со многими библиотеками и драйверами к БД.

Требует специальных версий библиотек и драйверов к БД.

Существует мнение, что асинхронное программирование работает быстрее для I/O приложений за счет отсутствия необходимости переходить в kernel space для переключения между потоками.

Эхо-сервер на Потоках

from socket import *
from threading import Thread

def echo_server(conn):
    while True:
        data = conn.recv(100)
        if not data: break
        conn.send(data)

s = socket(AF_INET, SOCK_STREAM)
s.bind(('127.0.0.1', 5000))
s.listen(1000)

while True:
    conn, addr = s.accept()
    Thread(target=echo_server, args=(conn, )).start()

Эхо-сервер на Корутинах

import asyncio

async def echo_server(reader, writer):
    while True:
        data = await reader.read(100)
        if not data: break
        writer.write(data)

loop = asyncio.get_event_loop()
coro = asyncio.start_server(echo_server, '127.0.0.1', 5000, loop=loop)
server = loop.run_until_complete(coro)

loop.run_forever()

Результат

Эхо-Сервер

Кол-во клиентов

Пропускная способность

на Потоках

100

~ 130000 запросов/с

на Корутинах

100

~ 20000 запросов/с

o_O

Исследование

Эхо-Сервер

CPU

linux perf

на Потоках

~ 195%

  • ~ 75% времени - linux kernel
  • ~ 17% времени - python3.5
  • ~ 7% времени - libpthread

на Корутинах

~ 99%

  • ~ 28% времени - linux kernel
  • ~ 67% времени - python3.5
  • ~ 5% времени - libc

Python медленный :'(

Зато потоки умеют параллельно исполняться :)

Хм.. А что в реальном мире?

Результат

Эхо-Сервер

Кол-во клиентов

Пропускная способность на локальном хосте

Пропускная способность в сети

на Потоках

100

~ 130000 запросов/с

~ 2800 запросов/с

на Корутинах

100

~ 20000 запросов/с

~ 2800 запросов/с

Что надо усвоить?

Материалы по теме

ihor@kalnytskyi.com