Предлагаю вам перевод статьи Gary Willoughby «Interesting ways of using Go channels».

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

gopher

Интересные способы использования Go каналов


Я написал этот пост, чтобы задокументировать доклад про Go каналы Джона Грэм-Камминга на конференции GopherCon 2014. Доклад назывался «Краткое руководство по каналам» и он доступен для просмотра на youtube.com.

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

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

Сигналы


Ожидание события


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

package main

import (
	"fmt"
	"time"
)

func main() {

	c := make(chan bool)

	go func() {
		// ... выполняем что-нибудь
		time.Sleep(time.Second * 5)
		close(c)
	}()

	// Останавливает выполнение до получения сообщения из канала или его закрытия.
	<-c

	fmt.Println("Done")
}

Координирование нескольких горутин


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

package main

func worker(start chan bool) {
	<-start
	// ... выполняем что-нибудь
}

func main() {
	start := make(chan bool)

	for i := 0; i < 100; i++ {
		go worker(start)
	}

	close(start)
	// ... все worker's запустятся сейчас
}

Скоординированное прекращение worker’ов


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

package main

func worker(die chan bool) {
	for {
		select {
		// ... выполняем что-нибудь в других case
		case <-die:
			return
		}
	}
}

func main() {
	die := make(chan bool)

	for i := 0; i < 100; i++ {
		go worker(die)
	}

	// Остановить всех worker'ов.
	close(die)
}

Проверка прекращения работы worker’ов


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

package main

func worker(die chan bool) {
	for {
		select {
		// ... выполняем что-нибудь в других case
		case <-die:
			// ... выполняем необходимые действия перед завершением.
			die <- true
			return
		}
	}
}

func main() {
	die := make(chan bool)
	go worker(die)
	die <- true

	// Ждем, пока все горутины закончат выполняться
	<-die
}

Инкапсулируем состояние


Уникальный ID сервиса


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

package main

import "fmt"

func main() {
	id := make(chan string)

	go func() {
		var counter int64 = 1
		for {
			id <- fmt.Sprintf("%x", counter)
			counter += 1
		}
	}()

	fmt.Printf("%s\n", <-id) // will be 1
	fmt.Printf("%s\n", <-id) // will be 2
}

Повторное использование памяти


В этом примере, горутина запускается для повторного использования буферов памяти. Канал give получает старые буферы памяти и сохраняет их в список. В это время канал get распределяет эти буферы для использования. Если нет доступных буферов в списке, создается один новый.

От переводичка
Проще говоря, мы активно повторно используем память, чтобы лишний раз ее не выделять (как мы знаем, ОС может выделять память очень долго). Используется список, в котором всегда есть как минимум 1 буфер. А уже использованные буфера отправляем обратно в этот же список.

package main

import "container/list"

func main() {

	give := make(chan []byte)
	get := make(chan []byte)

	go func() {
		q := new(list.List)

		for {
			if q.Len() == 0 {
				q.PushFront(make([]byte, 100))
			}

			e := q.Front()

			select {
			case s := <-give:
				q.PushFront(s)

			case get <- e.Value.([]byte):
				q.Remove(e)
			}
		}
	}()

	// Получаем новый буфер
	buffer := <-get

	// Возвращаем буфер
	give <- buffer

	// Получаем буфер снова
	buffer = <-get
}

Ограниченное повторное использование памяти


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

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

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

package main

func get(store chan []byte) []byte {
	select {
	case b := <-store:
		return b
	default:
		return make([]byte, 100)
	}
}

func give(store chan []byte, b []byte) {
	select {
	case store <- b:
	default:
		return
	}
}

func main() {

	// Создаем хранилище буферов.
	store := make(chan []byte, 5)

	// Получаем новый буфер из хранилища.
	buffer := get(store)

	// Возвращаем его обратно в хранилище.
	give(store, buffer)

	// Получаем буфер еще раз из хранилища.
	buffer = get(store)
}

Nil каналы


Отключение получения сообщений в операторе case


В этом примере горутина запускается и используется select для получения сообщений из двух каналов. Если канал закрывается, он устанавливается в nil. Поскольку nil каналы всегда блокируют выполнение, данный case больше не выполняется. Если оба канала будут установлены в nil, мы выйдем из горутины, поскольку она больше не может ничего получать.

От переводичка
В примере второе выводимое значение – false, так как после закрытия c1 мы получили x = false и ok = false. Если бы мы не присвоили каналу c1 значение nil, то в нашем бесконечном цикле мы бы продолжали бесконечно получать x = false, ok = false.

Важная мысль. Если вы закрыли канал, то в его case всегда будет приходить значение по умолчанию. Поэтому следует присваивать nil закрытому каналу. А после этого не стоит забывать проверить все каналы на nil, иначе можно навсегда заблокировать вашу горутину.

package main

import "fmt"

func main() {
	c1 := make(chan bool)
	c2 := make(chan bool)

	go func() {
		for {
			select {
			case x, ok := <-c1:
				if !ok {
					c1 = nil
				}
				fmt.Println(x)

			case x, ok := <-c2:
				if !ok {
					c2 = nil
				}
				fmt.Println(x)
			}

			if c1 == nil && c2 == nil {
				return
			}
		}
	}()

	c1 <- true

	// Отключение первого case из select'a выше.
	close(c1)

	c2 <- true
}

Отключение отправки сообщений в операторе case


В этом примере горутина запускается и используется для генерации рандомных чисел и отправки их в канал c. Когда приходит сообщение в канал d, канал c устанавливается в nil, отключая соответствующий оператор case. Отключенная горутина уже больше никогда не генерирует рандомных чисел.

От переводичка
Вот тут бы не помешала проверка на nil, иначе наша горутина не заканчивает свое выполнение. Она заблокировалась на select, так как первый case уже никогда не выполнится, а второй потерял смысл.

Тут есть ошибка в комментарии – c в предпоследней строке не равен nil, nil установили только для локальной переменной src. А deadlock происходит потому, что наш канал c больше никто не записывает.

package main

import (
	"fmt"
	"math/rand"
)

func main() {
	c := make(chan int)
	d := make(chan bool)

	go func(src chan int) {
		for {
			select {
			case src <- rand.Intn(100):

			case <-d:
				src = nil
			}
		}
	}(c)

	// Печатаем несколько случайных чисел.
	fmt.Printf("%d\n", <-c)
	fmt.Printf("%d\n", <-c)

	// Отключаем генерацию рандомных чисел.
	d <- true

	// Здесь прекращается выполнение, потому что канал c теперь nil.
	fmt.Printf("%d\n", <-c)
}

Таймеры


Таймаут


В этом примере горутина запускается, чтобы сделать некоторую работу. Канал timeout создан, чтобы обеспечить исполнение case, если select выполняется слишком долго. В данном случае, горутина завершается после 30 секунд ожидания. Таймаут пересоздается каждую итерацию select’a, чтобы быть уверенным, что он успешно выполнен. В каждой следующей итерации таймаут сбрасывается.

От переводичка
Тут небольшая опечатка — в коде 5 секунд, а в статье 30.

package main

import "time"

func worker() {
	for {
		timeout := time.After(5 * time.Second)

		select {
		// ... выполняем что-нибудь

		case <-timeout:
			// Закрываем эту горутину после указанного таймаута.
			return
		}
	}
}

func main() {
	go worker()
}

Heartbeat


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

package main

import "time"

func worker() {
	heartbeat := time.Tick(30 * time.Second)
	for {

		select {
		// ... выполняем что-нибудь

		case <-heartbeat:
			// ... выполняем что-нибудь по таймеру
		}
	}
}

func main() {
	go worker()
}

Примеры


Сетевой мультиплексор


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

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

package main

import "net"

func worker(messages chan string) {
	for {
		var msg string // ... генерируем сообщение
		messages <- msg
	}
}

func main() {

	messages := make(chan string)
	conn, _ := net.Dial("tcp", "example.com")

	for i := 0; i < 100; i++ {
		go worker(messages)
	}

	for {
		msg := <-messages
		conn.Write([]byte(msg))
	}
}

Первый ответ


В этом примере каждый url из массива передается в отдельную горутину. Каждая горутина выполняется ассинхронно и запрашивает переданный ей url. Каждый ответ на запрос передается в канал first, который, конечно, гарантирует, что первый полученный ответ первым попадет в канал. Затем мы можем прочитать этот ответ из канала и соответственно обработать.

От переводичка
Пример снова не рабочий (ругается на неиспользуемую переменную r). Вот тут похожий пример — youtube.com. Рекомендую к просмотру это видео целиком.

package main

import "net/http"

type response struct {
	resp *http.Response
	url  string
}

func get(url string, r chan response) {
	if resp, err := http.Get(url); err == nil {
		r <- response{resp, url}
	}
}

func main() {
	first := make(chan response)

	for _, url := range []string{"http://code.jquery.com/jquery-1.9.1.min.js",
		"http://cdnjs.cloudflare.com/ajax/libs/jquery/1.9.1/jquery.min.js",
		"http://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js",
		"http://ajax.aspnetcdn.com/ajax/jQuery/jquery-1.9.1.min.js"} {
		go get(url, first)
	}

	r := <-first
	// ... выполняем что-нибудь
}

Передача канала ответа


В этом примере канал w создан для передачи задач в горутину. Горутина получает задачу и делает запрос на url, содержащийся в ней. Канал resp тоже приходит в горутину как часть задачи. Как только запрос будет выполнен, ответ отправляется обратно через канал resp. Это позволяет этой горутине обрабатывать задачи и посылать результат обратно через разные каналы, настроенные для каждой отдельной задачи.

От переводичка
Если проще, мы для каждой нашей задачи создаем свой канал для ответа, через который горутина нам шлет результаты.

package main

import "net/http"

type work struct {
	url  string
	resp chan *http.Response
}

func getter(w chan work) {
	for {
		do := <-w
		resp, _ := http.Get(do.url)
		do.resp <- resp
	}
}

func main() {

	w := make(chan work)

	go getter(w)

	resp := make(chan *http.Response)
	w <- work{"http://cdnjs.cloudflare.com/jquery/1.9.1/jquery.min.js", resp}
	r := <-resp
	// ... выполняем что-нибудь
}

HTTP балансировщик нагрузки


В этом примере создается балансировшщик нагрузки на основе предыдущих примеров. Он обрабатывает прочитанные с stdin url’ы и для каждого запускает горутину для обработки. Каждый запрос проходит через балансировщик нагрузки, чтобы отфильтровать эти задачи на ограниченое число worker’ов. Эти worker’ы обрабатывают запросы и возвращают результаты в единственный канал answer.

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

package main

import (
	"fmt"
	"net/http"
)

type job struct {
	url  string
	resp chan *http.Response
}

type worker struct {
	jobs  chan *job
	count int
}

func (w *worker) getter(done chan *worker) {
	for {
		j := <-w.jobs
		resp, _ := http.Get(j.url)
		j.resp <- resp
		done <- w
	}
}

func get(jobs chan *job, url string, answer chan string) {
	resp := make(chan *http.Response)
	jobs <- &job{url, resp}
	r := <-resp
	answer <- r.Request.URL.String()
}

func balancer(count int, depth int) chan *job {
	jobs := make(chan *job)
	done := make(chan *worker)
	workers := make([]*worker, count)

	for i := 0; i < count; i++ {
		workers[i] = &worker{make(chan *job, depth), 0}
		go workers[i].getter(done)
	}

	go func() {
		for {
			var free *worker
			min := depth
			for _, w := range workers {
				if w.count < min {
					free = w
					min = w.count
				}
			}
			var jobsource chan *job
			if free != nil {
				jobsource = jobs
			}
			select {
			case j := <-jobsource:
				free.jobs <- j
				free.count++

			case w := <-done:
				w.count--
			}
		}

	}()

	return jobs
}

func main() {
	jobs := balancer(10, 10)
	answer := make(chan string)
	for {
		var url string
		if _, err := fmt.Scanln(&url); err != nil {
			break
		}
		go get(jobs, url, answer)
	}
	for u := range answer {
		fmt.Printf("%s\n", u)
	}
}

Заключение


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

Комментарии (28)


  1. Stronix
    02.03.2016 13:46
    +1

    Проверка прекращения работы worker’ов

    По-моему здесь присутствует race condition. Нет гарантии, что первой прочитает из канала горутина, а не основной поток. Да и есть https://golang.org/pkg/sync/#WaitGroup


    1. ilyashikhaleev
      02.03.2016 13:52

      Согласен, ответ от горутины лучше слать в другой канал.
      И сам лично тоже использую WaitGroup.
      Пример из оригинальной статьи, править не стал.


    1. creker
      02.03.2016 13:55

      Операция отправки в канал заблокируется, пока горутина не начнет чтение из него. Так что там все ок. Для одной горутины частенько использую такой вариант. В других случаях действительно удобно отдельным каналом слать факт завершения. А для нескольких уже конечно WaitGroup.


      1. Stronix
        02.03.2016 15:06

        Да, вы правы, забыл, что по-умолчанию ёмкость канала 0, а не 1.


  1. zuborg
    02.03.2016 14:33

    Почему-то пропущено использование каналов в качестве семафоров — http://www.golangpatterns.info/concurrency/semaphores


  1. leventov
    02.03.2016 15:05
    +2

    Если в руках есть только молоток (каналы), то все проблемы кажутся гвоздями. Ничего хорошего в этом нет, это не называется "фантастической поддержкой конкурентности". Больше (кроме locks еще) ничего нет не потому что не надо, а потому что дизайнеры дают очень высокий приоритет маленькому кол-ву концепций (и, видимо, простоте рантайма). То есть решают свою проблему, чтобы быстрее добавлять новые фичи и улучшать другие моменты, а не проблему пользователей. Они делают это в интересах пользователей, конечно, но в ином разрезе. Поэтому не всякий аспект языка Го является "великолепным" и "фантастическим" (а другие).


    1. creker
      02.03.2016 15:16
      +1

      Рантайм там как раз очень непростой. Простые конструкции языка, а за ними сложный и мощный рантайм, чтобы это действительно было "великолепным" и "фантастическим"


      1. leventov
        02.03.2016 16:14
        +4

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

        С одной стороны, Го хвалят за простоту, с другой, подход с впиливанием фич прямо в рантайм (мапы, слайсы, каналы, и т. д.) вместо предоставления примитивов, на которых это все можно построить библиотечным кодом — может очень сильно аукнуться через 5-10 лет.

        В этой связи интересно сравнение с Rust, который последние года 2 перед релизом целенаправленно "сушил" рантайм и достиг в этом успехов


        1. creker
          02.03.2016 18:35
          -1

          В райтайм впилены самые необходимые вещи по мнению авторов. Зачем? Потому что только так их можно сделать максимально удобными и лаконичными. Каналы и горутины так хороши именно потому, что они часть спецификации языка и его синтаксиса. Если исключить их оттуда, то мы получаем опять С++, где много всего, но им невозможно и неудобно пользоваться. Не дай бог в новый стандарт асинхронные функции включат не как синтаксис, а как функции STL. Этим опять будет невозможно пользоваться. C#, опять же, показывает, что async/await так прекрасно работают, потому что включены в сам язык, компилятор, рантайм.

          Что до простоты. То это частое заблуждение, я смотрю, которое я для себя развеял, когда услышал из первых уст. Команда Go никогда не стремилась к простоте везде. Им нужен был простой язык с простой спецификацией и концепциями, но никто не говорил, что внутри там все просто. Напротив, они специально за простыми конструкциями прятали очень сложные вещи вроде каналов и горутин со своим планировщиком задач и кооперативной многозадачностью с маленькими проблесками вытесняющей. Тот же сборщик мусора — он вообще разработчику не виден, команда даже намеренно не дает никаких средств его настройки, но внутри он совсем не прост, особенно в последних релизах.


          1. leventov
            02.03.2016 19:23
            +1

            Если исключить их оттуда, то мы получаем опять С++, где много всего, но им невозможно и неудобно пользоваться. Не дай бог в новый стандарт асинхронные функции включат не как синтаксис, а как функции STL. Этим опять будет невозможно пользоваться. C#, опять же, показывает, что async/await так прекрасно работают, потому что включены в сам язык, компилятор, рантайм.

            Посмотрим на примере Раста, можно этим будет пользоваться, или нет.

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

            На мой взгляд, ничего особо сложного, по сравнению со сложностью рантайма Java, или, например, сложностью компилятора C++, в Го нет. Потому-то они и выкатили очень и очень достойную реализацию всего за пару лет, при том, что над ней работало поначалу меньше 10 человек, а сейчас — не сильно больше.


            1. creker
              02.03.2016 19:37

              На мой взгляд, ничего особо сложного, по сравнению со сложностью рантайма Java, или, например, сложностью компилятора C++, в Го нет

              Сначала вы жалуетесь на сложность рантайма, теперь оказывается ничего особо сложного там нет. Сложный там рантайм и таковым стал к нынешним версиям. Зеленые потоки, стек динамического размера, конкурентный сборщик мусора — это совсем не простые вещи. Что до java, то ее можно назвать сложной разве что при условии JIT компиляции. Уж интерпретируемая виртуальная машина вообще задача для студента. А сборщик мусора и там и там есть довольно непростой.

              Потому-то они и выкатили очень и очень достойную реализацию всего за пару лет, при том, что над ней работало поначалу меньше 10 человек, а сейчас — не сильно больше.

              Как раз достойной реализация тогда не была. Сборщик мусора был тормозной и убогий — stop the world вариант. Планировщик был так же тупой и медленный. С тех времен рантайм сильно изменился. Ну и как бы опыт у людей уже был. Это не первый язык авторов с подобными фичами.


              1. leventov
                03.03.2016 12:49
                +1

                Сначала вы жалуетесь на сложность рантайма, теперь оказывается ничего особо сложного там нет. Сложный там рантайм и таковым стал к нынешним версиям. Зеленые потоки, стек динамического размера, конкурентный сборщик мусора — это совсем не простые вещи. Что до java, то ее можно назвать сложной разве что при условии JIT компиляции. Уж интерпретируемая виртуальная машина вообще задача для студента. А сборщик мусора и там и там есть довольно непростой.

                Я нигде не жаловался на сложность рантайма. Я предположил, что авторы ограничились молотком-каналами (и "так и быть", подумали они, простейшими локами), чтобы рантайм был прост, и они могли в 10-20 человек за несколько лет довести его до приличного уровня. Java пилят сотни людей десятки лет. Мы о Hotspot JVM конечно, а не студенческих задачах.

                Современные Java-сборщики сложнее, потому что они перемещают объекты, и тоже конкурентные.


                1. creker
                  03.03.2016 13:20
                  -2

                  Мне все так же непонятно, о чем вы вообще пишите. То сложный рантайм, то не сложный, яву зачем-то приплели и никаким образом не влияющий ни на что факт, что ее пилят черти сколько лет (при том, что в яве за это время прогресса, по сути, никакого и нет).

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

                  Go сборщик мусора не перемещает вроде бы объекты в куче, но и не дает гарантий, что не будет этого делать в будущем. А вот стек может запросто переместиться при смене его размера, а с ним и объекты в нем.


        1. mirrr
          03.03.2016 09:37

          Я не совсем понял мысль про "впиливание" локов и семафоров в рантайм, можете пояснить? И каким образом это влияет на сложность?


          1. leventov
            03.03.2016 12:52
            +2

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


            1. mirrr
              03.03.2016 13:02

              Да, без примера не разобраться. А что мешает использовать семафоры?


              1. mirrr
                03.03.2016 13:16

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


            1. creker
              03.03.2016 13:08
              +1

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

              В этом и есть высшее благо. Зеленые потоки зашиты в сам язык. Поэтому им так легко и удобно пользоваться. Любое иное решение будет хуже в любом случае. Может еще давайте из erlang уберем из рантайма зеленые потоки, потому что они слишком много прячут? Это не С++.

              У вас есть все простейшие примитивы, чтобы реализовать более сложные конструкции. Кооперативная многозадачность в последних версиях достигается так же засчет вставки вызовов планировщика перед каждым вызовом функции, а кроме каналов и локов там еще участвуют IO операции. Планировщик можно и самому вручную вызвать. Мне все так же непонятно, что вам не нравится. Вам стоит изучить язык по-лучше, раз так интересно.


              1. leventov
                04.03.2016 16:25
                +1

                У вас есть все простейшие примитивы, чтобы реализовать более сложные конструкции.

                Этим "простейшим примитивом" являются каналы, что как раз иллюстрирует эта статья — все реализуется через каналы. Это работает, но 1) не очень эффективно 2) не очень интуитивно

                Кооперативная многозадачность в последних версиях достигается так же засчет вставки вызовов планировщика перед каждым вызовом функции, а кроме каналов и локов там еще участвуют IO операции. Планировщик можно и самому вручную вызвать.
                Да, я в курсе, про все это рантайм тоже знает. Идея в том, что переключить выполнение с одной горутины на другую, либо сделать «пинг» какой-нибудь спящей горутины, никак нельзя, кроме как через все те же пресловутые каналы.

                Мне все так же непонятно, что вам не нравится.
                Мне не нравится чрезмерные восторги по поводу того, что через каналы можно сконструировать разные синхронизированные конструкции. В том же C++ все это можно реализовать более эффективно, и выглядеть будет хуже разве что из-за громоздкости синтаксиса, а по сути будет проще.


                1. mirrr
                  07.03.2016 18:47

                  что как раз иллюстрирует эта статья — все реализуется через каналы

                  Эта статья называется "способы использования Go каналов". Вы ожидали увидеть в ней способы реализации не через каналы?


                1. creker
                  07.03.2016 19:19

                  Этим «простейшим примитивом» являются каналы, что как раз иллюстрирует эта статья — все реализуется через каналы. Это работает, но 1) не очень эффективно 2) не очень интуитивно

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

                  Да, я в курсе, про все это рантайм тоже знает. Идея в том, что переключить выполнение с одной горутины на другую, либо сделать «пинг» какой-нибудь спящей горутины, никак нельзя, кроме как через все те же пресловутые каналы.

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

                  У меня такой вопрос, вам ОС тоже покоя не дает? Не дает вам возможности пинать какие-то потоки, переключать выполнение между ними. Все, гад, планировщик делает в сговоре с аппаратными прерываниями, да еще с пространстве ядра, куда процессор, негодяй такой, не пускает просто так.

                  Мне не нравится чрезмерные восторги по поводу того, что через каналы можно сконструировать разные синхронизированные конструкции. В том же C++ все это можно реализовать более эффективно, и выглядеть будет хуже разве что из-за громоздкости синтаксиса, а по сути будет проще.

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


    1. zuborg
      02.03.2016 15:37
      +1

      Тем не менее, концепция каналов является весьма мощной (о чем, собственно и пост). Для низкоуровневых вещей доступны локи и атомарные операции, а концептуальная лаконичность языка это скорее плюс, чем минус.


      1. leventov
        02.03.2016 16:16
        -1

        Может быть каналы даже тьюринг-полны. В машине Тьюринга очень мало концепций, но это не плюс для программирования на ней. Тут нужен баланс, который Го, мне кажется, чуть-чуть проскочил.


  1. Virviil
    02.03.2016 16:52

    Меня очень заинтересовал пример с HeartBeat. А есть ли возможность запускать не одну, а большое число рутин одновременно по такому вот таймеру? Или я чего-то не правильно понял...


    1. zuborg
      02.03.2016 18:38

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


    1. creker
      02.03.2016 18:50

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


      1. Virviil
        03.03.2016 09:25

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


        1. zuborg
          03.03.2016 09:58

          Для синхронных тиков? Тогда придется писать размножитель сигнала, т.к. исходный тик из канала сможет забрать только одна горутина, даже если этот канал будут слушать много горутин. Размножителю придется рассылать сигнал в индивидуальный канал каждой горутине.