Статьи

Пойдем: Параллельность Голанга, часть 1

У каждого успешного языка программирования есть особенность, которая сделала его успешным. Сильная сторона Go — параллельное программирование. Он был разработан для сильной теоретической модели (CSP) и предоставляет синтаксис уровня языка в форме ключевого слова «go», которое запускает асинхронную задачу (да, язык назван в честь ключевого слова), а также встроенный способ общаться между параллельными задачами.

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

CSP выступает за сообщение последовательных процессов . Впервые он был представлен Тони (CAR) Хоаром в 1978 году. CSP — это высокоуровневая структура для описания параллельных систем. Гораздо проще программировать правильные параллельные программы при работе на уровне абстракции CSP, чем на типичных потоках и уровне абстракции блокировок.

Goroutines — игра на сопрограммах . Однако они не совсем одинаковы. Goroutine — это функция, которая выполняется в отдельном потоке от потока запуска, поэтому она не блокирует его. Несколько программ могут использовать один и тот же поток ОС. В отличие от сопрограмм, подпрограммы не могут явно передать управление другой подпрограмме. Среда выполнения Go заботится о неявной передаче управления, когда конкретная программа будет блокировать доступ к вводу / выводу.

Давайте посмотрим код. Программа Go ниже определяет функцию, творчески называемую «f», которая произвольно спит до полсекунды и затем печатает свой аргумент. Функция main() вызывает функцию f() в цикле из четырех итераций, где на каждой итерации она вызывает f() три раза с «1», «2» и «3» в строке. Как и следовало ожидать, вывод:

01
02
03
04
05
06
07
08
09
10
11
12
13
— Run sequentially as normal functions
1
2
3
1
2
3
1
2
3
1
2
3

Затем main вызывает f() как программу в аналогичном цикле. Теперь результаты различны, потому что среда выполнения Go будет запускать f одновременно, а затем, поскольку случайный сон различен для подпрограмм, печать значений не происходит в порядке вызова f() . Вот вывод:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
— Run concurrently as goroutines
2
2
3
1
3
2
1
3
1
1
3
2
2
1
3

Сама программа использует пакеты стандартных библиотек «time» и «math / rand» для реализации случайного сна и ожидания в конце для завершения всех процедур. Это важно, потому что, когда основной поток завершается, программа завершается, даже если все еще выполняются ожидающие выполнения программы.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package main
 
import (
    «fmt»
    «time»
    «math/rand»
)
 
var r = rand.New(rand.NewSource(time.Now().UnixNano()))
 
func f(s string) {
    // Sleep up to half a second
    delay := time.Duration(r.Int() % 500) * time.Millisecond
    time.Sleep(delay)
    fmt.Println(s)
}
 
 
func main() {
    fmt.Println(«— Run sequentially as normal functions»)
    for i := 0;
        f(«1»)
        f(«2»)
        f(«3»)
    }
 
    fmt.Println(«— Run concurrently as goroutines»)
    for i := 0;
        go f(«1»)
        go f(«2»)
        go f(«3»)
    }
     
    // Wait for 6 more seconds to let all go routine finish
    time.Sleep(time.Duration(6) * time.Second)
    fmt.Println(«— Done.»)
}

Когда у вас есть куча диких горутинов, бегающих повсюду, вы часто хотите знать, когда они все закончили.

Есть разные способы сделать это, но одним из лучших подходов является использование WaitGroup . WaitGroup — это тип, определенный в пакете «sync», который обеспечивает операции Add() , Done() и Wait() . Он работает как счетчик, который подсчитывает, сколько подпрограмм го все еще активно, и ждет, пока они все не завершат. Всякий раз, когда вы запускаете новую программу, вы вызываете Add(1) (вы можете добавить более одной, если вы запускаете многократные процедуры). Когда выполнение процедуры завершено, она вызывает Done() , который уменьшает счет на один, и блоки Wait() пока счет не достигнет нуля.

Давайте преобразуем предыдущую программу, чтобы использовать WaitGroup вместо сна в течение шести секунд на всякий случай в конце. Обратите внимание, что функция f() использует defer wg.Done() вместо wg.Done() вызова wg.Done() . Это полезно, чтобы гарантировать, что wg.Done() всегда вызывается, даже если есть проблема, и выполнение программы завершается рано. В противном случае счетчик никогда не достигнет нуля, и wg.Wait() может заблокироваться навсегда.

Еще один маленький трюк заключается в том, что я вызываю wg.Add(3) только один раз, прежде чем трижды вызвать f() . Обратите внимание, что я вызываю wg.Add() даже при вызове f() в качестве обычной функции. Это необходимо, потому что f() вызывает wg.Done() независимо от того, работает ли он как функция или процедура.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package main
 
import (
    «fmt»
    «time»
    «math/rand»
    «sync»
)
 
var r = rand.New(rand.NewSource(time.Now().UnixNano()))
var wg sync.WaitGroup
 
func f(s string) {
    defer wg.Done()
    // Sleep up to half a second
    delay := time.Duration(r.Int() % 500) * time.Millisecond
    time.Sleep(delay)
    fmt.Println(s)
}
 
 
func main() {
    fmt.Println(«— Run sequentially as normal functions»)
    for i := 0;
        wg.Add(3)
        f(«1»)
        f(«2»)
        f(«3»)
         
    }
 
    fmt.Println(«— Run concurrently as goroutines»)
    for i := 0;
        wg.Add(3)
        go f(«1»)
        go f(«2»)
        go f(«3»)
    }
     
    wg.Wait()
}

Программы в программе 1,2,3 не взаимодействуют друг с другом и не работают с общими структурами данных. В реальном мире это часто необходимо. Пакет sync предоставляет тип Mutex с методами Lock() и Unlock() которые обеспечивают взаимное исключение. Отличным примером является стандартная карта Go.

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

Давайте сложим все это вместе. У знаменитого Tour of Go есть упражнение по созданию веб-сканера. Они предоставляют отличную структуру с фиктивным сборщиком и результатами, которые позволяют вам сосредоточиться на проблеме под рукой. Я настоятельно рекомендую вам попробовать решить это самостоятельно.

Я написал полное решение, используя два подхода: синхронизированная карта и каналы. Полный исходный код доступен здесь .

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

1
2
3
4
var fetchedUrls = struct {
    urls map[string]bool
    m sync.Mutex
}{urls: make(map[string]bool)}

Теперь код может заблокировать мьютекс перед доступом к карте URL-адресов и разблокировать, когда это будет сделано.

01
02
03
04
05
06
07
08
09
10
// Check if this url has already been fetched (or being fetched)
   fetchedUrls.m.Lock()
   if fetchedUrls.urls[url] {
       fetchedUrls.m.Unlock()
       return
   }
 
   // OK.
   fetchedUrls.urls[url] = true
   fetchedUrls.m.Unlock()

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

Go имеет превосходную поддержку параллелизма с использованием легких программ. Это намного проще в использовании, чем традиционные темы. Когда вам нужно синхронизировать доступ к разделяемым структурам данных, Go всегда поддерживает sync.Mutex .

Есть еще много чего рассказать о параллелизме Go. Будьте на связи…