Прогулка седьмая. Венеция

Сегодня я выступлю в роли венецианского GOндольера и покажу вам всю красоту каналов.

Что такое канал?

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

С помощью семофоров/мьютексов мы реализовываем IPC (межпроцессное взаимодействие) так: создаем расшаренный кусок памяти (доступный нескольким потокам), и защищаем доступ к нему семафором. А что же было раньше? Раньше были Unix pipes. В пайпах же мы, наоборот, расшаривали саму пайпу (средство коммуникации), и передавали по ней данные между процессами. Запутанно. В оригинале этот принцип звучит понятнее:

Do not communicate by sharing memory; instead, share memory by communicating.

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

Для работы с каналами используют функцию make(type, buffer size) или просто make(type) для канала без буфера.

Чтение запись в канал осуществляется с помощью оператора «<-«. Вот так:

ch := make(chan string, 100)
strIn := "hello"
ch <- strIn
strOut := <-ch
fmt.Printf("In: %s\nOut:%s\n", strIn, strOut)

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

Следует отметить, что операции с каналом блокируют поток. Т.е. если канал заполнен (в него записали ровно столько данных, сколько вмещает буфер), то поток «зависнет» до тех пор, пока часть данных не будет вычитана. А это означает, что если у вас всего один поток, то приложение аварийно завершит свою работу (да, это дедлок). Фактически, канал можно рассматривать как очередь объектов с буфером определенного размера.

Гопроцедуры

Ой как некрасиво звучит… На их языке этот термин называют goroutines, не без намека на co-routines — легковесное подобие потоков.В языке go любая функция может стать goroutine — для этого перед её вызовом надо написать слово «go»:

func demo(s string) {
    for i:=0; i<10; i++ {
        time.Sleep(1000 * 1000 * 1000) // подождать 1 сек.
        fmt.Printf("%s\n", s)
    }
}

func main() {
    go demo("foo") // Вызываем как goroutine
    demo("bar") // Вызываем в контексте основного потока
}

Что же здесь произошло? Одну и ту же функцию demo() мы вызвали как goroutine и как обучную функцию. go demo(«foo») создает отдельный поток/процесс и выполняет demo(«foo») в его контексте. При этом основной поток программы не блокируется. Поэтому, в основном потоке запускается функция demo(«bar»). Результат: на экране чередуются 10 надписей foo и bar.

Так в go реализуют распределенность. А вот синхронизировать все эти goroutines можно и нужно с помощью каналов:

func reader(ch chan int) {
    for {
        var i = <-ch
        fmt.Printf("reader: %d\n", i)
    }
}

func writer(ch chan int) {
    for i := 0; i<10; i++ {
        ch <- i
        fmt.Printf("writer: %d\n", i)
        time.Sleep(1000 * 1000 * 1000)
    }
}

func main() {
    var ch = make(chan int, 5)

    go writer(ch)
    go reader(ch)

    /* Бесконечный цикл. Sleep(0) позволяет переключать потоки */
    for { time.Sleep(0) } 
}

Один поток пишет числа, второй их читает и выводит на экран.

Нельзя ждать в два раза быстрее

Если вы заметили, то в последнем примере наша программа жадно забрала все ресурсы процессора. Разумеется, виновник — пустой бесконечный цикл в основном потоке.

Обычно разработчики используют маленькую хитрость — создаеют еще один канал, произвольного типа (как правило — int). Как только в канале появляются данные — приложение завершается:

 
var quit = make(chan int)
...
quit <- 1 /* Где-то в программе - посылаем сигнал завершения */
...
func main() {
    go ...
    go ...
    <-quit /* Ожидаем завершения потоков */
}

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

Применение: IRC

Ребята из suckless.org сделали минималистичный IRC клиент — sic. Особенность его в том, что исходный код на C занимает 250 строк кода (без комментариев и пустых строк). Посмотрим, насколько этот объем сокращается, если писать не на C, а на go:

xxx

Размер уменьшился практически в два раза. Хотелось бы отметить основные моменты (субъективное мнение, конечно):

  • В go удобнее парсить опции по сранвнению с getopt()
  • В go легче работать с сокетами
  • Каналы действительно удобно использовать для передачи потока данных из одной функции в другую
  • Не очень удобно парсить строки. Возможно, воспринимать строки как слайсы было бы проще, чем использовать Split()
  • В if-ах на одно действие фигурные скобки мешают.

Последнюю версию ergo (так называется этот IRC клиент) можно найти на http://bitbucket.org/zserge/ergo

На сегодня все. ВсеGO доброGO!

Реклама

Добавить комментарий

Заполните поля или щелкните по значку, чтобы оставить свой комментарий:

Логотип WordPress.com

Для комментария используется ваша учётная запись WordPress.com. Выход / Изменить )

Фотография Twitter

Для комментария используется ваша учётная запись Twitter. Выход / Изменить )

Фотография Facebook

Для комментария используется ваша учётная запись Facebook. Выход / Изменить )

Google+ photo

Для комментария используется ваша учётная запись Google+. Выход / Изменить )

Connecting to %s