Go: многопоточность

Заинтересовал меня топик о многопоточности в Go: habrahabr.ru/post/195574.
Внимательно перечитал автора и комментарии сообщества и решил, что тема все же раскрыта не полностью.
В дальнейшем, дабы не было непонимания, попрошу принять, что здесь и далее термин «поток» используется исключительно в значении «thread», а не в значении «stream». Спасибо.

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

Текст программы
/* channel_test01.go 
 * Tests how go-routines interact with channels
 * Call: channel_test01 --help 
 * Pls, do not use name "channel_test", because this name always is used by go-pkg-system
 */

package main

import (
	"fmt"
	"time"
	"flag"
	"os"
	"runtime"
)

// flag support for program
var MAXPROCS int
var LOAD_CYCLES int //internal burden cycle

var Usage = func() {
    fmt.Fprintf(os.Stderr, "Usage channel_test01 [-maxprocs=NN] [-cycles=NN] \n", os.Args[0])
}

func init() {
    flag.IntVar(&MAXPROCS, "maxprocs", 1, "maxprocs for testing. From 1 to 256 ");
    flag.IntVar(&LOAD_CYCLES, "cycles", 1000, "burden internal cycle for testing. From 1 to 1000000 and more ");
}

func main() {

    flag.Parse();//get MAXPROCS and LOAD_CYCLES from flags

    // runtime.GOMAXPROCS() returns previous max_procs
    max_procs := runtime.GOMAXPROCS(MAXPROCS) 
    // second call to get real state
    max_procs = runtime.GOMAXPROCS(MAXPROCS)
    fmt.Println("MaxProcs = ", max_procs)

    
    ch1 := make(chan int)
    ch2 := make(chan float64)

	go chan_filler(ch1, ch2)
	go chan_extractor(ch1, ch2)

    fmt.Println("Total:", <-ch2, <-ch2)

}

func chan_filler(ch1 chan int, ch2 chan float64) {

    const CHANNEL_SIZE = 1000000 

    for i := 0; i < CHANNEL_SIZE; i++ {
        for j := 0; j < LOAD_CYCLES; j++ {
            i++
        }
        //thus we avoid optimizer influence
        i = i - LOAD_CYCLES

        ch1 <- i
    }

    ch1 <- -1
    ch2 <- 0.0
}

func chan_extractor(ch1 chan int, ch2 chan float64) {

    const PORTION_SIZE = 100000
    total := 0.0

    for {
        t1 := time.Now().UnixNano()
        for i := 0; i < PORTION_SIZE; i++ {
            // burden cycle
            for j := 0; j < LOAD_CYCLES; j++ {
                i++
            }
            i = i - LOAD_CYCLES

            m := <-ch1
            if m == -1 {
                ch2 <- total
            }
        }
		
        t2 := time.Now().UnixNano()
        dt := float64(t2 - t1)/1e9 //nanoseconds ==> seconds
        total += dt
        fmt.Println(dt)
    }
}





Результаты

Машина: Intel, 4 ядра по 3ГГц, 4 ГБ RAM, linux-x86-64, kernel-3.11, golang-1.1.2

Максимальное достижимое значение runtime.MAXPROCS на моей машине равно 256. Можно задать и больше, но все равно будет выставлено 256. Значение по умолчанию = 1.

Число потоков, порождаемых программой (ps -C channel_test01 -L)
При -maxprocs=1: 3 потока.
При -maxprocs=2 и более: всегда 4 потока.

Иногда при длительных операциях появлялось еще 2 потока, которые оставались до конца работы процесса. Похоже на сборку мусора.

(Попробовал и на одном из виртуальных серверов, который показывает в /proc/cpuinfo наличие 24 ядер. На нем число потоков также было равно 4. Но часто был и пятый поток — весьма похоже на то, что жадная vds-ка не спешила выделять память, и сборщик мусора вылезал осмотреться).

Зависимость скорости наполнения канала от количества потоков при cycles = 1000

perf-procs graph

Максимальная производительность развивается при MAXPROCS=1

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


perf-load graph

Под нагрузкой многопоточность все же берет свое.

Выводы из графиков

При maxprocs=1 все работает в 1 поток, псевдо-многопоточность обеспечивается внутренним планировщиком голанга, который является весьма эффективным.
При maxprocs=2 и более появляется больше потоков, включается механизм взаимодействия между потоками. Возникают блокировки, и все становится медленнее, на спящих потоках — примерно в 2 раза.
При maxprocs=3..256 реальное число потоков в системе не растет, но часть потоков становится «псевдо-потоками», взаимодействие между которыми начинает попадать под управление внутреннего планировщика голанга. Это дает небольшой прирост скорости с каждым увеличением количества «псевдо-потоков».
С дальнейшим увеличением maxprocs (вплоть до своего максимального значения 256) все больше потоков становятся псевдо-потоками и попадают под управление внутреннего планировщика. Но часть потоков все равно остается независимыми системными потоками. На спящих потоках и быстрых вычислениях это нам обходится примерно в 10% производительности.

Вывод.

Изменение параметра runtime.MAXPROCS может принести как выигрыш, так и потерю в производительности. Это еще раз подтверждает общий принцип, что эффект от распараллеливания зависит от конкретной задачи и конкретного оборудования.
  • +21
  • 10,7k
  • 3
Поделиться публикацией

Похожие публикации

Комментарии 3
    0
    Спасибо за детальный анализ. Для полноты картины было бы ещё интересно посмотреть на вариант, когда вручную запускаются несколько процессов программы с MAXPROCS от одного до нескольких. По идеи с MAXPROCS=1 и количеством процессов равным количеству ядер, производительность должна увеличиваться ровно пропорционально. Интересно посмотреть что будет происходить с увеличением MAXPROCS в этом случае.
      0
      Приятно, что вас заинтересовала моя статья. Поясните, что такое псевдо поток? Это гоурутина или нечто третье?
      При -maxprocs=2 и более: всегда 4 потока.

      То есть фактически в многопоточном Go всегда только два потока?
      Это тема для очередной статьи ))) Как в Go занять все 8 ядер
      Предположу Go создает потоки по необходимости. (для вашего и моего теста больше двух не нужно)
      runtime.GOMAXPROCS — это максимально дозволенное значение, а четкое значение сколько потоков запускать,
        0
        Понятием «псевдо-поток» пытался описать го-роутину, которая в системе отдельного «треда» не имеет, а управляется внутренним runtime-планировщиком голанга в режиме разделения времени.

        Насчет 4 потоков — увы да. На vds'ке все равно было 4 потока, хотя го-роутин было 20, MAXPROCS выставлялся в 20, а ядер по /proc/cpuinfo показывало 24. Тем не менее прирост производительности наблюдался практически линейно (10-кратное увеличение производительности при увеличении количества го-роутин с 20 до 1000), пока хватало оперативной памяти. Мне хватило на 1000 гороутин, параллельно пишущих в БД, причем процесс голанг-утилиты забирал в сумме 40МБ RAM, а вот БД порождала процессы, каждый их которых поедал 10-15МБ RAM.

        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

        Самое читаемое