cpp

Многопоточные приложения

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

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

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

Запуск задачи в отдельном потоке

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

Запустить задачу в отдельном потоке очень просто. Достаточно перед вызовом функции указать ключевое слово go. Все инструкции внутри функции будут выполнены в отдельном потоке.

Поток завершает работу в следующих случаях:

  • пользовательская функция, использованная для создания потока, завершила свою работу (например, поток управления достиг конца функции или встретился оператор return);
  • завершается процесс, например, поток управления достиг конца функции main().

В качестве примера запустим три потока, внутри которых просто будем выводить данные в окно консоли (листинг 18.1).

Листинг 18.1. Создание потока с помощью обычной функции

package main

import (
   "fmt"
   "time"
)

func main() {
   fmt.Println("Начало функции main()")
   for i := 1; i < 4; i++ {
      go test(i)
   }
   time.Sleep(20 * time.Second) // Ожидаем завершения потоков
   fmt.Println("Конец функции main()")
}
func test(n int) {
   for i := 1; i < 11; i++ {
      fmt.Println("Поток:", n, "i =", i)
      time.Sleep(time.Second) // Имитация выполнения задачи
   }
}

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

Поток: 1 i = 1
Поток: 3 i = 1
Поток: 2 i = 1

Как видно из результата, поток 2, вызванный перед потоком 3, был выполнен после потока 3. В этом результате наоборот:

Поток: 2 i = 2
Поток: 3 i = 2
Поток: 1 i = 2

Обратите также внимание на эту инструкцию:

time.Sleep(20 * time.Second) // Ожидаем завершения потоков

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

Начало функции main()
Конец функции main()

Иными словами, потоки даже не успевают запуститься. Поэтому важно дождаться окончания выполнения потоков внутри функции main().

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

Ключевое слово go можно указать не только перед вызовом обычной функции, но и перед вызовом анонимной функции. В этом случае захватывается контекст (для доступа к данным требуется синхронизация). Переделаем код из листинга 18.1 и используем анонимную функцию вместо функции test() (листинг 18.2).

Листинг 18.2. Создание потока с помощью анонимной функции

package main

import (
   "fmt"
   "time"
)

func main() {
   fmt.Println("Начало функции main()")
   for i := 1; i < 4; i++ {
      go func(n int) {
         for j := 1; j < 11; j++ {
            fmt.Println("Поток:", n, "j =", j)
            time.Sleep(time.Second) // Имитация выполнения задачи
         }
      }(i)
   }
   fmt.Scanln() // Ожидаем завершения потоков
   fmt.Println("Конец функции main()")
}

Учебник Go (Golang)
Учебник Go (Golang) в формате PDF

Помощь сайту

ЮMoney (Yandex-деньги): 410011140483022

ПАО Сбербанк:
Счет: 40817810855006152256
Реквизиты банка:
Наименование: СЕВЕРО-ЗАПАДНЫЙ БАНК ПАО СБЕРБАНК
Корреспондентский счет: 30101810500000000653
БИК: 044030653
КПП: 784243001
ОКПО: 09171401
ОКОНХ: 96130
Скриншот реквизитов

cpp