Комментарии 16
У вас один и тот же код для блокирующего и не блокирующего сокета
UPD: спасибо! был один и тот же код, поправили. Сейчас актуальный вариант.
Для блокирующего варианта используется простой вызов accept(), для неблокирующего варианта уже accept4 с флагом SOCK_NONBLOCK дополнительно обернутый в do/while для разрешения ситуации с EAGAIN.
Спасибо за внимательность! Поправили
пробежал бегло по тексту глаз зацепился за 2 утверждения
""При использовании pthread
из набора стандартной библиотеки С glibc в Linux доступен богатый выбор примитивов синхронизации: мьютексы, условные переменные, спинлоки и барьеры, которые обычно редко где используются в коде на С ""
что значит редко ... по надобности использутся, если аппликация multithreaded то без них нельзя
и да, как говориться в С так носят, конечно сегодня выбор С для написания чего либо не всегда очевиден, но когда кроме него ничего не было , по другому и нельзя.
Все эти примитивы широко используются везде и всегда
"Используя даже самые простые механизмы в виде базовых примитивов синхронизации, предоставляемых операционной системой и стандартной библиотекой языка С, можно проектировать и создавать достаточно гибкие в плане функциональности многопоточные приложения. "
все языки в итоге компилирутся/интерпретируються и работают на конкретной опреционой системе
и ничем кроме того что операционная система предостволяет пользоваться не могут ..
примитивы С максимально приближены к ОС
все остальное реализовано через них и поверх них
так что Go тоже пользуеться ими же
Спасибо за размышления!
Из личного опыта мысль про редкость использования относится в основном к pthread барьерам, остальные примитивы используются почти повсеместно. Вероятно оттого что данный примитив не так сильно распространен и используется для ограниченного круга задач. Встречались различные вариации других примитивов явно напоминающие использование барьера, например, счетный вариант (EFD_SEMAPHORE) eventfd или сами семафоры в чистом виде.
Не полностью понял часть комментария про примитивы С и основу ОС.
Про привязку примитивов синхронизации к ОС вопрос достаточно обширный только потому что у Golang необходимо их рассматривать как часть рантайма: там в целом очень большое число оптимизаций для того чтобы избегать накладных расходов в вызовы ОС и обходиться поддержкой уровня горутин. Сам golang в чистом виде не использует вызовы С для имплементации примитивов синхронизации (системные вызовы, например, futex тут не рассматриваю).
Но основа так или иначе это одна - ОС которая предоставляет основные возможности для базовых операций.
Итак, мои выводы: хотя на языке C можно достичь высокой производительности и эффективности при работе с потоками, разработка и отладка подобного кода требует значительных усилий. С другой стороны, Go предоставляет более высокоуровневые средства для работы с параллелизмом, что делает разработку проще и удобнее. В конечном итоге выбор между этими языками зависит от конкретных требований проекта и предпочтений разработчика.
Что насчет сравнения потребления памяти в реализации GO по сравнению с Си от 1к потоков например?
Чисто с теоретической точки зрения, у Go никаких проблем не будет, потому что там стек горутины (собственно, гошного легковесного потока) 2-8 кбайт в зависимости от версии. И работать это будет поверх реальных потоков, которых будет выделено не больше, чем их доступно в ОС.
На Си, если я правильно понимаю, два варианта:
Поднимать 1000 потоков, мне кажется, будет крайне жирно, вес будет зависеть от реализации в ОС (1-4 мбайт вроде в зависимости от битности)
Писать самопальные горутины с собственным стеком как в Go поверх реальных потоков, и выйдет то же самое
Как я уже писал выше, при обработке нового входящего запроса проще создать новую горутину и поручить обработку запроса ей
Насколько мне известно, горутины это стекфул корутины, а от сегментированного стека Go в какой-то момент отказался в связи с оверхедом и прочими сложностями, а создание стекфул корутины вполне сравнимо по затратам с созданием целого ОС треда. Нагрузка на шедулер (просто другой) возрастёт, памяти потребляется много, так что я сомневаюсь, что это хорошее решение
Опишем общую концепцию работы потока в пуле: при получении задания после пробуждения из сна поток захватывает «свой» мьютекс и держит его на протяжении всей обработки запроса. Отпускает лишь в самом конце — и так циклично, на протяжении всего времени работы.
Зачем? Просто зачем он держит мьютекс не только во время получения задачи, а постоянно всё время работы? Это похоже на неправильное использование мьютекса
Также ожидал и не увидел самое наивное устройство тредпула - одна очередь с кондваром и ожидающими работы потоками. В этой схеме не нужно искать свободного - он сам найдёт работу, в общем отпадают все поставленные вопросы:
как выбирать свободный поток в пуле, готовый обработать запрос,
как пробудить поток и сообщить ему о новом запросе на обработку,
что если на очередном запросе не будет свободных потоков в пуле.
Конечно это далеко неидеальный тредпул, но я не вижу недостатков по сравнению с предложенным в статье. Плюс можно рассмотреть дальнейшие варианты с несколькими очередями и разными стратегиями выбора потока и тд (https://github.com/kelbon/kelcoro/blob/main/include/thread_pool.hpp). Тут уже добавляет своих проблем странное ограничение на запрет С++, при том что всё апи линукса почему-то разрешено
Насколько мне известно, горутины это стекфул корутины, а от сегментированного стека Go в какой-то момент отказался в связи с оверхедом и прочими сложностями, а создание стекфул корутины вполне сравнимо по затратам с созданием целого ОС треда. Нагрузка на шедулер (просто другой) возрастёт, памяти потребляется много, так что я сомневаюсь, что это хорошее решение
Вот замеры для Linux - Goroutines Are Not Significantly Smaller Than Threads и комментарии к ним
потребление памяти у стекфул корутин и тредов одинаковое, можно изменить размер стека и у корутин просто стек по дефолту поменьше. Насколько я вижу реализации использующие стекфул корутины по той или иной причине скатываются к просто созданию ещё одной целой корутины на запрос, что по итогу эквивалентно одному треду на запрос от которого они хотели спасти свой код (только всё стало гораздо сложнее + свой шедулер)
Конкретно я говорю про Go и userver, я не отрицаю что можно сделать хорошо, только вот люди не хотят думать и писать хороший код
"Создавать потоки в Linux даже на С, используя glibc, несложно.."
--------------------
Создавать потоки в Windows на С еще проще. Для этого нужна всего одна функция QueueUserWorkItem , которая помещает рабочий элемент(функцию пользователя) в очередь рабочего потока в пуле потоков, который создает OС. ttps://learn.microsoft.com/en-us/windows/win32/procthread/thread-pooling
https://learn.microsoft.com/ru-ru/windows/win32/procthread/thread-pools
А вы не рассматривали вопрос насколько эффективно иметь в пул-потоков количество потоков большее (или меньшее) чем количество физических процессоров (аппаратных ядер) в системе?
Как выбирается/чем определяется количество доступных потоков в пуле?
Укрощение примитивов синхронизации: сравниваем решения задачи с построением пула потоков на С и Go