Конкурентность в Go против Си с pthreads
Перевод статьи "Go Concurrency versus C and pthreads".
Недавно я принимал участие в одном программистком соревновании:
Необходимо написать программу для тестирования реакции пользователя. При запуске эта программа ожидает случайный промежуток времени от 1 до 10 секунд и выводит сообщение "GO!" в консоль.
После этого программа ожидает когда пользователь нажмет enter. В момент нажатия, замеряется промежуток времени от отображения надписи до нажатия кнопки.
Если пользователь умудрился нажать enter до сообщения "GO!", то выводится другое сообщение - "FAIL".
По сути, это упражнение на применение конкуренности(concurrency): пока одна часть логики следит за нажатием кнопки, вторая часть генерирует случайное время ожидание перед выводом сообщения и в конце нужно выполнить сравнение времени нажатия и задержки.
Так как я изучаю Go, то мне показалось, что описанная задача идеально ложится па концепцию go-рутин и каналов.
И действительно, решить эту задачу на Go очень просто.
package main
import (
"bufio"
"fmt"
"math/rand"
"os"
"time"
)
func init() {
rand.Seed(time.Now().UnixNano())
}
func main() {
channel := make(chan time.Time)
go func() {
reader := bufio.NewReader(os.Stdin)
reader.ReadString('\n')
channel <- time.Now()
}()
time.Sleep(time.Second * time.Duration(rand.Intn(11)))
paused := time.Now()
fmt.Println("GO!")
entered := <- channel
if paused.Sub(entered) > 0 {
fmt.Println("FAIL")
} else {
fmt.Printf("%v\n", time.Since(entered))
}
}
И тут я вспомнил, что на развитие Go очень повлиял Си. Или, если быть точнее, Go это современный Си. Я заинтересовался, насколько сложнее сделать аналогичную программу на Си?
В стандартном ANSI Си нет никаких функций для работы с потоками и во втором издании K & R тоже нет никаких упоминаний о потоках. Но есть POSIX потоки(они же нити, они же треды), или pthreads, которые можно использовать как отдельную библиотеку.
В результате у нас получилось больше текста, но концептуально это практически одинаковые программы:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/time.h>
#include <pthread.h>
void *listen(void *timestamp) {
int c;
while((c = getc(stdin)) != EOF) {
if( c == '\n' ) {
gettimeofday((struct timeval *)timestamp, NULL);
break;
}
}
return NULL;
}
int main(void) {
pthread_t listener;
struct timeval entered, paused, diff;
if( pthread_create(&listener, NULL, listen, &entered) ) {
fprintf(stderr, "Error: could not create keyboard listening thread\n");
return 1;
}
srand(time(NULL));
sleep(rand() % 10 + 1);
printf("GO!\n");
gettimeofday(&paused, NULL);
if( pthread_join(listener, NULL) ) {
fprintf(stderr, "Error: could not join keyboard listening thread\n");
return 2;
}
if( timercmp(&paused, &entered, >) )
printf("FAIL\n");
else {
timersub(&entered, &paused, &diff);
printf("%ld\n", (1000LL * diff.tv_sec) + (diff.tv_usec / 1000));
}
return 0;
}
Мы также делаем обработчик, который ожидает нажатия клавиши enter и используем функцию gettimeofday для получения текущего времени и функцию для сравнения дат, которые работают аналогично пакету time.
Создавать новые потоки с помощью pthread достаточно просто, но вместо использования каналов для обмена данными между потоками, приходится использовать передачу структуры по ссылке.
Еще одно большое отличие - при использовании pthread необходимо выполнять "присоединение"(joined) чтобы дождаться завершения работы потока.
Вот это замечательный пример с подробными объяснениями помогли мне вникнуть в суть того, как работает pthread.
И при создании обработчика, и при "присоединении" нам необходимо проверять ошибки. В нашем случае, для этой игрушечной программы я просто делаю выход в случае ошибки. Однако, для более объемных проектов, нужно будет реализовать значительно больше дополнительной логики.
Кроме некоторого различия в реализации логики этих двух примеров, существует еще много нюансов, скрытых под капотом.
Несмотря на то, что потоки, реализованные с помощью pthread, значительно легче чем процессы операционной системы, они все еще не настолько легковесные как go-рутины. Конкурентность в Go реализована на принципах взаимодействующих последовательных процессов(Communicating Sequential Processes (CSP)).
В результате, go-рутины объединены и работают на небольшом количестве системных процессов, вместо использования одного потока для каждой go-рутины.
Конечно, это не совсем честное соревнование, так как Си намного старше Go, а phthread это вообще сторонняя библиотека.
Go вобрал в себя весь опыт полученный с Си и еще много всего полезного из других языков программирования.