The Go Blog

Делитесь памятью, общаясь

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

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