Перейти к основному содержимому

Многопоточность на С

Разработчику Архитектору

Многопоточность на С

Один процесс — одна запущенная программа. Внутри него можно создать несколько потоков (threads): у каждого свой стек и счётчик команд, но общие глобальные переменные, куча и код.

Аналогия: один офис (процесс), несколько сотрудников (потоки) с личными блокнотами (стек), но общая доска объявлений (глобальные данные). Если двое одновременно пишут на доске без правил — получится каша (гонка данных, data race).

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

Обзор стандартов C11 — в Стандартах языка С; здесь — практика на POSIX и сравнение с <threads.h>.

Когда потоки уместны

  • обработка сетевых соединений, пока главный цикл отвечает на UI или сигналы;
  • фоновое сохранение на диск;
  • параллельная обработка независимых блоков данных (когда накладные расходы на потоки окупаются);
  • перекрытие ожидания I/O с вычислениями.

Процесс (fork в Unix) получает копию памяти (с оптимизациями ОС) — изоляция сильнее, создание тяжелее. Поток дешевле, зато любая ошибка синхронизации бьёт по всему процессу.

Словарь

ТерминСмысл
Потокнезависимая цепочка выполнения внутри процесса
Мьютексзамок: только один поток в критической секции
Критическая секциякод, трогающий общие данные
Гонкадва потока пишут в одну переменную без синхронизации
Deadlockпотоки вечно ждут друг друга
joinдождаться завершения потока

POSIX threads (pthreads)

На Linux, macOS и большинстве Unix потоки создают через pthread:

#include <pthread.h>

void *worker(void *arg)
{
int id = *(int *)arg;
/* ... */
return NULL;
}

int id = 2;
pthread_t tid;
pthread_create(&tid, NULL, worker, &id);
pthread_join(tid, NULL);

pthread_join ждёт завершения потока. Аргумент arg должен оставаться валидным до старта потока (не передавать адрес локальной переменной main, если main уже вышел из области — лучше malloc или структура с длительным временем жизни).

Компиляция: флаг -pthread (GCC/Clang) подключает библиотеку и макросы.


Стандарт C11: <threads.h>

Стандарт предлагает переносимый API: thrd_create, thrd_join, mtx_t, cnd_t. Поддержка на всех платформах неполная (исторически Windows опиралась на свои потоки). На современных GCC/Clang в Linux <threads.h> часто доступен.

Выбор для нового кроссплатформенного кода:

APIГде
pthreadsсерверы и Unix, зрелая экосистема
C11 threadsминимальная переносимость там, где есть
обёртка проектаодин слой над платформой

Мьютекс

Мьютекс (взаимное исключение) защищает критическую секцию — код, который трогает общую структуру:

pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
int shared_counter = 0;

void increment(void)
{
pthread_mutex_lock(&lock);
shared_counter++;
pthread_mutex_unlock(&lock);
}

Правила:

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

В C11: mtx_lock, mtx_unlock из <threads.h>.


Условные переменные

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

/* псевдокод паттерна */
pthread_mutex_lock(&q->lock);
while (q->empty)
pthread_cond_wait(&q->not_empty, &q->lock);
item = dequeue(q);
pthread_mutex_unlock(&q->lock);

wait атомарно отпускает мьютекс и засыпает; при пробуждении снова захватывает мьютекс. Всегда ждать в цикле while, а не if — из-за ложных пробуждений.


Атомарные операции C11

Для счётчиков и флагов без полного мьютекса — <stdatomic.h>:

#include <stdatomic.h>

atomic_int requests = 0;

void on_request(void)
{
atomic_fetch_add(&requests, 1);
}

Модели памяти (memory_order_acquire, release, seq_cst) задают, какие перестановки инструкций допустимы между потоками. Для учебного кода часто достаточно последовательной согласованности по умолчанию; для lock-free структур нужна отдельная проработка.


Типичные ошибки

ОшибкаПоследствие
гонка на shared++ без атомика/мьютекса«случайные» значения
data race на указателеUB по стандарту С
возврат адреса стека из потокависячий указатель
sqlite3 / FILE * из разных потоков без правил APIповреждение данных
забытый joinутечка ресурсов потока

Инструменты: ThreadSanitizer (-fsanitize=thread), отладочные сборки с проверкой mutex.


Потоки и идиомы ошибок

Глобальные переменные с состоянием усложняют тесты. Предпочтительнее передавать контекст в void *arg worker-функции или использовать очередь задач с одним потоком-потребителем — проще, чем широкие блокировки.

Для встраиваемой БД: одно соединение на поток или явная сериализация доступа.


Когда не усложнять

Один поток + неблокирующий I/O (select, poll, epoll) часто достаточен для утилит и сетевых демонов средней нагрузки. Потоки оправданы, когда блокирующие вызовы неизбежны или есть готовый пул воркеров.

См. также: Системное программирование, Справочник.


См. также

Другие статьи этого же раздела в боковом меню (как на странице «О разделе»).