Как стать автором
Обновить

Комментарии 20

Спешу возмутиться спорному утверждению автора, что поток — эмуляция параллелизма.
Никакая он не эмуляция, и при наличии возможности (несколько ядер и/или процессоров) планировщик задач ОС запускает потоки действительно параллельно. В данном случае у нас есть не один, а несколько (по числу ядер) потоков команд, идущих параллельно, в них-то планировщик всё и раскладывает.

если у Вас на машине потоков больше чем ядер (а в современности это практически всегда), расскажите, как несколько потоков будут делить одно ядро без эмуляции параллелизма

Давайте посмотрим внимательнее. Предположим, имеем на процессоре два ядра. Соответственно, планировщик ОС имеет две очереди на исполнение. И каждый тайм-слот на наших двух ядрах параллельно (совершенно честно!) выполняются две задачи.
Предположим, что наше приложение создаёт два потока. Ничто не мешает планировщику запланировать выполнение этих двух потоков на двух ядрах совершенно честно параллельно.
А само приложение никогда не знает, исполняется ли оно честно параллельно, или параллельность «эмулируется». И не имеет права знать: сегодня его на одном ядре запустили, и потенциальные гонки, которые проглядел программист, просто не случились. А завтра запустили на 32 ядрах, все его потоки заработали действительно параллельно, и всё сломалось.

Я вас понял. Но в текущей ситуации все потоки эмулируют параллелизм относительно каких-то других. Нынче почти нет вероятности получения непрерывного отрезка времени, большего чем квант. Активных потоков на одном ядре всегда больше одного. Даже если поток_1 и поток_2 находятся на разных ядрах, вероятность их чисто параллельного исполнения крайне мала: помимо них на ядрах есть другие и попасть в один квант — скорее большая удача (особенно если учесть постоянное динамическое повышение приоритетов и динамическое изменение длинны квантов на десктопе и как следствие — изменение этого порядка). От слов покаотказываться не буду: они покрывают 99.99% случаев.

Зато создание потоков — отличный способ получить заметный прирост производительности на современном процессоре. Конечно, "заметность" зависит от свойств самой задачи, от того, сколько в ней параллелизма, вообще, есть.

НЛО прилетело и опубликовало эту надпись здесь

И далее про HyperThreading. "Распараллеливание" идёт за счёт того, что планировщик процессора (есть там такой блок) может запускать на исполнение две команды из соседних виртуальных "ядер", если эти команды задействуют разные исполнительные блоки процессора. Поэтому в данном случае, опять же, исполнение будет действительно параллельным. Но более медленным, чем на честных ядрах, как раз из-за блокировок. Но не доступа к памяти (тут-то вся ответственность как раз-таки ложится на программиста), а доступа к исполнительным устройствам процессора (АЛУ и т.п.)

Большинство команд, применяемых при компиляции задействуют 1-2 такта. Процессор не может отработать 1,5 такта. Поэтому даже если замедление и небольшое, для большинство команд оно будет "в 2 раза". Это — раз. Второе — повествование идёт для .NET разработчиков. У нас нет понимания, на каком ядре запущен код. Поэтому нет возможности убедиться, что код работает с одними данными, но, например, не на одном физическом ядре. Этого можно добиться, создав потоки друг за другом, да. Но уверенности это знание не даёт.

Процессор давно стал суперскалярным.
Он может выполнить 2-3 команды за 1 такт.

Это не имеет отношения к замедлению отдельных команд.

Плюс не ясно, о чем спор: в тексте шла речь про наихудший сценарий

Про Hyper-Threading абзац во многом неверный. HT подразумевает одно АЛУ на 2 вируальных ядра, и в случае ошибок предсказания ветвления и прочего, что вызвало бы сброс конвейера и ожидание его заполнения, происходит переключение на второй набор регистров и исполнение инструкций с соседнего "ядра". Так что потоки на НТ ядре работают по очереди не "в худшем случае", а всегда.


Далее,


Организация планирования в … Windows является: приоритетной и вытесняющей.… А вытесняющей потому, что если возникает более приоритетный поток, он вытесняет тот, который сейчас исполнялся.

Это вообще неверно. Есть понятия вытесняющей и кооперативной многозадачности. В случае кооперативной, процессы (потоки) сами решают, когда отдать управление планировщику ОС для дальнейшей передачи другому процессу. (И если они плохо написаны или виснут, то могут не передать вовсе). А при вытесняющей многозадачности, планировщиком полностью рулит ОС, отбирая процессор у программ без их участия и согласия, "вытесняя" их. И возникновение новых потоков и их приоритеты с принципом работы такой многозадачности не связаны.

Благодарю за конструктивную критику, отправил Вам приглашение на Хабр :)


По первому вопросу — спасибо. Я не нашёл (может не там искал) хорошей статьи по HT. Тема, как говорится, не раскрыта. Дадите источник, с удовольствием дополню абзац.


По второму — по кооперативной и вытесняющей уточнил в статье: возможны оба варианта.

Очень длинно и подробно описаны приоритеты потоков, но совершенно не раскрыто зачем это всё нужно знать программисту на C#? Точнее, я ещё хоть как-то могу понять зачем внутри своей программы создавать потоки с разными приоритетами, но зачем знать их приоритет с потоками других процессов ума не приложу. Более того, мне действительно сложно представить ситуацию где практически будет полезно создавать потоки и управлять их приоритетом в современном C# приложении. Было бы очень полезно, чтобы цикл статей начинался именно с обоснования зачем это нужно. Иначе не ясно, почему бы не использовать Task.Start везде и не думать о потоках

Task.Start() относится к параллельной вселенной. Вы можете создать свой пул потоков, объеденить их в SynchronizationContext, на основе которого создать TaskScheduler и все таски, заассайненные на него начнут крутиться в вашем пуле. Но мысль понял, опишу :)

Хочется увидеть описание реального проекта, где ручное создание потока с нестандартным приоритетом решило проблему, по сравнению с простым запуском того же потока через Task.Start

Ну, например, необходимо подгрузить и подготовить данные в кэше, пока приложение занимается чем-то другим. То, что приходит в голову на основе всем известных продуктов — есть вот IDE, и она предназначена для C# разработки. И если в ней есть JS код, IDE может также его анализировать, строить AST и прочие вопросы, но фоном: когда нет других задач. На пониженном приоритете.

Из того, что мы не имеем в .NET явно — это структура безопасности с атрибутами безопасности и размер стэка.

Размер стека задаётся в параметрах конструктора потока.

Краткий (на самом деле не очень) пересказ главы из книги Windows Internal получился неплохим.
>Именно поэтому раздача ядер идёт +=2 в случае Hyper-Threading. Т.е. пропуская парные ядра.

Это уже давно немного не так.

> Технология эта — достаточно спорная

Это тоже давно не так, спорной она была в своих первых (не самых удачных) реализациях, сейчас же это отличный способ получить больше ядер и повысить общую производительность системы.

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

Это тоже, проблема с доступом к шареной между потоками/процессами памяти, возникает независимо от того, является ядро логическим (от Hyper-Threading/SMT) или физически вторым (третьим, четвертым и т.д.), к разделению ресурсов между логическими ядрами на одном физическом это практически не имеет отношения.

Современные процессоры давно умеют выполнять множество операций за 1 такт, изменять порядок исполнения инструкций, предсказывать ветвления и пытается дальше исполнять код не дожидаясь реального срабатывания перехода (угадали – хорошо часть инструкций за переходом уже выполнена, не угадали – просто отбрасываем результаты)

Но хорошо это работает только на сильно оптимизированном специфическом коде, который равномерно нагружает все доступные блоки выполнения.
«Среднестатистический» код, даже после оптимизирующих компиляторов редко когда может равномерно загрузить все доступные ресурсы.
Если прибавить к этому огромные (в тактах) задержки если данные (даже не шаренные)лежат дальше, чем в L1 кэше
то получается, что большинство блоков исполнения просто простаивают большую часть тактов.

Отсюда и родилась идея добавить еще немного элементов и показать для ОС второй процессор, которые может в это время исполнять код на свободных блоках. У процессоров SPARС емнип были вообще модели, где на одном физическом ядре было 4 или даже 8 логических.

Теоретически серьезная конкуренция между логическими процессорами за ресурсы, и просадка производительности может быть, когда оба логических процессора исполняют очень сильно оптимизированный код и все необходимые данные лежат в кэше, но на практике такое возникает достаточно редко.
Зарегистрируйтесь на Хабре, чтобы оставить комментарий