Задачи на горутины
1) Что выведет данная программа и почему?
Данная программа предназначена для увеличения значения переменной counter
в 1000 параллельно работающих горутинах. Каждая горутина увеличивает counter
на 1 в цикле, который выполняется 100 раз. Таким образом, теоретически, после выполнения всех горутин, значение counter
должно быть 100000 (1000 горутин * 100 инкрементов).
Однако, в данной программе есть проблема синхронизации доступа к переменной counter
, которая не защищена мьютексом или другим механизмом синхронизации. Это приводит к гонке данных (race condition), когда несколько горутин одновременно изменяют значение counter
. В результате, фактическое значение counter
по завершении всех горутин может быть меньше 100000, так как некоторые инкременты могут "теряться" из-за одновременного доступа к памяти без должной синхронизации.
Чтобы увидеть, какое значение будет у counter
на вашем компьютере, вы можете запустить программу. Однако, без использования мьютекса или атомарных операций, результат будет недетерминированным и может изменяться при каждом запуске программы. Чтобы исправить эту проблему и гарантировать, что counter
достигнет 100000, можно использовать sync.Mutex
для защиты каждого инкремента или использовать sync/atomic
для атомарного инкремента значения counter
.
Для исправления данной программы с использованием sync.Mutex
для корректной синхронизации доступа к переменной counter
, можно добавить мьютекс, который будет блокировать доступ к переменной во время каждого инкремента. Вот исправленный вариант программы:
В этом коде mutex.Lock()
вызывается перед каждым инкрементом counter
, а mutex.Unlock()
— сразу после инкремента. Это гарантирует, что в каждый момент времени только одна горутина может изменять значение counter
, что предотвращает гонку данных и обеспечивает корректное выполнение программы с ожидаемым результатом в 100000.
Для исправления программы с использованием пакета sync/atomic
для атомарных операций с переменной counter
, вы можете использовать функцию atomic.AddInt32
или atomic.AddInt64
в зависимости от типа вашей переменной counter
. Эти функции обеспечивают атомарное увеличение значения переменной, что предотвращает гонку данных без необходимости блокировки мьютексом.
Вот пример исправленной программы с использованием atomic.AddInt64
:
В этом коде:
Тип переменной
counter
изменен наint64
, так как функции из пакетаsync/atomic
требуют указания типа данных.Вместо использования мьютекса для блокировки и разблокировки доступа к переменной, используется функция
atomic.AddInt64
, которая атомарно увеличивает значениеcounter
на 1. Это обеспечивает безопасное изменение значения переменной из нескольких горутин без гонки данных.
Использование атомарных операций обычно более эффективно по сравнению с блокировками мьютексом, особенно в высококонкурентных сценариях, так как они уменьшают накладные расходы на управление блокировками.
2) В один канал поступают числа, во второй канал мы должны отправлять результаты их возведения в квадрат по мере возникновения данных в первом канале. Результаты работы программы должны выводиться на экран.
Для решения этой задачи на языке программирования Go, мы можем использовать горутины и каналы. Ниже приведен пример программы, которая читает числа из одного канала, возводит их в квадрат, отправляет результат в другой канал, и выводит результаты на экран.
Объяснение кода:
Каналы: Создаются два канала,
input
для входных чисел иoutput
для результатов их возведения в квадрат.Горутина для обработки чисел: В первой горутине читаются числа из канала
input
, каждое число возводится в квадрат, и результат отправляется в каналoutput
. После того как все числа обработаны и каналinput
закрыт, каналoutput
также закрывается.Отправка чисел в канал
input
: Во второй горутине числа отправляются в каналinput
. После отправки всех чисел каналinput
закрывается, что сигнализирует первой горутине о том, что все данные были переданы.Чтение и вывод результатов: В основной горутине читаются результаты из канала
output
и выводятся на экран. Как только каналoutput
закрывается, цикл завершается.
Этот пример демонстрирует, как можно использовать горутины и каналы для асинхронной обработки данных в Go.
3) Что выведет данная программа в зависимости от версий Go?
Программа, которую вы привели, создает 10 горутин, каждая из которых печатает значение переменной i
. Однако, из-за особенностей замыканий в Go, результат выполнения программы может зависеть от версии Go и от того, как именно компилятор обрабатывает замыкания.
Проблема с замыканиями
В данном коде все горутины используют одну и ту же переменную i
, которая изменяется в цикле. Это означает, что к моменту выполнения горутин значение i
может быть уже изменено. В результате, все горутины могут напечатать одно и то же значение, которое будет равно последнему значению i
в цикле (в данном случае, 10
).
Ожидаемое поведение
Go 1.19 и ранее
В версиях Go до 1.20 включительно, горутины могут напечатать одно и то же значение i
, которое будет равно 10
, так как все горутины могут начать выполнение после завершения цикла.
Go 1.20 и позже
В Go 1.20 была введена оптимизация, которая может изменить поведение замыканий в горутинах. Однако, даже с этой оптимизацией, результат все равно может быть непредсказуемым, так как горутины могут начать выполнение в любой момент времени, и значение i
может изменяться.
Пример вывода
Go 1.19 и ранее
Go 1.20 и позже
Вывод может быть более разнообразным, но все равно непредсказуемым:
Исправление кода
Чтобы гарантировать, что каждая горутина печатает свое значение i
, нужно передать i
как параметр в анонимную функцию:
Ожидаемый вывод после исправления
Теперь каждая горутина будет печатать свое значение i
, и вывод будет следующим:
Заключение
В версиях Go до 1.20 включительно, все горутины могут напечатать одно и то же значение
i
, равное10
.В Go 1.20 и позже, поведение может быть более разнообразным, но все равно непредсказуемым.
Чтобы гарантировать корректный вывод, нужно передать
i
как параметр в анонимную функцию.
4) Что будет выведено и как исправить?
Давайте разберем, что произойдет при выполнении данного кода, и как его можно исправить.
Описание кода
Создается переменная
wg
типаsync.WaitGroup
.Вызов
wg.Add(1)
увеличивает счетчикWaitGroup
на 1.Запускается первая горутина, которая спит 2 секунды, затем печатает "1" и вызывает
wg.Done()
, уменьшая счетчикWaitGroup
на 1.wg.Wait()
блокирует выполнение основной горутины до тех пор, пока счетчикWaitGroup
не станет равным 0.После завершения первой горутины запускается вторая горутина, которая печатает "2".
Второй вызов
wg.Wait()
не блокирует выполнение, так как счетчикWaitGroup
уже равен 0.Печатается "3".
Проблема
Вторая горутина может завершиться до того, как будет напечатано "3", так как второй вызов wg.Wait()
не блокирует выполнение. Это может привести к тому, что "2" может не быть напечатано до завершения программы.
Ожидаемый вывод
С большой вероятностью вывод будет следующим:
Однако, "2" может быть напечатано, если вторая горутина успеет выполниться до завершения программы:
Исправление
Чтобы гарантировать, что все горутины завершатся до завершения программы, нужно использовать sync.WaitGroup
для всех горутин. Вот исправленный код:
Объяснение исправленного кода
Добавляем еще один вызов
wg.Add(1)
перед запуском второй горутины.Внутри второй горутины вызываем
defer wg.Done()
, чтобы уменьшить счетчикWaitGroup
после завершения горутины.Вызов
wg.Wait()
в конце основной функции гарантирует, что программа не завершится, пока обе горутины не завершат выполнение.
Ожидаемый вывод после исправления
Теперь вывод будет гарантированно следующим:
Заключение
Исправленный код гарантирует, что все горутины завершатся до завершения программы, и вывод будет предсказуемым.
5) Что здесь не так ? Какие есть способы решения ?
В данном коде есть проблема с конкурентным доступом к общей переменной m
(карте). Две горутины (f1
и f2
) одновременно изменяют карту, что может привести к гонкам данных (data races) и непредсказуемому поведению программы.
Проблема
Гонки данных возникают, когда несколько горутин одновременно читают и записывают в одну и ту же переменную без надлежащей синхронизации. В данном случае, карта m
изменяется одновременно из двух горутин, что может привести к некорректным результатам или даже к панике.
Способы решения
Использование мьютекса (
sync.Mutex
):Мьютекс (mutual exclusion) позволяет синхронизировать доступ к общей переменной, гарантируя, что только одна горутина может изменять переменную в любой момент времени.
Использование синхронизированной карты (
sync.Map
):Пакет
sync
предоставляет типsync.Map
, который является безопасным для использования в многопоточной среде.
Решение с использованием мьютекса
Решение с использованием sync.Map
sync.Map
Объяснение решений
Использование мьютекса:
В этом решении используется мьютекс
mu
для синхронизации доступа к картеm
. Каждый раз, когда горутина хочет изменить карту, она захватывает мьютекс с помощьюmu.Lock()
, выполняет изменения и затем освобождает мьютекс с помощьюmu.Unlock()
. Это гарантирует, что только одна горутина может изменять карту в любой момент времени.
Использование
sync.Map
:В этом решении используется
sync.Map
, который является безопасным для использования в многопоточной среде. МетодыLoadOrStore
иStore
обеспечивают атомарные операции для чтения и записи значений. МетодRange
используется для итерации по карте и печати значений.
Заключение
Оба решения обеспечивают корректный и безопасный доступ к общей карте в многопоточной среде. Выбор между ними зависит от конкретных требований и предпочтений. Мьютексы предоставляют более явный контроль над синхронизацией, в то время как sync.Map
упрощает работу с картами в многопоточной среде.
6) Что выведет данная программа и почему?
Разбор кода
Функция
main
:Записывает текущее время в переменную
timeStart
.Запускает две горутины через вызов функции
worker
и ожидает получения значений из двух каналов.Выводит количество секунд, прошедших с момента запуска программы.
Функция
worker
:Создает канал
ch
.Запускает горутину, которая ждет 3 секунды и затем отправляет значение
3
в каналch
.Возвращает канал
ch
.
Что происходит при выполнении программы
В
main
вызываются две функцииworker
, каждая из которых запускает горутину, которая ждет 3 секунды и затем отправляет значение в канал.Оператор
_, _ = <-worker(), <-worker()
ожидает получения значений из обоих каналов.Важно отметить, что оператор
_, _ = <-worker(), <-worker()
выполняет последовательное ожидание значений из каналов, а не параллельное.
Почему программа выводит 6
6
Первая горутина, запущенная функцией
worker
, ждет 3 секунды перед отправкой значения в канал.Оператор
_, _ = <-worker(), <-worker()
сначала ожидает получения значения из первого канала, что занимает 3 секунды.После получения значения из первого канала, оператор переходит к ожиданию значения из второго канала, что занимает еще 3 секунды.
В результате общее время ожидания составляет 3 + 3 = 6 секунд.
Исправление для параллельного ожидания
Чтобы ожидание значений из каналов происходило параллельно, можно использовать sync.WaitGroup
:
Вывод исправленной программы
Исправленная программа выведет 3
, так как обе горутины будут выполняться параллельно, и общее время ожидания составит 3 секунды.
Заключение
Изначальная программа выводит 6
, потому что оператор _, _ = <-worker(), <-worker()
выполняет последовательное ожидание значений из каналов, что приводит к суммарному времени ожидания в 6 секунд. Для параллельного ожидания значений из каналов можно использовать sync.WaitGroup
.
7) Что выведет данный код и почему?
Этот код создает пять горутин, каждая из которых печатает значение переменной i
. Однако, из-за того, что горутины выполняются асинхронно, значение i
может измениться до того, как горутина выполнится. Это приводит к неопределенному поведению, и все горутины могут напечатать одно и то же значение i
, которое будет равно 5 (последнее значение в цикле).
Проблема
Горутины захватывают переменную i
по ссылке, и к моменту выполнения горутины значение i
может измениться. В результате все горутины могут напечатать одно и то же значение.
Поведение в разных версиях Go
В разных версиях Go поведение этого кода может быть одинаковым, так как проблема заключается в захвате переменной i
по ссылке, а не в самой реализации языка. Однако, в зависимости от времени выполнения горутин, результаты могут быть разными.
Go 1.4 и ранее:
В этих версиях Go горутины могут захватывать переменную
i
по ссылке, что приводит к тому, что все горутины могут напечатать одно и то же значениеi
, которое будет равно 5.
Go 1.5 и позже:
В этих версиях Go поведение остается таким же, так как проблема захвата переменной по ссылке не изменилась. Все горутины могут напечатать значение
i
, равное 5.
Пример вывода
В большинстве случаев, независимо от версии Go, вы увидите следующий вывод:
Исправление кода
Чтобы избежать этой проблемы, можно передать текущее значение i
в качестве аргумента в анонимную функцию. Это создаст новую копию переменной i
для каждой горутины.
Исправленный код
Объяснение исправлений
Передача
i
как аргумента:В анонимную функцию передается текущее значение
i
как аргумент. Это создает новую копию переменнойi
для каждой горутины, что предотвращает проблему захвата переменной по ссылке.
Использование
sync.WaitGroup
:Для того чтобы дождаться завершения всех горутин перед завершением программы, используется
sync.WaitGroup
.wg.Add(1)
увеличивает счетчик горутин, которые нужно дождаться.defer wg.Done()
уменьшает счетчик на 1 после завершения работы горутины.wg.Wait()
блокирует выполнение функцииmain
до тех пор, пока счетчик не станет равным нулю, то есть пока все горутины не завершат свою работу.
Заключение
Эти изменения гарантируют, что каждая горутина будет работать с правильным значением переменной i
, и программа дождется завершения всех горутин перед завершением. Это делает код более предсказуемым и корректным.
Last updated