Гид по технологиям

Потоки в C/C++ на Linux с pthread

6 min read Linux C/C++ Обновлено 05 Dec 2025
Потоки в C/C++ на Linux с pthread
Потоки в C/C++ на Linux с pthread

Монитор и клавиатура рядом с настольным ПК и множеством кабелей

Короткая справка

  • pthread — реализация POSIX-потоков для Linux. Определения находятся в заголовке pthread.h.
  • Потоки в Linux часто называют light-weight processes (лёгкие процессы): каждому потоку соответствует запись в ядре.
  • Типичные операции: создание (pthread_create), ожидание (pthread_join), отделение (pthread_detach), отмена (pthread_cancel).

Важно: при компиляции не забудьте явно связать библиотеку pthread: gcc -o app file.c -lpthread

История использования потоков в Linux

До ядра Linux 2.6 основой реализации были LinuxThreads — с ограничениями по производительности и синхронизации. В 2003 году была внедрена NPTL (Native POSIX Thread Library), которая значительно улучшила поведение для таких задач, как запуск JVM на Linux. Сегодня в GNU C Library содержатся реализации, обеспечивающие совместимость и производительность.

Примечание: pthread не является «зелёными потоками» — каждую pthread-ветку создаёт ядро; зелёные потоки, напротив, управляются на уровне виртуальной машины и выполняются в пользовательском пространстве.

Как потоки работают в Linux — логика выполнения

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

Информация по потокам процесса доступна в /proc//task — здесь для каждого потока создаётся отдельная запись.

Создание потока в C с pthread

Функция pthread_create создаёт новый поток. Сигнатура:

int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void *), void *arg);

Краткое объяснение параметров:

  • thread — указатель на pthread_t: через эту переменную вы сможете ссылаться на поток.
  • attr — указатель на атрибуты потока (можно передать NULL для значений по умолчанию). Через pthreadattr* можно настроить размер стека, политику планирования, состояние detach и др.
  • start_routine — функция, в которой выполняется код потока; она должна возвращать void и принимать void как аргумент.
  • arg — указатель на данные, передаваемые в функцию потока.

Функция возвращает 0 при успехе, иначе — код ошибки.

Пример программы

#include 
#include 
#include 
#include 

void *worker(void *data)
{
    char *name = (char*)data;

    for (int i = 0; i < 120; i++)
    {
        usleep(50000);
        printf("Hi from thread name = %s\n", name);
    }

    printf("Thread %s done!\n", name);
    return NULL;
}

int main(void)
{
    pthread_t th1, th2;
    pthread_create(&th1, NULL, worker, "X");
    pthread_create(&th2, NULL, worker, "Y");
    sleep(5);
    printf("Exiting from main program\n");
    return 0;
}

Команда компиляции:

gcc -o test test_thread.c -lpthread

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

Типы потоков: соединяемые и отделённые

По умолчанию создаваемые потоки являются соединяемыми (joinable): другой поток может вызывать pthread_join, чтобы дождаться их завершения и освободить системные ресурсы. Если поток завершился, но не вызван pthread_join, то часть системных ресурсов может оставаться занята (аналогично “zombie”-процессам).

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

int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
int pthread_detach(pthread_t thread);

Пример: вызов pthread_detach(th) сразу после создания — поток будет освобождён ядром при завершении.

Пример использования pthread_join

Замените main в первом примере на:

int main(void)
{
    pthread_t th1, th2;
    pthread_create(&th1, NULL, worker, "X");
    pthread_create(&th2, NULL, worker, "Y");
    sleep(5);
    printf("exiting from main program\n");
    pthread_join(th1, NULL);
    pthread_join(th2, NULL);
    return 0;
}

Вывод покажет, что основной поток ожидает завершения th1 и th2, и только затем программа полностью завершается.

Принудительное завершение потока

Для отмены потока можно использовать pthread_cancel:

int pthread_cancel(pthread_t thread);

Пример смены main для демонстрации отмены:

int main(void)
{
    pthread_t th1, th2;
    pthread_create(&th1, NULL, worker, "X");
    pthread_create(&th2, NULL, worker, "Y");
    sleep(1);
    printf("====> Cancelling Thread Y!!\n");
    pthread_cancel(th2);
    usleep(100000);
    printf("====> Cancelling Thread X!\n");
    pthread_cancel(th1);
    printf("exiting from main program\n");
    return 0;
}

Вывод программы: потоки выполняются, затем отменяются

Важно: поведение pthread_cancel зависит от состояния «точек отмены» (cancellation points) и политик отмены потока; безопасное использование требует продуманного управления ресурсами и освобождением памяти.

Когда создают потоки: сценарии и мотивация

  • Параллелизм ввода/вывода и вычислений на многопроцессорных системах.
  • Фоновые службы и обработчики событий.
  • Разделение работы на независимые задачи для повышения отзывчивости UI или сетевых служб.

Системы могут блокировать потоки, если они ждут ввода/вывода, ответов от других потоков или блокируются синхронизацией.

Синхронизация: мьютексы, условные переменные и барьеры

Для предотвращения гонок данных используйте:

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

Краткий пример использования мьютекса:

pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;

// В потоке A
pthread_mutex_lock(&lock);
// критическая секция
pthread_mutex_unlock(&lock);

Всегда освобождайте мьютексы в блоках finally-подобной логики (или используйте pthread_cleanup_push/pthread_cleanup_pop), чтобы избежать взаимоблокировок при ошибках.

Практические рекомендации и лучшие практики

  • Минимизируйте разделяемое состояние: проектируйте потоки так, чтобы обмен данными был через очереди или сообщения.
  • Делайте данные неизменяемыми (immutable) там, где это возможно.
  • Используйте RAII-обёртки в C++ (std::lock_guard, std::unique_lock) для автоматического управления блокировками.
  • Всегда обрабатывайте возвращаемые коды функций pthread.
  • Выбирайте join для потоков, результат которых нужен; detach — для независимых фоновых задач.
  • Не доверяйте pthread_cancel для освобождения критических ресурсов — лучше организовать явный протокол остановки (флаг остановки, условная переменная).

Альтернативы pthread и когда их выбирать

  • std::thread (C++11 и выше): более удобный интерфейс для C++ проектов и интеграция с std::mutex, std::future.
  • OpenMP: для параллелизации вычислительных циклов без ручного управления потоками.
  • libuv / libevent: для асинхронного ввода/вывода и событийной модели (чаще используются в сетевых приложениях).
  • Зеленые потоки и пользовательские планировщики (go-routine, fibers): если нужна лёгкая контекстная смена в пользовательском пространстве.

Выбор зависит от требований: контроль над ядровыми потоками (pthread) даёт предсказуемую производительность и точный контроль над ресурсами.

Ментальные модели и эвристики

  • Поток = тяжёлый объект ОС, но легче, чем отдельный процесс: разделяет память процесса.
  • Думайте о потоках как о рабочих, у каждого свой стек и регистрационный контекст; данные — в общем хранилище (heap).
  • Сделайте мьютекс атомарным «телохранителем» для защиты состояния: перед доступом — взять, после — отпустить.

Контроль качества: тесты и критерии приёмки

  • Многопоточный тест, воспроизводящий «peak» нагрузки и задержки.
  • Отсутствие гонок данных: тесты с ThreadSanitizer (TSan) или аналогичными инструментами.
  • Детектирование утечек ресурсов: valgrind, проверка количества потоков после выполнения задачи.
  • Стабильность при отмене: корректное освобождение памяти и дескрипторов при вызове cancel/stop.

Критерии приёмки:

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

Методология проектирования многопоточных подсистем (мини-метод)

  1. Определите обязанности каждого потока (I/O, обработка, мониторинг).
  2. Минимизируйте количество точек синхронизации.
  3. Спроектируйте API обмена (очереди, сигналы, условные переменные).
  4. Протестируйте отдельные сценарии в изоляции (unit), затем интеграционные тесты под нагрузкой.
  5. Используйте статические и динамические анализаторы: clang-tidy, AddressSanitizer, ThreadSanitizer.

Дерево решений: join vs detach (Mermaid)

flowchart TD
  A[Нужен ли результат работы потока?] -->|Да| B[Использовать join]
  A -->|Нет| C[Можно ли явно управлять временем жизни?]
  C -->|Да| B
  C -->|Нет| D[Использовать detached]
  B --> E[Вызвать pthread_join]
  D --> F[Вызвать pthread_detach или установить атрибут PTHREAD_CREATE_DETACHED]

Рольовые чек-листы

Разработчик:

  • Проверить обработку ошибок возвращаемых значений pthread.
  • Минимизировать разделяемое состояние.
  • Добавить юнит-тесты и использовать TSan.

Инженер QA:

  • Запустить стресс-тесты с задержками, искусственными блокировками.
  • Проверить отсутствие утечек потоков в /proc и в логах.

Системный администратор:

  • Мониторить количество потоков процесса и использование CPU/mem.
  • Настроить limits (ulimit/pam) при необходимости на стороне ОС.

Частые ошибки и как их избежать (галерея крайних случаев)

  • Забытый pthread_join — приводит к удержанию ресурсов. Решение: join или detach по дизайну.
  • Использование pthread_cancel для быстрой очистки — может привести к утечкам. Решение: явный протокол завершения.
  • Неправильная инициализация атрибутов — ошибки планирования или нестабильность. Решение: проверять return-коды и инициализировать через pthread_attr_init.

Короткий словарь (1 строка на термин)

  • pthread_t — тип идентификатора потока.
  • pthread_attr_t — структура для атрибутов при создании потока.
  • detached — состояние потока, при котором ресурсы освобождаются автоматически.
  • joinable — обычное состояние, требующее pthread_join для освобождения ресурсов.

Сводка

  • pthread остаётся надёжным и низкоуровневым инструментом для параллелизма на Linux.
  • Поверьте в дизайн: предпочитайте простые модели обмена данными и тщательно управляйте жизненным циклом потоков.
  • Используйте современные инструменты тестирования и анализаторы для обнаружения гонок и утечек.

Дополнительные заметки:

  • Для C++ проектов рассмотрите std::thread и стандартные обёртки для безопасности исключений.
  • На production-системах мониторьте число потоков и политики планирования, особенно при высоконагруженных сервисах.
Поделиться: X/Twitter Facebook LinkedIn Telegram
Автор
Редакция

Похожие материалы

Swapfile в Linux: настройка и лучшие практики
Linux

Swapfile в Linux: настройка и лучшие практики

Как подключить Android к проектору — полное руководство
How-to

Как подключить Android к проектору — полное руководство

Быстрая перемотка ветки в Git — как и когда
GIT

Быстрая перемотка ветки в Git — как и когда

LocalStorage в Vue — To‑Do с сохранением
Веб-разработка

LocalStorage в Vue — To‑Do с сохранением

Как отключить VPN на iPhone — пошагово
Безопасность

Как отключить VPN на iPhone — пошагово

Fix Download failed — network error в Chrome
Troubleshooting

Fix Download failed — network error в Chrome