Многопоточность на С
Многопоточность на С
Один процесс — одна запущенная программа. Внутри него можно создать несколько потоков (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) часто достаточен для утилит и сетевых демонов средней нагрузки. Потоки оправданы, когда блокирующие вызовы неизбежны или есть готовый пул воркеров.
См. также: Системное программирование, Справочник.
См. также
Другие статьи этого же раздела в боковом меню (как на странице «О разделе»). История языка C - происхождение, ключевые идеи и влияние на развитие операционных систем и компиляторов. Язык С — это процедурный, компилируемый язык программирования, созданный в начале 1970-х годов Деннисом Ритчи в Bell Labs. Программирование на языке С требует понимания не только самого языка, но и всей совокупности программ, задействованных в процессе превращения исходного текста в исполняемый файл. Программа на языке С не выполняется напрямую процессором. Исходный текст проходит несколько этапов обработки, прежде чем превратится в машинный код, который может быть запущен операционной системой. Язык программирования С существует не как набор случайных правил, а как строго определённая спецификация, зафиксированная в международных стандартах. Как исполняемый файл на С раскладывается по областям памяти — код, данные, BSS, куча и стек — и что это даёт при отладке. Архитектура программ на C - организация модулей, процесс компиляции и взаимосвязь компонентов системы. Язык программирования С занимает особое место в истории и практике разработки программного обеспечения. Типизация, набор правил определения типа данных значений языка. Язык программирования С предоставляет механизм создания составных типов данных, позволяющих объединять разнородные элементы под единым именем. Этот механизм называется структурой. Как на С организовать функции, владение ресурсами, коды ошибок и очистку без исключений и сборщика мусора. Реализация ассоциативного массива на С — хеш-функция, коллизии, цепочки, открытая адресация и изменение размера.История языка С
Основы языка С
Инструментальная цепочка компиляции С
Преобразование исходного кода в исполняемый файл
Стандарты языка С
Память процесса и сегменты
Архитектура программ на С
Компиляторы и среды разработки для С
Типы данных в С
Структуры и объединения
Идиомы кода и обработка ошибок
Хеш-таблица на С