Pull to refresh

Comments 76

UFO just landed and posted this here
UFO just landed and posted this here
:) В следующий раз обязательно воспользуюсь советом. Сейчас уже лень. Это не какой-то фундаментальный топик и скрипт, а так, скорее быстрая вечерняя поделка.
хотелось порыться именно в сокетах и проблемах, с ними связанных. Там было несколько неочевидных вещей вроде всплывающего исключения с кодом ошибки errno.EINPROGRESS.
Черно-белая подсветка кода выглядит классно. Еще бы оттенков серого добавить и был бы шедевр.
Отформатируйте, пожалуйста, код моноширинным шрифтом
UFO just landed and posted this here
На самом деле нужно просто-напросто и всего-навсего примерно набросать области применения различных методов работы с сетью и решить для себя, в каком случае какой применять. Я по большому счету знаю три:
1) Классический многопроцессорный, я форком на каждое соединение.
+ Самый надежный и в принципе простой в написании
— Плохо подходит на Виндовс
— Нужно заморачиваться с IPC
2) Мультитредовый
+ Более производительный, чем 1)
— Менее надежный, чем 1)
+ Лучше работает на Виндовс
3) Асинхронный
+ Самый производительный
— надежность хуже чем 1)
+ работает и на Вин и на Лин (Unix)
Дисклаймер: Все вышесказанное — мое ИМХО, ни за что не отвечаю.
третий вариант хоть и самый производительный, но на нём одном к сожалению не реализовать ничего сложного, видимо поэтому он у вас и получается самым производительным :)
а нам приходиться дополнительно городить свои планировщики и выбирать использование безстэковых/стэковых coroutine для простоты программирования. либо вырисовывая различные конечные автоматы, восстановление стэка в которых тоже бывает не такой уж скоростной операцией с кучей кэш миссов.
> но на нём одном к сожалению не реализовать ничего сложного,

что вы имеете ввиду?
1) Классический многопроцессорный
стэйт хранится в стэке

2) Мультитредовый
стэйт хранится в стэке

3) Асинхронный
>а нам приходиться дополнительно городить…
Вообще, необходимо расширить описание этих трех пунктов. Поправьте, если я ошибаюсь

Примеры прилжожений:
1) Apache по классической схеме (не мультитредовый) — Известен своей надежностью
2) IIS, Apache версии 2.0, мультитредовый. Пока не рекомендуется там, где нужна высокая надежность
3) Nginx — отличается самой высокой производительностью

Рекомендации к выбору решения:
1) Если нужно быстро сделать приложение, которое будет работать достаточно надежно, ДАЖЕ при присутствии в коде немалого количества ошибок. Трудозатраты низкие.
2) Если нужно сделать преносимое Unix/Win32 решение. Трудозатраты на разработку низкие, на отладку и доведение до уровня надежности 1) высокие
3) Если нужна максимальная производительность. Самые высокие трудозатраты.
1 и 2) легко расширяются с помощью модулей, можно делать блокирующие вызовы, какую-нибудь тяжёлую работу, зависимость в модулях от проприетарщины, которая делает блокирующие вызовы.
3) нужно подстраиваться под стэйт машину, которая была реализована в этом приложении. Не знаю как сейчас, но раньше с nginx'ом небыло возможности даже сделать простое синхронное логирование без блокирующего вызова в своём модуле. Но это не проблема данного подхода, просто так уж сделали.
Реальная многопоточность несет оверхед, больше переключений контекста, например (кстати, почему разработчики процессоров не сделают эту операцию быстрой, рах ее постоянно упоминают как причинц снижения производительности?). Плюс нужна синхронизация для доступа к общим переменным, а это опять-так и ломает всю производительность.

И вообще, кто вам сказал, что многопоточность — хорошо? Как вообще может быть хорошей программа, в которой несколько потоков работают в общей области памяти и любой может вызвать трудно обнаружимую ошибку?
>больше переключений контекста
А это уж как реализовано в ОС.
>Как вообще может быть хорошей программа, в которой несколько потоков работают в общей области памяти
Если сделать доступ атомарным, то программа будет чрезвычайно хорошей.

Вобщем, ничего плохого в потоках нет, надо просто уметь их готовить.
>Если сделать доступ атомарным, то программа будет чрезвычайно хорошей.
Хочется тоже попасть в эту сказку :)

>надо просто уметь их готовить.
И вы конечно же работали с libnuma
> Если сделать доступ атомарным

Ага, только этот атомарный доступ останавливает все ядра.
> Как вообще может быть хорошей программа,
> в которой несколько потоков работают в общей области памяти,
> и любой может вызвать трудно обнаружимую ошибку?
Есть хороший способ избежать этой проблемы — отказаться от общих переменных.
В статье не раскрыт вопрос почему асинхронность (и данное решение в частности) лучше многопоточности.
Ест меньше ресурсов. Отлично было раскрыто в одной из статей Ивана Сагалаева, про то как он писал некий кусок кода для скачивания медиа-файликов, и в итоге пришел к asyncore. Найдите, прочтите, просветление настигнет внезапно.
А, почему не раскрыто. Автор видимо не подумал, что это надо раскрывать в сотый раз — на хабре есть уже множество статей, как однопоточное приложение может обойти многопоточное.
А смысл раскрывать очевидный вопрос?

Асинхронность ортогональна многопоточности, как их сравнивать вообще?
Неблокирующую работу с сетью можно использовать вместе с многопоточностью, где кол-во потоков сделать равным кол-ву логических процессоров и все будут довольны :)
А может все же проще плюнуть на сервер пачку запросов аяксом?

Эффект тот же
:-)
«Кроме того, в Питоне все равно не существует настоящих потоков уровня ядра, а здравствует и по сей день треклятый GIL. Соответственно, никаких преимуществ в производительности на многоядерных процессорах получить нельзя.»

Можно пояснить? Я сейчас написал сприптик на питоне — создаю 4 потока в каждом деалаю 1000*1000, вижу в топ 4 потока отъдающие по ~100% cpu и еще один ~0,01 (я полагаю GIL). Те вроде как получается занять все 4 ядра. python25 если это важно.
угу. А попробуйте сначала сделать 100 тыс раз (1000*1000) тупо в одном потоке, а потом по 25 тыс. раз в 4 четырех параллельных и сравните время )
4 потока по 2500000 итрераций умножения

real 0m3.319s
user 0m3.299s
sys 0m0.018s

без потоков 10000000 итераций

real 0m4.704s
user 0m4.758s
sys 0m0.310s

Выйрыша небольшой, но 1) он есть 2) потоки native 3) на других задачах разница может быть выше. Натив threads в питон в версии 2.4 уже точно были. Журнал Dr. Dobb's в свое время писал на эту тему.

По ссылке все написано верно, но там пример 2 потока I/O и CPU интенсивыный, CPU интенсивный и дергает постоянно GIL, потому что только в нем и интрпретируется python код, в то время как I/O вызовы умеют освобождать GIL. Те в случае двух I/0 процессов — картина была бы совсем иной.
ммм. А где там написано вообще что-то про потоки IO? Там мне кажется как раз ваш случай — 2 CPU потока без IO которые переодически переключаются между собой по тикеру.
In this graph, you're seeing how difficult it is for the I/O bound to get the GIL in order to perform its small amount of processing.
Там совершенно не мой случай, мой случай 4 CPU-bound thread, а по ссылке 1CPU-bound а втрой merely echoes UDP packets.
Так это про последний рисунок. Ваш случай это ведь второй. А по нему видно что несмотря на то, что 2 треда исполняются на 2 процессорах на самом деле полезную работу в каждый момент времени выполняет только один.
Если вы делаете просто:

for i in range(2500000):
1000 * 1000

то интерпретатор оптимизирует это выражение и умножения просто не будет при каждой итерации.

добавьте: * i

тогда увидите реальную картину при которой вариант с тредами медленнее.
Я как раз это понимаю, результаты выше для такого кода
hcount = 2500000
while (hcount>1):
j=1000*1000
hcount=hcount-1
4 потока с таким циклом
Вот как раз тут у вас нет умножения:-)

Лучше вообще взять классический пример с счетчиком:

COUNT = 100000000
CPU_COUNT = 4

def count(n):
while n:
n -= 1

def sequence():
for i in range(CPU_COUNT):
count(COUNT)

def parallel():
pool = []
for i in range(CPU_COUNT):
t = threading.Thread(target=count, args=(COUNT,))
t.start()
pool += [t]

[t.join() for t in pool]
Скорее да — но вот ведь прикол — пустой цикл в 4 потока отработал чуть быстрее чем в 1.
Трудно анализировать такое поведения, не видя код.
#!/usr/bin/python3.1
import threading

class mythread(threading.Thread): # subclass Thread object
def __init__(self, myId, hc ):
self.myId = myId
self.hcount = hc
threading.Thread.__init__(self)

def run(self): # run provides thread logic
hcount = 2500000
while (hcount>1):
j=1000*1000
hcount=hcount-1

MAXTHR=4


threads = []
for t in range(MAXTHR):
thread = mythread(t, 0) # make/start 10 threads
thread.start( ) # start run method in a thread
threads.append(thread)

for thread in threads:
thread.join( ) # wait for thread exits

Вот такой вот запускал
А говорили что питон 2.5:-)

Слишком мало повторений. На таких объемах накладные расходы на переключение GIL не так отчетливо проявляются. А полученный временной результат может зависеть от общей нагруженности системы в какой-то конкретный момент запуска теста.

Суть в том, что треды как минимум не быстрее в CPU-bound задачах.
Кстати, судя по всему, в питоне 3.2 сделают быстрый GIL. Он останется, но не будет так тупить на счетных задачах. Раньше переключение шло каждые N инструкций (что, понятное дело, убивало все к черту на счетных циклах), теперь будет идти каждые N миллисекунд.

www.dabeaz.com/python/NewGIL.pdf
> что, понятное дело, убивало все к черту на счетных циклах

Убивало не это, а то что тред, отпустив GIL, мог сам же его снова захватить. Так было из-за того, что он не знает есть ли кто-то другой ждущий выполнения, а OS в свою очередь не успевала в этот интервал переключить контекст.

В новом варианте он отпускает его сам только если флаг выставлен и потом засыпает до того момента как GIL захватит другой тред.

То что всю эту системы перевели на временные интервалы в место числа инструкций, можно сказать, побочный эффект нового подхода:-)
Спецом по GIL не являюсь, код питона не читал, сужу по разным записям в интернете. Берем первоисточник:

mail.python.org/pipermail/python-dev/2009-October/093321.html

Автор патча пишет 3 вещи, которые его в GIL не устраивали, снижали производительность и побудили, в конце концов, написать патч. Первые 2 причины — это то, что я написал, 3я — то, что ты.
UFO just landed and posted this here
что за жизнь без multiprocessing? (на худой конец хоть multi-threading)

в CPython dev team как клала на это болт так и кладёт.

Хорошо хоть мульти-ядерники стали чаще появляться на столах. Команда CPython стала немножко шевелиться в эту сторону.

вся надежда на unladen swallow, но и им не легко.
кст, проблема в том, что в CPython нет и настоящих процессов…
Т.е.? Не писал, врать не буду, но вроде ведь можно форк вызвать.
можно и просто multiprocessing module пользовать.

но GIL и ref counting убивают любое не слишком тривиальное приложение даже сидящее на мультипроцессинге.
По идее у каждого процесса будет свой GIL и refcounting.
верно. а проблема таки вылазит:

если есть обычная жирная структура в памяти, к которой процессы лазят даже лишь на чтение, то каждый процесс из-за этих ref counters получает себе copy-on-write копию этого дурацкого реф-каунтера со всей его жирной окрестностью в RAM.

вот и получается: там где вы в С можете делать 100 процессов без проблем, там же в CPython вы имеете 100 кратное увеличение ерундового 500Мб куска в памяти.
s/увеличение/размножение
man execve
execve() does not return on success, and the text, data, bss, and stack
of the calling process are overwritten by that of the program loaded.

Никакого увеличения куска памяти. Используйте инструменты с умом.
мы вроде о CPython тут?
в контретно том примере используется только fork, из-за которого получаем всё наследие родителя на которое вы жалуетесь.
> Используйте инструменты с умом.
> [...] snip [...]

ни на что я не жалуюсь.
Тут возможно недопонимание COW? Нам нужно наследство родителя, мы хотим его, мы хотим им пользоваться. но без лишних затрат. Это дает нам fork() и любим мы его именно за это.
В классическом случае мы получаем наследие родителя полностью, но если мы его только читаем, то нм получение наследия никаких затрат мы не несем.
Но в примере vak мы казалось бы получили в дочерние процессы данные, которые только читаем, по идее у каждого дочернего процесс должна быть большая виртуальная память и маленькая резидентная. На Си или Спп так бы и было, однако на Pythone получается не так из-за указанных vak особенностей реализации сборщика мусора.
Это ни плохо ни хорошо, это реальность данная нам в ощущениях.
А вот если от этого эффекта удасться уйти без больших затрат, это вообще будет суперкул!
100% ditto.

кст, есть неплохая заплатка от итальянца, который хотя бы сдвигает эти реф каунтеры в одно место. пару недель назад он собирался «дополировать» его (я пока не смотрел чем дело кончилось).
Так нужно чтобы задача выполнялась быстрее в CPython'е или наследство родителя? На Си вообще большинство вещей делалось бы иначе. В питоне свои особенности, зачем пытаться делать так же как в си…
Можно попробовать через пайп прокидывать всё нужное наследство, так получим параллелизм(если конечно кол-во логических процессоров больше одного)
ещё можно приклеить clone или posix_spawn к питону и делать clone/exec без оверхеда на копирование таблиц для работы cow.
Пока процессы не пишут в этот кусок, они пользуются физически одной и той же памятью, так что увеличение использованной памяти в многопроцессорной системе не происходит. (Называется этот механизм как раз copy-on-write, суть в том, что страница памяти, если не обшибаюсь, как правило 4k, физически выделяется дочернему процессу тогда и только в тот момент, когда он попробует в нее записать)
Да, это все, что я написал, это Linux и fork(), в Виндовсе все не так гладко.
когда-то было не так гладко не только в виндовсе :) что изобретали всякие vfork (BSD)
именно.
и, казалось бы, всё шоколадно и CPython должен бы профитировать от этого механизма тоже?

беда в том, как я попытался уже написать выше, что ref counters изменяются даже при операциях чтения. а это провоцирует copy-on-write копирование.

последний гвоздь — ref counters не живут в одном лепрозории месте в памяти,
а лежат в заголовке структуры, которую они обслуживают.
результат? — несложно догадаться.

нужен 10 строчный пример кода? дайте знать.

P.S. и, как обычно, те, кто не в теме, умеют, однако, жать на минус вполне уверенно. ну, ничего, меньше возможностей писать, больше времени на что-то полезное :)
Несколько отвлеченное соображение. Я в Питоне недавно, где-то 1-2+ года. Для меня вообще стало открытием, что универсальный скриптовый язык используется для написания неплохо работающих, в том числе и по производительности серверов, в том числе и многопоточных. Это приятная неожиданность. Надеюсь, что и проблему с GIL решат. Вот тогда и наступит коммунизм!
> и, казалось бы, всё шоколадно и CPython должен бы профитировать от этого механизма тоже?

Но ведь, ИМХО, должен. Ведь для разных процессов и GIL-ы разные и проэтому процессы могут исполняться на разных ядрах без простоев. Единственное, что экономия памяти и времени будет меньше из-за выделения страниц, в которые подпадают ref-counters.

> последний гвоздь — ref counters не живут в одном лепрозории месте в памяти,
а лежат в заголовке структуры, которую они обслуживают.
результат? — несложно догадаться.

> нужен 10 строчный пример кода? дайте знать.

Давайте. Не для спора, а просто интересно для углубления в проблему.
import time
from multiprocessing import Pool
def f(_):
    time.sleep(5)
    res = sum(parent_x) # "read-only"
    return res

if __name__ == '__main__':
    parent_x = [1./i for i in xrange(1,10000000)]# read-only data 
    p = Pool(7)
    res= list(p.map(f, xrange(10)))
    print res


заходите в ps/top или в любой другой ваш любимой монитор памяти и смотрите как процессы взрывают mem usage.
Спасибо, проверил, все действительно так, будем учитывать эту фичу. Поигрался немного с текстом программы, прогнал три варианта:
1) Ваш вариант
2) То же, на форке (переписал для себя, для понятности)
3) На форке, с вычислением суммы внутри функции
res = sum(1./i for i in xrange(1,10000000))
Результаты получились ожидаемые, большой точности не добивался, но все-таки:
Вариант Время ПамВирт ПамРез ПамШар
1)          14с     200        198      836
2)          11с     198        193      432
3)          17с     5456      1780     460

Выводы:
1) Описанный Вами эффект оказывает сильное вредное влияние при больших объемах часто используемых данных
2) Разница во времени между 1) и 2) — скорее всего погрешности эксперимента
3) При вычислениях по функциональному типу даже вычисляя одно и то же отдельно в каждом процессе проигрыш по времени неожиданно мал. Объяснить — не хватает моих знаний внутренностей Python
4) Вычисления по функциональному типу дают большую экономию памяти.
В принципе, все это давно известно, но интересно было потрогать своими руками.
Еще раз спасибо.
gern geschehen!

надеюсь, команда unladen swallow не сдастся и добьёт эту важную тему до конца.

З.Ы. хотя мне самому осталось не очень ясно зачем я тут минусы собираю. пополз-ка я обратно, в читателей ;)
Пардон, заметил недочет. Таблицу следует читать так:
Вариант Время ПамВирт ПамРез ПамШар
1)          14с     200M        198M      836
2)          11с     198M        193M      432
3)          17с     5456        1780     460
хм, 1780 без «М»?.. если ошибки нет, то такое впечатление, что форковые версии процессов практически не пересекались во времени исполнения, но это маловероятно…

З.Ы. и, кст, спасибо за :)
Вот код:
#! /usr/bin/python
# http://habrahabr.ru/blogs/python/81716/

import time,os,sys

def f():
    time.sleep(5)
    res = sum(1./i for i in xrange(1,10000000))
    return res

if __name__ == '__main__':

    t0 = time.time()
    for i in range(7):
        pid = os.fork()
        if(pid == 0):
            res = f()
            print res
            sys.exit(0)
        else:
            next
    os.wait()
    print "time =", time.time() - t0

коварное исчезновение квадратных скобок я-то и не заметил сначала :)
о, если генераторы используются, то тогда конечно большого блока памяти и не появляется — ни у родителя, ни у детишек.

ОК, всё сошлось.
напомнило мои извращения с php, только функции слегка другие, а суть таже, см php.net/socket_select
Sign up to leave a comment.

Articles