В чем различия goroutine от потока системы
Перейти к содержимому

В чем различия goroutine от потока системы

  • автор:

Backend interview

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

Отличия горутин от потоков

  • Каждый поток операционной системы имеет блок памяти фиксированного размера (зачастую до 2 Мбайт) для стека — рабочей области, в которой он хранит локальные переменные вызовов функций, находящиеся в работе или приостановленные на время вызова другой функции. В противоположность этому go-подпрограмма начинает работу с небольшим стеком, обычно около 2 Кбайт. Стек go-подпрограммы, подобно стеку потока операционной системы, хранит локальные переменные активных и приостановленных функций, но, в отличие от потоков операционной системы, не является фиксированным; при необходимости он может расти и уменьшаться. Максимальный размер стека go-подпрограммы может быть около 1 Гбайта, на порядки больше типичного стека с фиксированным размером, хотя, конечно, такой большой стек могут использовать только несколько go-подпрограмм.
  • Потоки операционной системы планируются в ее ядре, а у go есть собственный планировщик (m:n) мультиплексирующий(раскидывающий) горутинки(m) по потокам(n). Основной плюс = отсуствие оверхеда на переключение контекста.
  • Планировщик Go использует параметр с именем GOMAXPROCS для определения, сколько потоков операционной системы могут одновременно активно выполнять код Go. Его значение по умолчанию равно количеству процессоров компьютера, так что на машине с 8 процессорами планировщик будет планировать код Go для выполнения на 8 потоках одновременно (GOMAXPROCS равно значению п в т:п-планировании). Спящие или заблокированные в процессе коммуникации go-подпрограммы потоков для себя не требуют. Go-подпрограммы, заблокированные в операции ввода-вывода или в других системных вызовах, или при вызове функций, не являющихся функциями Go, нуждаются в потоке операционной системы, но GOMAXPROCS их не учитывает.
  • В большинстве операционных систем и языков программирования, поддерживающих многопоточность, текущий поток имеет идентификацию, которая может быть легко получена как обычное значение (обычно — целое число или указатель). Это облегчает построение абстракции, именуемой локальной памятью потока, которая, по существу, является глобальным отображением, использующим в качестве ключа идентификатор потока, так что каждый поток может сохранять и извлекать значения независимо от других потоков. У горутин нет идентификации, доступной программисту. Так решено во время проектирования языка, поскольку локальной памятью потока программисты злоупотребляют.

context.Context

Хорошей практикой, считается «управлять» горутинами через контекст:

// Контекст предоставляет механизм дедлайнов, сигнал отмены, и доступ к запросозависимым значениям. // Эти методы безопасны для одновременного использования в разных go-рутинах. type Context interface < // Done возвращает канал, который закрывается когда Context отменяется // или по таймауту. Done() // Err объясняет почему контекст был отменен, после того как закрылся канал Done. Err() error // Deadline возвращает время когда этот Context будет отменен. Deadline() (deadline time.Time, ok bool) // Value возвращает значение ассоциированное с ключем или nil. Value(key interface<>) interface<> > 

Лучшие практики

  1. context.Background следует использовать только на самом высоком уровне, как корень всех производных контекстов.
  2. context.TODO должен использоваться, когда вы не уверены, что использовать, или если текущая функция будет использовать контекст в будущем.
  3. Отмены контекста рекомендуются, но эти функции могут занимать время, чтобы выполнить очистку и выход.
  4. context.Value следует использовать как можно реже, и его нельзя применять для передачи необязательных параметров. Это делает API непонятным и может привести к ошибкам. Такие значения должны передаваться как аргументы.
  5. Не храните контексты в структуре, передавайте их явно в функциях, предпочтительно в качестве первого аргумента.
  6. Никогда не передавайте nil-контекст в качестве аргумента. Если сомневаетесь, используйте TODO.
  7. Структура Context не имеет метода cancel, потому что только функция, которая порождает контекст, должна его отменять.
  • https://habr.com/ru/company/nixys/blog/461723/

Goroutines — суть потоки?

В документации Golang как-то неясно раскрывается суть goroutines. Скажите, goroutines — это ничто иное как потоки операционной системы? (т.е. как я пониманию, компилятор Go сам заботится о внутренней реализации этих потоков в зависимости от операционной системы)

Отслеживать
задан 21 июн 2013 в 2:08
136 1 1 серебряный знак 8 8 бронзовых знаков

3 ответа 3

Сортировка: Сброс на вариант по умолчанию

по простому — это более высокоуровневые потоки. К примеру, если нужно выполнять очень мелкие действия в отдельных потоках, то обычные потоки будут накладными — система будет дольше их создавать. для goroutines run-time может переиспользовать один и тот же поток, эмулируя работу настоящих потоков. В результате получается быстро и дешево. Проблема номер два — некоторые процессы могут создавать очень большое количество потоков (например, из за ошибки программиста). goroutines такого не должно случиться — они будут прост в очереди.

Можно считать, что goroutines — это такой встроенный в язык thread pool.

Отслеживать
ответ дан 21 июн 2013 в 7:08
112k 6 6 золотых знаков 93 93 серебряных знака 159 159 бронзовых знаков

Не ясно совершенно. Что значит эмуляция потоков? В документации ясно говориться, что если goroutine «зависает» блокирующей io-операцией, то это никак не сказывается на другие потоки. Но если процесс всего один и псевдопотоки это суть один процесс, то как это все решается?

21 июн 2013 в 14:10

ну пусть себе зависает, кто мешает? есть технологии, которые это позволяют обработать. К примеру (это я фантазирую, просто предполагаю, как я бы это сделал!), когда горутине нужно сделать i/o, то она останавливается (при этом она тратит немного памяти), а сам запрос обрабатывается асинхронно run-time’ом. Когда данные придут, горутине выделяются новый поток или тот же + данные, и она продолжает работать. Так как за этот период горутина не работала, то она не мешала другим. Ожидание легко делаются на select/poll/другая технология. В принципе, ОС это и сама делает.

21 июн 2013 в 14:28
Что подразумевается под run-time’ом в данном случае? Отдельный поток операционной системы?
21 июн 2013 в 15:06

run-time — это набор функций, которые вставляет компилятор, чтобы обеспечить работу программы. Он может быть как внутри exe файла (Delphi), так и отдельными (Builder), так и целыми сложными системами с кучей dll и exe. Он может и создавать потоки (как в Java для GC), и по сети с другими компами связываться. Это личное дело run-time. В случае Go я думаю, что он создает несколько потоков, по которым раскидывает goroutines. Но может и не создавать. Например, если это горутина, которая просто складывает два числа.

Goroutines

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

Две функциональные возможности Go , Goroutines и каналы, упрощают параллелизм, когда используются вместе. Goroutines предлагает базовый первичный тип для этой возможности. Goroutines менее затратны в отношении пространства памяти, межпоточного взаимодействия и сетевого подключения. Они также имеют лучшее время запуска, чем потоки, которые поддерживаются многими языками программирования. По сути, Go относится к Goroutines как то, что Java и другие языки программирования называют потоками.

Что такое Goroutines?

Горутина — это простой поток реализации в Golang, который выполняется одновременно с остальной частью программы. Goroutines доступны при сходстве со стандартными потоками, поскольку стоимость создания горутины чрезвычайно мала. Поэтому они широко используются в Go для параллельного программирования. Каждая программа состоит как минимум из одной программы, известной как главная программа. Главная горутина управляет всеми другими горутинами; таким образом, если главная горутина завершается, то завершаются и все остальные горутины в сценарии. Goroutine всегда активна в фоновом режиме.

Пример: Вот иллюстрация: вы пишете что-то в своем блокноте и проголодались; вы сделаете перерыв и перекусите. Затем снова начинаете писать. Теперь вы выполняете две роли (пишете и едите) в течение времени, которое считается одновременной занятостью. Следует подчеркнуть, что эти две задачи (писать и есть) все равно должны выполняться одновременно. Когда элементы выполняются одновременно, это называется параллелизмом (представьте, что вы пользуетесь мобильным телефоном, одновременно поедая чипсы). Параллелизм включает в себя взаимодействие со многими вещами одновременно. Параллелизм (одновременное выполнение многих действий) является подклассом этого (не обязательно одновременное выполнение) с некоторым расписанием.

Мы можем добавить горутину, применив ключевое слово go к вызову функции. Как только мы применим ключевое слово go к вызову функции, мы установим параллелизм с производительностью. Но сначала давайте определим эффект от применения ключевого слова go к выполнению. Рассмотрим, что в программе есть две горутины. Текст » package main import » будет входом программы. В Go , пакет main import является декларативным оператором импорта. Главная горутина ( first goroutine ) подразумевается. Когда мы выполняем go ( f ), формируется вторая goroutine (0). Обычно при выполнении функции наша программа выполняет каждую команду в основной функции, прежде чем перейти к следующей строке.

С помощью goroutine мы можем быстро выполнить процедуру перед переходом к следующему шагу. В результате был добавлен метод scanln; в противном случае код закончился бы, не успев показать все числа. Goroutines прост в создании и может быть выполнен в большом количестве. Вы заметите, что при выполнении этой программы горутины работают последовательно, а не параллельно.

Преимущества Goroutines перед потоками

Goroutines стоят мало

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

Существуют мультиплексированные Goroutines

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

Goroutines общаться с помощью каналов

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

Что такое каналы?

Две горутины могут взаимодействовать друг с другом и координировать свою работу через каналы. Эта программа будет постоянно печатать » ping «. Чтобы обозначить форму канала, префикс » chan » сопровождается элементами, передаваемыми по каналу.» В данном случае мы предоставляем строки. На канале сообщения отправляются и принимаются с помощью оператора. » ping » означает отправку сообщения » ping «.

Канал используется для синхронизации двух горутин. Прежде чем попытаться отправить сообщение по каналу, маршрутизатор будет ждать, пока принтер не будет готов его принять. Для этого используется термин «торможение». При создании канала можно использовать второй параметр для функции make: c:= make (chan int, 1)

Попробуйте no-code платформу AppMaster

AppMaster поможет создать любое веб, мобильное или серверное приложение в 10 раз быстрее и 3 раза дешевле

Направление канала

Мы можем определить ориентацию формы канала и параметры, чтобы он либо принимал, либо передавал. Например, знак метода Pinger может быть изменен следующим образом:

C может быть отправлено только сейчас. Получение от c приведет к ошибке кода. Аналогично, мы можем изменить вывод на:

func printer(c string -chan)

Полуканал не имеет таких ограничений. Двунаправленный канал может быть передан процедуре, которая принимает только каналы «только передача» или «только прием», но не наоборот.

Как создать Goroutine?

Метод запуска горутины прост. Примените ключевое слово » go » к вызову функции, чтобы создать горутину, которая будет выполняться параллельно. Здесь мы собираемся создать горутину. Предположим, у нас есть программа, содержащая две функции: одна — функция приветствия, а другая — главная функция.

Когда мы создадим новую goroutine, код welcome () будет выполняться параллельно с функцией main(). Когда вы выполните эту программу, вы будете удивлены. Эта программа просто показывает текст основной операции. Что произошло с первой инициированной нами goroutine или новой goroutine? Чтобы понять, почему это произошло, мы должны сначала знать две основные особенности гораутинов.

  • Функция goroutine реагирует мгновенно, когда мы создаем новую goroutine. Кроме функций, управление не полагается на goroutine, поэтому не ждет, пока она завершит свое выполнение. Выполнение передается следующему после функции goroutine блоку кода. Поэтому любые заданные параметры из goroutine не принимаются во внимание.
  • Любые дополнительные программы должны выполняться одновременно с основной программой. Если основная горутина завершится неудачно, программа завершится, и никакие дополнительные горутины выполняться не будут.

Теперь вы понимаете, почему наш код не сработал. После вызова функции go welcome() управление было передано следующей строке кода, которая не дождалась завершения работы программы hello goroutine, и была выведена функция main. Главная goroutine умерла, потому что не было сценария для выполнения, не давая возможности hello Goroutine запуститься.

Мы вызвали технику sleep ( time.sleep(1 * time.second )), чтобы приостановить работу goroutine на 1 секунду. Теперь функция приветствия () имеет достаточно времени, чтобы завершиться перед выходом из основной goroutine. Эта программа сначала пишет » welcome goroutine «, а затем ждет одну секунду, прежде чем вывести главную функцию. Мы используем технику sleep в основной программе, чтобы приостановить ее на мгновение и дать возможность другим программам завершить работу.

Создание множества Goroutines

Мы собираемся запустить еще одну программу для создания нескольких горутин. Для начала мы можем создать две параллельно выполняющиеся горутины. Эти две горутины — горутина чисел[go numbers()] и горутина алфавита [ go alphabets ()].

Гороутина числа остается на 250 мс перед печатью 1, снова отдыхает перед печатью 2, и так далее, пока не выдаст 5. Аналогично, алфавитно-цифровая горутина выводит на экран буквы от a до e, а затем ждет 400 миллисекунд. Наконец, основная горутина создает целые числа и алфавитно-цифровые символы и делает паузу, а затем основная горутина завершает работу.

Частые ошибки параллельного программирования

  • Отсутствие синхронизации, когда это необходимо
  • Использование времени. Sleep для выполнения синхронизаций
  • Оставлять в гораутинах висящие значения копий типов из стандартного пакета синхронизации
  • Вызвать синхронизацию
  • Группа ожидания
  • Добавлять метод в неправильных местах
  • Использовать каналы в качестве закрытых завтрашних каналов, а не от последней функциональной отправляющей горутины

Заключение

В этой статье мы разработали программу, которая использовала ключевое слово go для запуска одной goroutine и нескольких goroutine, которые одновременно печатали целые числа. После запуска этой программы мы создали новый канал, который затем использовали для получения чисел в одной горутине и передачи их в другую горутину, чтобы они могли быть выведены на экран. В качестве последней демонстрации того, как запускать goroutine для ускорения работы приложений на многоядерных системах, вы одновременно запустили множество «печатающих» goroutine.

Как известно, goroutines — это способ более быстрого и эффективного выполнения задач. Это одна из процедур развертывания, предлагаемых AppMaster для улучшения функциональности вашего приложения. С помощью AppMaster даже люди с минимальными знаниями программирования могут выполнять задачи, которые было бы сложно выполнить с помощью ручного кодирования.

AppMaster это no-code платформа, позволяющая создавать мобильные и веб-приложения , а также бэкенд. Интересный факт: AppMaster создает бэкенд на Go со скоростью 22 000 строк в секунду, и вы можете получить доступ к исходному коду.

Потоки в C++ против потоков в Go

После поста про потоки в Go я прочитал другое мнение про общую целесообразность Go в плане продвинутости в многопоточном программировании.

Признаюсь, я не боец в бусте и новом C++, но благодаря предоставленному примеру, было очевидно, что и на С++ решение получается весьма изящное.

Интересно было сравнить производительнось потоков во обоих языках в плане скорости из создания и назначения им работы. Как я понял, это битва между pthreads и системой Go-рутин, которые не являются потоками операционной системы. Как сказано в документации:

Goroutines are multiplexed onto multiple OS threads so if one should block, such as while waiting for I/O, others continue to run. Their design hides many of the complexities of thread creation and management.

Я взял последний boost, и на той же восьми процессорной машине провел эксперимент.

Программе надо будет выполнить множество однотипной работы (фактически, вызвать функцию). Задачи будут мультиплексироваться между несколькими параллельными потоками. Сама функция будет элементарной и быстрой. Надеюсь, этим удастся сфокусировать тестирование именно на подсистеме потоков, нежели на полезной нагрузке.

Итак, программа на Go:

package main import ( "flag" "fmt" ) var jobs *int = flag.Int("jobs", 8, "number of concurrent jobs") var n *int = flag.Int("tasks", 1000000, "number of tasks") func main() flag.Parse() fmt.Printf("- running %d concurrent job(s)\n", *jobs) fmt.Printf("- running %d tasks\n", *n) tasks := make(chan int, *jobs) done := make(chan bool) for i := 0; i < *jobs; i++ go runner(tasks, done) > for i := 1; i n; i++ tasks  i > for i := 0; i < *jobs; i++ tasks  0  done > > func runner(tasks chan int, done chan bool) for  if arg := tasks; arg == 0  break > worker() > done  true > func worker() int  return 0 > 

Makefile для прогона по серии параметров:

target = go_threading all: build build: 6g $(target).go 6l -o $(target) $(target).6 run: (time -p ./$(target) -tasks=$(args) \ 1>/dev/null) 2>&1 | head -1 | awk '< print $$2 >' n = \ 10000 \ 100000 \ 1000000 \ 10000000 \ 100000000 test: @for i in $(n); do \ echo "`printf '% 10d' $$i`" `$(MAKE) args=$$i run`; \ done
#include #include  #include  #include #include #include class thread_pool  typedef boost::function0void> worker; boost::thread_group threads_; std::queueworker> queue_; boost::mutex mutex_; boost::condition_variable cv_; bool done_; public: thread_pool() : done_(false) for(int i = 0; i  boost::thread::hardware_concurrency(); ++i) threads_.create_thread(boost::bind(&thread_pool::run, this)); > void join() threads_.join_all(); > void run() while (true) worker job;  boost::mutex::scoped_lock lock(mutex_); while (queue_.empty() && !done_) cv_.wait(lock); if (queue_.empty() && done_) return; job = queue_.front(); queue_.pop(); > execute(job); > > void execute(const worker& job) job(); > void add(const worker& job) boost::mutex::scoped_lock lock(mutex_); queue_.push(job); cv_.notify_one(); > void finish() boost::mutex::scoped_lock lock(mutex_); done_ = true; cv_.notify_all(); > >; void task() volatile int r = 0; > int main(int argc, char* argv[]) thread_pool pool; int n = argc > 1 ? std::atoi(argv[1]) : 10000; int threads = boost::thread::hardware_concurrency(); std::cout  <"- executing "   <" concurrent job(s)"  ::endl; std::cout  <"- running "   <" tasks"  ::endl; for (int i = 0; i  n; ++i) pool.add(task); > pool.finish(); pool.join(); return 0; > 
BOOST = ~/opt/boost-1.46.1 target = boost_threading build: g++ -O2 -I $(BOOST) -o $(target) \ -lpthread \ -lboost_thread \ -L $(BOOST)/stage/lib \ $(target).cpp run: (time -p LD_LIBRARY_PATH=$(BOOST)/stage/lib ./$(target) $(args) \ 1>/dev/null) 2>&1 | head -1 | awk '< print $$2 >' n = \ 10000 \ 100000 \ 1000000 \ 10000000 \ 100000000 test: @for i in $(n); do \ echo "`printf '% 10d' $$i`" `$(MAKE) args=$$i run`; \ done

В обоих языках число потоков будет равно количеству процессоров — 8. Количество задач, прогоняемых через эти восемь поток будет варьироваться.

Запускаем программу на C++:

make && make -s test g++ -O2 -I ~/opt/boost-1.46.1 -o boost_threading \ -lpthread \ -lboost_thread \ -L ~/opt/boost-1.46.1/stage/lib \ boost_threading.cpp (time -p LD_LIBRARY_PATH=~/opt/boost-1.46.1/stage/lib ./boost_threading \ 1>/dev/null) 2>&1 | head -1 | awk '< print $2 >' 10000 0.03 100000 0.35 1000000 3.43 10000000 29.57 100000000 327.37 
make && make -s test 6g go_threading.go 6l -o go_threading go_threading.6 10000 0.00 100000 0.03 1000000 0.35 10000000 3.72 100000000 38.27 

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

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *