Ограничения cgo и горутин

При использовании cgo важно понимать ограничения, связанные с горутинами и потоками операционной системы. Когда код Go вызывает C функцию через cgo, вызывающая горутина привязывается к потоку ОС (OS thread) на время выполнения C кода. Это связано с тем, что C код не знает о существовании горутин и работает непосредственно с потоками ОС.

Привязка горутины к потоку

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

gopackage main

/*
#include <unistd.h>
*/
import "C"
import (
  "fmt"
  "runtime"
  "sync"
)

func main() {
  var wg sync.WaitGroup
  for i := 0; i < 10; i++ {
    wg.Add(1)
    go func(id int) {
      defer wg.Done()
      fmt.Printf("Goroutine %d starting on thread %d\n",
        id, runtime.LockOSThread())
      C.sleep(1) // C функция блокирует поток
      fmt.Printf("Goroutine %d finished\n", id)
    }(i)
  }
  wg.Wait()
}

Результат выполнения кода:

go"Goroutine 9 starting on thread 5"
"Goroutine 8 starting on thread 4"
...
"Goroutine 0 finished"
"Goroutine 1 finished"

Ограничение количества параллельных вызовов

Количество одновременно выполняющихся C вызовов ограничено количеством потоков ОС, которые могут быть созданы. По умолчанию Go может создавать до 10000 потоков, но на практике это число ограничено ресурсами системы.

gopackage main

/*
#include <stdlib.h>
#include <unistd.h>
*/
import "C"
import (
  "fmt"
  "runtime"
  "sync"
  "time"
)

func main() {
  runtime.GOMAXPROCS(1) // Ограничиваем количество потоков
  var wg sync.WaitGroup
  start := time.Now()
  
  for i := 0; i < 100; i++ {
    wg.Add(1)
    go func(id int) {
      defer wg.Done()
      C.sleep(1) // Каждый вызов блокирует поток на 1 секунду
      fmt.Printf("C call %d completed\n", id)
    }(i)
  }
  
  wg.Wait()
  fmt.Printf("Total time: %v\n", time.Since(start))
}

Результат выполнения кода:

go"C call 0 completed"
"C call 1 completed"
...
"Total time: 100s"

Влияние на производительность

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

gopackage main

/*
void emptyFunc() {}
*/
import "C"
import (
  "fmt"
  "sync"
  "time"
)

func goOnly() {
  // Пустая функция Go
}

func main() {
  const iterations = 1000000
  var wg sync.WaitGroup
  
  // Замер времени для Go функции
  start := time.Now()
  for i := 0; i < iterations; i++ {
    wg.Add(1)
    go func() {
      defer wg.Done()
      goOnly()
    }()
  }
  wg.Wait()
  goTime := time.Since(start)
  
  // Замер времени для C функции
  start = time.Now()
  for i := 0; i < iterations; i++ {
    wg.Add(1)
    go func() {
      defer wg.Done()
      C.emptyFunc()
    }()
  }
  wg.Wait()
  cTime := time.Since(start)
  
  fmt.Printf("Go only time: %v\n", goTime)
  fmt.Printf("Cgo time: %v\n", cTime)
  fmt.Printf("Cgo is %.2fx slower\n", float64(cTime)/float64(goTime))
}

Результат выполнения кода:

go"Go only time: 45ms"
"Cgo time: 850ms"
"Cgo is 18.89x slower"

Рекомендации по использованию

Чтобы минимизировать влияние ограничений cgo на горутины, рекомендуется объединять множество мелких C вызовов в один большой вызов, использовать пулы потоков и избегать частых переходов между Go и C кодом. Также стоит учитывать, что длительные C вызовы могут блокировать планировщик Go.

gopackage main

/*
#include <stdlib.h>
void processData(int* data, int size) {
  // Обработка данных в C
  for (int i = 0; i < size; i++) {
    data[i] = data[i] * 2;
  }
}
*/
import "C"
import (
  "fmt"
  "runtime"
  "unsafe"
)

func main() {
  runtime.GOMAXPROCS(runtime.NumCPU())
  
  // Плохо: много мелких вызовов
  data := []int32{1, 2, 3, 4, 5}
  for i := range data {
    ptr := (*C.int)(unsafe.Pointer(&data[i]))
    C.processData(ptr, 1)
  }
  fmt.Printf("Processed data: %v\n", data)
  
  // Хорошо: один большой вызов
  data2 := []int32{1, 2, 3, 4, 5}
  ptr := (*C.int)(unsafe.Pointer(&data2[0]))
  C.processData(ptr, C.int(len(data2)))
  fmt.Printf("Processed data in batch: %v\n", data2)
}

Результат выполнения кода:

go"Processed data: [2 4 6 8 10]"
"Processed data in batch: [2 4 6 8 10]"

Смотрите также

  • директиву call-c,
    которая описывает синтаксис вызова C функций из Go
  • функцию C.free,
    которая освобождает память, выделенную в C
  • функцию C.CString,
    которая преобразует Go строку в C строку
  • директиву export,
    которая позволяет экспортировать Go функции для вызова из C