LINUX.ORG.RU

Зеленые потоки в C и C++

 , , ,


4

7

Тема, конечно, избитая, но хотелось бы собрать в одном месте всю информацию как по наличию библиотек, так и по самостоятельной реализации.

  1. Есть ли готовые реализации, кроме прямого порта горутин со всеми их недостатками?
  2. Чем оно лучше и чем хуже пула нативных потоков?
  3. Посоветуйте литературу для более глубокого изучения альтернативных концепций трединга, желательно с собственным мнением что лучше и универсальнее.
★★★★★

Последнее исправление: beastie (всего исправлений: 1)

Ответ на: комментарий от redixin

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

arturpub ★★
()
Ответ на: комментарий от arturpub

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

filequest
()

Готовые библиотеки: boost.fiber (на данный момент в буст не включена) и synca.

Есть мнение, что сопрограммы очень удобны, когда приложению нужно обслуживать множество параллельных и независимых активностей, при этом каждая активность предполагает:

  • практически линейное выполнение прикладной логики;
  • большую часть времени активность проводит в ожидании ответов от других частей приложения.

Грубо говоря, что-то вроде:

response process_request(request req)
{
  auto r1 = do_processing_stage1(req);
  if(!r1) return some_error(r1);
  auto r2 = do_processing_stage2(req);
  if(!r2) return some_error(r2);
  ...
  auto rN = do_processing_stageN(req);
  return result(rN);
}
При этом каждая стадия обработки запроса может занять какое-то длительное время.

Если такую обработку реализовать лоб-в-лоб на обычных потоках, то синхронные обращения к do_processing_stage1/.../stageN будут блокировать текущий поток. И для обслуживания сотен тысяч параллельных активностей потребуется создавать пул из большого количества рабочих потоков. Что дорого, неэффективно, а потому не есть гуд.

Если попытаться реализовать это дело на конечных автоматах, то можно получить лапшу вида:

class request_processor {
  state wait_stage1{ [this](stage1_res r) {
    if(!r) initiate_some_error(r);
    else {
      switch_to(wait_stage2);
      send_request_to_stage2(req_);
    }
  };
  state wait_stage2( [this](stage2_res r) {
    if(!r) initiate_some_error(r);
    else {
      switch_to(wait_stage3);
      send_request_to_stage3(req_);
    }
  };
  ...
  state wait_stageN( [this](stageN_res r) { ... }
  request req_;
public :
  request_processor(request req) : req_{req} {
    switch_to(wait_stage1);
    send_request_to_stage1(req_);
  }
};
Что заметно хуже.

Можно пытаться делать на механизмах future, но по сути это будет лишь более компактная и понятная запись конечного автомата:

void process_request(request req) {
  send_request_to_stage1(req_)
    .then( [=](stage1_res r) {
      if(!r) initiate_some_error(r);
      send_request_to_stage2(req);
    } )
    .then( [=](stage2_res r) {
      if(!r) initiate_some_error(r);
      send_request_to_stage3(req);
    } )
    ...
    .then( [=](stageN_res r) { ... } );
}
Это выглядит лучше, чем КА, но до callback-hell здесь остается совсем чуть-чуть.

Тогда как линейная запись прикладной логики при использовании зеленых потоков намного проще в восприятии, реализации и сопровождении.

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

eao197 ★★★★★
()
Ответ на: комментарий от eao197

Можно пытаться делать на механизмах future, но по сути это будет лишь более компактная и понятная запись конечного автомата:

то что Вы показали — это не механизм future. Вы просто конфигурируете объект, вешая на него коллбеки. Future — это объект, который принимает следующее сообщение после того, как превратился в значение. Это еще называется transparent future, но изначально термин future (а он появился в модели Акторов) означал именно это.

filequest
()
Ответ на: комментарий от eao197

обидно, что происходит профанация. Примерно то же самое что с ООП. Сначала было ООП, а потом всякий мусор, все что не поподя под соусом ООП, и люди начинают думать, что Алан Кей придумал какую то парашу. Так же и здесь. Тут суть не в терминологических спорах, а в подмене понятий.

filequest
()
Ответ на: комментарий от filequest

Значит таки да. Извольте проследовать в игнор.

eao197 ★★★★★
()
Ответ на: комментарий от beastie

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

Clear is better than clever. Reflection is never clear.

рефлексия — сердце ООП, его мощнейшая сторона, именно она позволяет строить абстракции состояния, по сути дела. Модель акторов — основа конкурентности (с которой, BTW, была слизана вторая, исправленная версия CSP, которая, в свою очередь задекларирована там как теоретическая основа), также рефлексивна.

filequest
()
Ответ на: комментарий от beastie

Смешно выглядит, когда попытались «concurrency», а вышло «parallelism».

Concurrency is not parallelism.

Почему же на всех визуализациях параллельные линии(потоки) и почему длинна общего пути увеличивается с количеством потоков и почему 2 и более линии потока растут одновременно?

Это называется параллелизм.

registrant27492
()
Ответ на: комментарий от filequest

конкурентность не исключает параллелизм

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

По поводу интерфейсов. interface{} says nothing: тут должно быть всё понятно. Это как void * с полностью утерянной мета-информацией. В частности о типе.

По поводу второго. Чем меньше интрефейс, тем более он полезный. Сам сравни io.Reader (в котором только один метод) с каким-нибудь монстром на десяток-два методов. Как часто ты можешь (ре-)использовать первое и второе?

beastie ★★★★★
()
Ответ на: комментарий от registrant27492

Дело не в параллельных линиях, а в коммуникации между ними.

beastie ★★★★★
()
Ответ на: комментарий от eao197

Это всё хорошо, только это обман. Любой блокирующий вызов - это ожидание какого-то события генерируемые не потоком исполнения. Т.е. залочить может только ОС, ибо блокировка в юзерспейсе не имеет смысла.

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

Т.е. это имеет смысл только как обёртка вокруг ассинхронных операций. Но эти операции изначально не имеют контекста - они его передают и имеют события в юзерспейсе. Весь твой пример в реальном мире будет выглядеть как шедулер над ядерной ассинхрощиной.

В твоих сопрограммах реализация примитивна - все вызовы ридов/врайтом(это конечно верх маразма, но кого это волнует) и прочего заменяются на ассинхронные аналоги и все вызовы посылают в этот самый шедуллер, который связывает низкоуровневый идентифекатор и контекст потока. После завершения вызова - оно его возобновляет.

Т.е. это хранение тех же самых контекстов, что и в случае с «множеством потоков».

Конечно же - всё это обёртка и эмуляция того, что на уровне сишного кода есть уже десятки лет. При этом с тоннами оверхеда.

Без сопрограмм это реализует быстрее и проще, если использовать изначально ассинхронные подходы, ибо хранение того же контекста( в частности стека) - не нужно. Всё это можно хранить внутри класса, ибо класс, в отличии от функции - обладает состоянием.

registrant27492
()
Ответ на: комментарий от yoghurt

Про треды речь и не идёт

Чем оно лучше и чем хуже пула нативных потоков?

ок

redixin ★★★★
()
Ответ на: комментарий от registrant27492

Царь и анонiмус в одном треде — это могло бы быть то еще шоу. Если бы не одно но: эти два уникальных персонажа пытаются излить свои потоки сознания на меня.

Шли бы вы, ребята, лесом.

eao197 ★★★★★
()
Ответ на: комментарий от arturpub

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

Ядро это не ресурс. Ок

redixin ★★★★
()
Ответ на: комментарий от beastie

Ссылка годная, но я не сильно понял каким образом она относится к моей фразе о многоядерности.

redixin ★★★★
()
Ответ на: комментарий от tailgunner

А вот и node-птушники подтянулись.

Мимо. И вообще — с такой «аргументацией» сразу в игнор.

...оппа толстый модератор детектед...

redixin ★★★★
()
Последнее исправление: redixin (всего исправлений: 1)
Ответ на: комментарий от redixin

с такой «аргументацией» сразу в игнор.

После «треды для многоядерности» - сразу на пересдачу.

Ядро это не ресурс.

Ядро - это не единственный вид ресурса. Более того, «многоядерность» - это всего лишь техническая деталь реализации SMP.

tailgunner ★★★★★
()
Последнее исправление: tailgunner (всего исправлений: 1)

охереть это просто праздник какой-то. захожу на ЛОР, а тут сразу 2 баболки беспонтовые: царь (или косящий под него) и анонiмус! весна, обострения, торт

anonymous
()

Рутины придумали не в go, не нужно их везде называть горутинами, это корутины, а на нашем ещё могут называться сопрограммами.

Под С реализации есть, причём с разными подходами.

Bfgeshka ★★★★★
()
Ответ на: комментарий от anonymous

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

Наверняка это все в концентрированном виде описывается, но ссылку на конкретную книгу или мануал не приведу.

eao197 ★★★★★
()
Ответ на: комментарий от eao197

Тогда как линейная запись прикладной логики при использовании зеленых потоков намного проще в восприятии, реализации и сопровождении.

Один мой знакомый node девелопер говорит что когда был callback hell то люди хоть немного думали прежде чем писать, и получалось не так плохо, а с появлением корутин в ноде они начали писать такое же говно как и писали когда были пхпшниками.

Я немного кодил под всякие twisted/tornado, и честно говоря не сильно понимаю о чем говорят люди ругающие их. Может я просто мало кодил.

redixin ★★★★
()
Ответ на: комментарий от redixin

Опыт из других языков программирования — это вещь хорошая. Но каким боком node/twisted/tornado к C++? С++ — это сильно небезопасный язык с кучей своих заморочек. Зеленые потоки способны количество этих заморочек увеличить. Ну или уменьшить :)

eao197 ★★★★★
()
Ответ на: комментарий от eao197

Ага, значит под каждым языком своя, абсолютно чужая другим языкам теория. Ок.

У Boost.Asio нет ничего общего с нодой и торнадо. Всевозможные варианты Futures в С++ не имеют ничего общего с одноименными штуками в жс. Ок.

КА он и в африке КА. Ок?

redixin ★★★★
()
Последнее исправление: redixin (всего исправлений: 1)
Ответ на: комментарий от redixin

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

Не под каждым, но бывает так. К примеру, то что Вы называете Futures, может и аналогичны в перечисленных Вами, но в более других языках они означают совершенно иное. К примеру

fetch(url).then(handle).then(smthElse)
//
fetch(url) handle smthElse
2 большие разницы.

filequest
()
Ответ на: комментарий от filequest

Да, с futures все немного странней. Мы говорили о КА vs coroutines в плане читабельности/понимабильности/сопровождабильности кода.

КА не так уже и плохи. Не понимаю почему они так трудно даются народу, и народ требует корутин.

redixin ★★★★
()
Ответ на: комментарий от redixin

Ага, значит под каждым языком своя, абсолютно чужая другим языкам теория. Ок.

Я не пытался в этом треде говорить про теорию. Надеюсь, с этим Ok.

На практике у callback-ов в C++ есть одна неприятная особенность по сравнению с callback-ами в языках со сборкой мусора: возможность нарваться на повисшие указатели и битые ссылки после того, как какая-то переменная, захваченная в callback-лямбде по ссылке, будет разрушена.

И эта особенность придает активной работе с callback-ами в C++ некоторый привкус, отсутствующий, насколько я помню, в JavaScript и Python.

Надеюсь, с пониманием этого тоже Ok.

Ну и, напоследок. Хотите блеснуть эрудицией или глубокими познаниями, ответьте что-нибудь путное ТС. Показывать мне, что я ничего не знаю, не следует, я сам в курсе. Ok?

eao197 ★★★★★
()
Ответ на: комментарий от redixin

Если Вы говорите об обычных коллбеках в стиле JS до промисов, то это действительно говно, и дело тут не в читаемости, хотя и в ней тоже. И сопрограммы тут не при чем особо, так как все равно используются те же промисы внутри. Самое хреновое в коллбечном стиле — дубовость. Н примеру, что-нибудь такое

fetch(url, onResponse)
// что если, я теперь захотел перед onResponse, сделать еще что-то?
// мне придется переписывать onResponse, вхерачивая туда еще один коллбек. А так я просто пишу
fetch(url)
 .then(somethingElse)
 .then(onResponse)
//а потом еще
fetch(url)
 .then(somethingElse)
 .then(somethingElseElse)
 .then(onResponse)
мне не надо думать сразу о реализации, потому что я могу сделать все в любой момент, ничего не трогая, из того, что было написано раньше, комбинировать все в произвольном порядке. Это шаг в сторону правильной архитектуры. Бонусом получаем еще возможность выскочить из любого вызова прямо в catch. Оборачивать ли это все генераторами, это вопрос второстепенный.

Кроме того, мы думаем о коде линейно, у нас не выворачиваются мозги наизнанку.

Конечно, это все далеко не идеально, но по сравнению с тем что было, это просто рай.

filequest
()
Ответ на: комментарий от filequest

мне придется переписывать

ну и соответственно, переписывать все места где он используется. Я вообще недоумеваю, как какому-то дебилу пришло в голову начать так писать? И это все практикуется до сих пор.

filequest
()
Ответ на: комментарий от arturpub

у потоков 2 назначения:

а) способ разделения ресурсов между разными задачами-контекстами,

б) способ оптимизации скорости работы приложения за счёт многоядерности

next_time ★★★★★
()
Ответ на: комментарий от eao197

Ну и, напоследок. Хотите блеснуть эрудицией или глубокими познаниями, ответьте что-нибудь путное ТС.

Я как раз и говорю о плюсах и минусах тредов и корутин, а так же об «альтернативных тредингу вещах» не забывая о старых добрых КА. Ок? Ок.

возможность нарваться на повисшие указатели и битые ссылки

Сэр не в курсе как с этим принято бороться в крестах? Это неок.

redixin ★★★★
()
Ответ на: комментарий от next_time

а) способ разделения ресурсов между разными задачами-контекстами

Это для тех кто не в курсе про «альтернативные» способы.

Один толковый товарищ говорил «A Computer is a state machine. Threads are for people who can't program state machines»

Конечно же есть годное применение тредам:

б) способ оптимизации скорости работы приложения за счёт многоядерности

Но оно только одно. Всё остальное от лукавого.

redixin ★★★★
()

В треде выше уже были хорошие ответы. Попробую еще добавить кое-что. Например web/rpc серверы часто обрабатывают много IO запросов, в которых время которых мало тяжелой CPU-intensive работы, но зато одновременно происходит выполнение множества одновременных процессов для разных подключений. Процессов в широком смысле слова, а не процессов UNIX. Примером работы таких процессов есть много ожидания событий сети и парсинг форматов протоколов. Если говорить в числах, то например сервер мог бы обслуживать 10000 подключений и по сути только парсить сообщения и отправлять RPC дальше, больше ничего не делая. В таком случае количество состояния в каждом из таких «процессов» может быть например пару килобайт.

Можно использовать по потоку на процесс. Но системные потоки имеют большой системный стек и относительно высокую задержку планировщика когда процессов много.

Можно использовать пул потоков, но тогда сложно обойтись без множества блокировок для его реализации.

Можно распределять подключения между ядрами процессора на уровне дескрипторов входных сокетов, а после распределения обрабатывать каждый набор подключений одним потоком на одном ядре. В таком случае для каждого такого поток будет создан «селектор», в виде например epoll API для Linux. Это обьект, который в цикле выдает из ядра ОС сразу наборы сообщений напрмер о поступающих данных. Тогда в таком же цикле можно их обработать обычно без единой блокировки. Для удобства тут используют несколько техник, одна из них - зеленые потоки. Создается набор " контекстов". Контекст - стек (обычно меньше стандартного системного) и ссылка на функцию и ее аргументы. Потом они все «запускаются» в том смысле что каждой из нтх в цикле могут дать поработать пока они не запросят больше данных и тогда дают поработать следующей. Так они полностью в юзерспейсе переключаются путем сохранения и загрузки регистров процессора. Как только больше данных поступает из селектора - пробуждается зеленый поток и ему дают «поработать» до следующей зеленой блокировки - того момента как он запросит больше данных

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

Вместо зеленых потоков можно использовать классы - машины состояний. Они будут еще компактнее и нету вероятности переполнить стек. Но они могут быть менее читаемы и в целом достаточно запутаны

vertexua ★★★★★
()
Последнее исправление: vertexua (всего исправлений: 3)
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.