The Go Blog

Делитесь памятью, используя коммуникацию

Эндрю Герранд
13 июля 2010

Традиционные модели работы с потоками (как правило, используемые при написании программ на Java, C++ и Python, например) требуют от программиста общения между потоками с использованием общей памяти. Обычно структуры общих данных защищаются блокировками, и потоки конкурируют за эти блокировки для доступа к данным. В некоторых случаях это упрощается использованием потокобезопасных структур данных, таких как очередь (Queue) в Python.

Примитивы параллелизма в Go — горутины и каналы — предоставляют элегантное и отличное средство для структурирования конкурентного программного обеспечения. (Эти концепции имеют интересную историю, которая начинается с языка C. А. Р. Хоара и его Коммуникационные последовательные процессы.) Вместо того чтобы явно использовать блокировки для управления доступом к общим данным, Go поощряет использование каналов для передачи ссылок на данные между горутинами. Этот подход гарантирует, что в любой момент времени только одна горутина имеет доступ к данным. Концепция описана в документе Effective Go (обязательное чтение для каждого Go-разработчика):

Не передавайте данные через разделяемую память; вместо этого передавайте память через коммуникацию.

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

<code>type Resource struct {
  url        string
  polling    bool
  lastPolled int64
}
type Resources struct {
  data []*Resource
  lock *sync.Mutex
}
</code>

И функция Poller (множество таких функций будет работать в отдельных потоках) могла бы выглядеть примерно так:

<code>func Poller(res *Resources) {
  for {
    // получить ресурс, который был опрашиваем последним
    // и отметить его как находящийся в процессе опроса
    res.lock.Lock()
    var r *Resource
    for _, v := range res.data {
      if v.polling {
        continue
      }
      if r == nil || v.lastPolled < r.lastPolled {
        r = v
      }
    }
    if r != nil {
      r.polling = true
    }
    res.lock.Unlock()
    if r == nil {
      continue
    }
    // опросить URL
    // обновить данные ресурса о времени последнего опроса
    res.lock.Lock()
    r.polling = false
    r.lastPolled = time.Nanoseconds()
    res.lock.Unlock()
  }
}
</code>

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

Рассмотрим ту же функциональность, реализованную с использованием идиоматического подхода в Go. В этом примере Poller — это функция, которая получает ресурсы для опроса из входного канала, и отправляет их в выходной канал после завершения.

<code>type Resource string
func Poller(in, out chan *Resource) {
  for r := range in {
    // опросить URL
    // отправить обработанный ресурс в out
    out <- r
  }
}
</code>

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

В приведённых выше фрагментах кода много опущено. Для подробного обзора полной, идиоматической программы на Go, использующей эти идеи, см. Codewalk Share Memory By Communicating.

Следующая статья: Defer, Panic и Recover
Предыдущая статья: Синтаксис объявления в Go
Индекс блога

GoRu.dev Golang на русском

На сайте представлена адаптированная под русский язык документация языка программирования Golang