Задачи на горутины

1) Что выведет данная программа и почему?

package main
import (
	"fmt"
	"sync"
)
	
func main() {
	var counter int
	var wg sync.WaitGroup
	wg.Add(1000)

	for i := 0; i < 1000; i++ {
		go func() {
			for j := 0; j < 100; j++ {
				counter++
			}
			wg.Done()
		}()
	}
	wg.Wait()
	fmt.Println(counter)
}

Данная программа предназначена для увеличения значения переменной 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, можно добавить мьютекс, который будет блокировать доступ к переменной во время каждого инкремента. Вот исправленный вариант программы:

package main

import (
	"fmt"
	"sync"
)

func main() {
	var counter int
	var wg sync.WaitGroup
	var mutex sync.Mutex // Добавляем мьютекс для синхронизации доступа к counter

	wg.Add(1000)

	for i := 0; i < 1000; i++ {
		go func() {
			for j := 0; j < 100; j++ {
				mutex.Lock() // Блокируем мьютекс перед изменением counter
				counter++
				mutex.Unlock() // Разблокируем мьютекс после изменения counter
			}
			wg.Done()
		}()
	}
	wg.Wait()
	fmt.Println(counter) // Теперь должно всегда выводить 100000
}

В этом коде mutex.Lock() вызывается перед каждым инкрементом counter, а mutex.Unlock() — сразу после инкремента. Это гарантирует, что в каждый момент времени только одна горутина может изменять значение counter, что предотвращает гонку данных и обеспечивает корректное выполнение программы с ожидаемым результатом в 100000.

Для исправления программы с использованием пакета sync/atomic для атомарных операций с переменной counter, вы можете использовать функцию atomic.AddInt32 или atomic.AddInt64 в зависимости от типа вашей переменной counter. Эти функции обеспечивают атомарное увеличение значения переменной, что предотвращает гонку данных без необходимости блокировки мьютексом.

Вот пример исправленной программы с использованием atomic.AddInt64:

package main

import (
	"fmt"
	"sync"
	"sync/atomic"
)

func main() {
	var counter int64 // Изменим тип переменной на int64 для использования с atomic
	var wg sync.WaitGroup

	wg.Add(1000)

	for i := 0; i < 1000; i++ {
		go func() {
			for j := 0; j < 100; j++ {
				atomic.AddInt64(&counter, 1) // Атомарно увеличиваем counter на 1
			}
			wg.Done()
		}()
	}
	wg.Wait()
	fmt.Println(counter) // Теперь должно всегда выводить 100000
}

В этом коде:

  • Тип переменной counter изменен на int64, так как функции из пакета sync/atomic требуют указания типа данных.

  • Вместо использования мьютекса для блокировки и разблокировки доступа к переменной, используется функция atomic.AddInt64, которая атомарно увеличивает значение counter на 1. Это обеспечивает безопасное изменение значения переменной из нескольких горутин без гонки данных.

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

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

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

package main

import (
	"fmt"
)

func main() {
	// Создаем канал для входных чисел и канал для результатов
	input := make(chan int)
	output := make(chan int)

	// Горутина для чтения чисел, возведения их в квадрат и отправки результатов
	go func() {
		for num := range input {
			output <- num * num
		}
		close(output)
	}()

	// Отправляем числа в канал input
	go func() {
		for _, num := range []int{2, 4, 6, 8, 10} {
			input <- num
		}
		close(input)
	}()

	// Читаем и выводим результаты из канала output
	for result := range output {
		fmt.Println(result)
	}
}

Объяснение кода:

  1. Каналы: Создаются два канала, input для входных чисел и output для результатов их возведения в квадрат.

  2. Горутина для обработки чисел: В первой горутине читаются числа из канала input, каждое число возводится в квадрат, и результат отправляется в канал output. После того как все числа обработаны и канал input закрыт, канал output также закрывается.

  3. Отправка чисел в канал input: Во второй горутине числа отправляются в канал input. После отправки всех чисел канал input закрывается, что сигнализирует первой горутине о том, что все данные были переданы.

  4. Чтение и вывод результатов: В основной горутине читаются результаты из канала output и выводятся на экран. Как только канал output закрывается, цикл завершается.

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

3) Что выведет данная программа в зависимости от версий Go?

package main

import (
 "fmt"
 "sync"
)

func main() {
wg := new(sync.WaitGroup)


for i := 0; i < 10; i++ {
    wg.Add(1)

    go func() {
        fmt.Println(i)
        wg.Done()
    }()
}

wg.Wait()

} 

Программа, которую вы привели, создает 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 и ранее

10
10
10
10
10
10
10
10
10
10

Go 1.20 и позже

Вывод может быть более разнообразным, но все равно непредсказуемым:

3
0
1
2
7
8
9
5
4
6

Исправление кода

Чтобы гарантировать, что каждая горутина печатает свое значение i, нужно передать i как параметр в анонимную функцию:

package main

import (
	"fmt"
	"sync"
)

func main() {
	wg := new(sync.WaitGroup)

	for i := 0; i < 10; i++ {
		wg.Add(1)

		go func(i int) {
			defer wg.Done()
			fmt.Println(i)
		}(i)
	}

	wg.Wait()
}

Ожидаемый вывод после исправления

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

0
1
2
3
4
5
6
7
8
9

Заключение

  • В версиях Go до 1.20 включительно, все горутины могут напечатать одно и то же значение i, равное 10.

  • В Go 1.20 и позже, поведение может быть более разнообразным, но все равно непредсказуемым.

  • Чтобы гарантировать корректный вывод, нужно передать i как параметр в анонимную функцию.

4) Что будет выведено и как исправить?

package main

import (
"fmt"
"sync"
"time"
)

func main() {
	var wg sync.WaitGroup

	wg.Add(1)
	go func() {
		time.Sleep(time.Second * 2)
		fmt.Println("1")
		wg.Done()
	}()

	wg.Wait()

	go func() {
		fmt.Println("2")
	}()

	wg.Wait()
	fmt.Println("3")

}

Давайте разберем, что произойдет при выполнении данного кода, и как его можно исправить.

Описание кода

  1. Создается переменная wg типа sync.WaitGroup.

  2. Вызов wg.Add(1) увеличивает счетчик WaitGroup на 1.

  3. Запускается первая горутина, которая спит 2 секунды, затем печатает "1" и вызывает wg.Done(), уменьшая счетчик WaitGroup на 1.

  4. wg.Wait() блокирует выполнение основной горутины до тех пор, пока счетчик WaitGroup не станет равным 0.

  5. После завершения первой горутины запускается вторая горутина, которая печатает "2".

  6. Второй вызов wg.Wait() не блокирует выполнение, так как счетчик WaitGroup уже равен 0.

  7. Печатается "3".

Проблема

Вторая горутина может завершиться до того, как будет напечатано "3", так как второй вызов wg.Wait() не блокирует выполнение. Это может привести к тому, что "2" может не быть напечатано до завершения программы.

Ожидаемый вывод

С большой вероятностью вывод будет следующим:

1
3

Однако, "2" может быть напечатано, если вторая горутина успеет выполниться до завершения программы:

1
2
3

Исправление

Чтобы гарантировать, что все горутины завершатся до завершения программы, нужно использовать sync.WaitGroup для всех горутин. Вот исправленный код:

package main

import (
	"fmt"
	"sync"
	"time"
)

func main() {
	var wg sync.WaitGroup

	wg.Add(1)
	go func() {
		defer wg.Done()
		time.Sleep(time.Second * 2)
		fmt.Println("1")
	}()

	wg.Add(1)
	go func() {
		defer wg.Done()
		fmt.Println("2")
	}()

	wg.Wait()
	fmt.Println("3")
}

Объяснение исправленного кода

  1. Добавляем еще один вызов wg.Add(1) перед запуском второй горутины.

  2. Внутри второй горутины вызываем defer wg.Done(), чтобы уменьшить счетчик WaitGroup после завершения горутины.

  3. Вызов wg.Wait() в конце основной функции гарантирует, что программа не завершится, пока обе горутины не завершат выполнение.

Ожидаемый вывод после исправления

Теперь вывод будет гарантированно следующим:

1
2
3

Заключение

Исправленный код гарантирует, что все горутины завершатся до завершения программы, и вывод будет предсказуемым.

5) Что здесь не так ? Какие есть способы решения ?

package main

import (
	"fmt"
	"time"
)

var m map[string]int

func main() {
	m = make(map[string]int)
	go f1()
	go f2()

	time.Sleep(time.Second * 1)
	fmt.Printf("print map = %+v", m)

}

func f1() {
	for i := 0; i < 100000; i++ {
		m["f1"]++
	}
}

func f2() {
	for i := 0; i < 100000; i++ {
		m["f2"]++
	}
}

В данном коде есть проблема с конкурентным доступом к общей переменной m (карте). Две горутины (f1 и f2) одновременно изменяют карту, что может привести к гонкам данных (data races) и непредсказуемому поведению программы.

Проблема

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

Способы решения

  1. Использование мьютекса (sync.Mutex):

    • Мьютекс (mutual exclusion) позволяет синхронизировать доступ к общей переменной, гарантируя, что только одна горутина может изменять переменную в любой момент времени.

  2. Использование синхронизированной карты (sync.Map):

    • Пакет sync предоставляет тип sync.Map, который является безопасным для использования в многопоточной среде.

Решение с использованием мьютекса

package main

import (
	"fmt"
	"sync"
	"time"
)

var (
	m  map[string]int
	mu sync.Mutex
)

func main() {
	m = make(map[string]int)
	go f1()
	go f2()

	time.Sleep(time.Second * 1)
	mu.Lock()
	fmt.Printf("print map = %+v", m)
	mu.Unlock()
}

func f1() {
	for i := 0; i < 100000; i++ {
		mu.Lock()
		m["f1"]++
		mu.Unlock()
	}
}

func f2() {
	for i := 0; i < 100000; i++ {
		mu.Lock()
		m["f2"]++
		mu.Unlock()
	}
}

Решение с использованием sync.Map

package main

import (
	"fmt"
	"sync"
	"time"
)

var m sync.Map

func main() {
	go f1()
	go f2()

	time.Sleep(time.Second * 1)
	printMap()
}

func f1() {
	for i := 0; i < 100000; i++ {
		val, _ := m.LoadOrStore("f1", 0)
		m.Store("f1", val.(int)+1)
	}
}

func f2() {
	for i := 0; i < 100000; i++ {
		val, _ := m.LoadOrStore("f2", 0)
		m.Store("f2", val.(int)+1)
	}
}

func printMap() {
	m.Range(func(key, value interface{}) bool {
		fmt.Printf("%s: %d\n", key, value)
		return true
	})
}

Объяснение решений

  1. Использование мьютекса:

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

  2. Использование sync.Map:

    • В этом решении используется sync.Map, который является безопасным для использования в многопоточной среде. Методы LoadOrStore и Store обеспечивают атомарные операции для чтения и записи значений. Метод Range используется для итерации по карте и печати значений.

Заключение

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

6) Что выведет данная программа и почему?

package main

import(
	"time"
)

func main() {
	timeStart := time.Now()
	_, _ = <-worker(), <-worker()
	
	println(int(time.Since(timeStart).Seconds()))	
}
	
func worker() chan int {
	ch := make(chan int)
	go func() {
		time.Sleep(3 * time.Second)
		ch <- 3
	}()
	return ch
}

Разбор кода

  1. Функция main:

    • Записывает текущее время в переменную timeStart.

    • Запускает две горутины через вызов функции worker и ожидает получения значений из двух каналов.

    • Выводит количество секунд, прошедших с момента запуска программы.

  2. Функция worker:

    • Создает канал ch.

    • Запускает горутину, которая ждет 3 секунды и затем отправляет значение 3 в канал ch.

    • Возвращает канал ch.

Что происходит при выполнении программы

  • В main вызываются две функции worker, каждая из которых запускает горутину, которая ждет 3 секунды и затем отправляет значение в канал.

  • Оператор _, _ = <-worker(), <-worker() ожидает получения значений из обоих каналов.

  • Важно отметить, что оператор _, _ = <-worker(), <-worker() выполняет последовательное ожидание значений из каналов, а не параллельное.

Почему программа выводит 6

  • Первая горутина, запущенная функцией worker, ждет 3 секунды перед отправкой значения в канал.

  • Оператор _, _ = <-worker(), <-worker() сначала ожидает получения значения из первого канала, что занимает 3 секунды.

  • После получения значения из первого канала, оператор переходит к ожиданию значения из второго канала, что занимает еще 3 секунды.

  • В результате общее время ожидания составляет 3 + 3 = 6 секунд.

Исправление для параллельного ожидания

Чтобы ожидание значений из каналов происходило параллельно, можно использовать sync.WaitGroup:

package main

import (
	"fmt"
	"sync"
	"time"
)

func main() {
	timeStart := time.Now()
	var wg sync.WaitGroup
	wg.Add(2)

	go func() {
		defer wg.Done()
		<-worker()
	}()

	go func() {
		defer wg.Done()
		<-worker()
	}()

	wg.Wait()
	fmt.Println(int(time.Since(timeStart).Seconds()))
}

func worker() chan int {
	ch := make(chan int)
	go func() {
		time.Sleep(3 * time.Second)
		ch <- 3
	}()
	return ch
}

Вывод исправленной программы

Исправленная программа выведет 3, так как обе горутины будут выполняться параллельно, и общее время ожидания составит 3 секунды.

Заключение

Изначальная программа выводит 6, потому что оператор _, _ = <-worker(), <-worker() выполняет последовательное ожидание значений из каналов, что приводит к суммарному времени ожидания в 6 секунд. Для параллельного ожидания значений из каналов можно использовать sync.WaitGroup.

7) Что выведет данный код и почему?

package main

import "fmt"

func main() {
    for i := 0; i < 5; i++ {
    go func() {
        fmt.Println(i)
    }()
    }
}

Этот код создает пять горутин, каждая из которых печатает значение переменной i. Однако, из-за того, что горутины выполняются асинхронно, значение i может измениться до того, как горутина выполнится. Это приводит к неопределенному поведению, и все горутины могут напечатать одно и то же значение i, которое будет равно 5 (последнее значение в цикле).

Проблема

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

Поведение в разных версиях Go

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

  1. Go 1.4 и ранее:

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

  2. Go 1.5 и позже:

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

Пример вывода

В большинстве случаев, независимо от версии Go, вы увидите следующий вывод:

5
5
5
5
5

Исправление кода

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

Исправленный код

package main

import (
	"fmt"
	"sync"
)

func main() {
	var wg sync.WaitGroup

	for i := 0; i < 5; i++ {
		wg.Add(1)
		go func(i int) {
			defer wg.Done()
			fmt.Println(i)
		}(i)
	}

	wg.Wait()
}

Объяснение исправлений

  1. Передача i как аргумента:

    • В анонимную функцию передается текущее значение i как аргумент. Это создает новую копию переменной i для каждой горутины, что предотвращает проблему захвата переменной по ссылке.

  2. Использование sync.WaitGroup:

    • Для того чтобы дождаться завершения всех горутин перед завершением программы, используется sync.WaitGroup.

    • wg.Add(1) увеличивает счетчик горутин, которые нужно дождаться.

    • defer wg.Done() уменьшает счетчик на 1 после завершения работы горутины.

    • wg.Wait() блокирует выполнение функции main до тех пор, пока счетчик не станет равным нулю, то есть пока все горутины не завершат свою работу.

Заключение

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

Last updated