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

Работа с потоками в Linux с помощью pthread

6 min read Разработка Обновлено 04 Jan 2026
Потоки в Linux с pthread
Потоки в Linux с pthread

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

Ключевая цель статьи

Показать, как создавать, синхронизировать и завершать потоки в C/C++ на Linux с использованием pthread, а также дать практические советы и шаблоны для распространённых задач.

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

До версии Linux 2.6 основной реализацией потоков была LinuxThreads. У той реализации были ограничения по производительности и по синхронизации. Максимально доступное количество потоков в некоторых конфигурациях было ограничено тысячами и приносило проблемы на серверных нагрузках.

В 2003 году появилась Native POSIX Thread Library (NPTL). Её внедрили, чтобы исправить проблемы с производительностью JVM на Linux. Сегодня в glibc присутствуют механизмы, совместимые с POSIX, и NPTL — это стандартная реализация в большинстве дистрибутивов.

Важно: pthread не реализует зелёные потоки (green threads), которые управляются виртуальной машиной и выполняются исключительно в пользовательском пространстве. При использовании pthread ядро создаёт нативный поток (light-weight process) для каждого вызова создания потока.

Информация о потоках любого запущенного процесса доступна в /proc//task — стандартное расположение procfs для данных о задачах. В однопоточных приложениях вы увидите запись задачи с тем же значением, что и PID.

Логика работы потоков

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

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

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

Функция pthread_create создаёт новый поток. Подключите и при компиляции явно свяжитесь с библиотекой pthread:

gcc -o test test_thread.c -lpthread

Подпись функции:

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

Возвращает 0 при успехе или код ошибки при неудаче.

Параметры:

  • thread (pthread_t*): дескриптор создаваемого потока.
  • attr (pthread_attr_t*): атрибуты потока (политика планирования, размер стека, detach-state и т.д.). Можно передать NULL для значений по умолчанию.
  • start_routine: указатель на функцию, которую выполнит поток.
  • arg: указатель на аргумент для start_routine.

Пример простого приложения:

#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;
}

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

Синхронизация и примитивы

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

  • mutex (pthread_mutex_t) — для взаимного исключения.
  • условные переменные (pthread_cond_t) — для ожидания событий между потоками.
  • семафоры (sem_t) — альтернативный механизм.

Базовый пример использования mutex (схематично):

pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;

pthread_mutex_lock(&lock);
// доступ к общей памяти
pthread_mutex_unlock(&lock);

Помните: неправильное использование mutex может привести к дедлоку. Всегда фиксируйте порядок блокировок и минимизируйте объём критической секции.

Типы потоков и возвращаемые ресурсы

Если главный поток (main) возвращает управление, процесс завершает работу и закрывает все потоки. Аналогично, вызов exit() в любом потоке завершит весь процесс.

pthread_join позволяет одному потоку ждать завершения другого. Вызвав pthread_join, вы блокируете текущий поток до тех пор, пока целевой поток не завершится. Без join ресурс, занимаемый потоком (thread stack, т.п.), не будет освобождён системой, если поток был создаваем как joinable.

Если заранее нельзя предсказать, когда поток завершится, удобнее делать поток detached. В detached-режиме ОС автоматически освободит ресурсы после завершения потока.

Установить detached можно двумя способами:

int pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
int pthread_detach(pthread_t thread);

Пример с ожиданием через 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;
}

Ожидаемый вывод (фрагмент):

Hi from thread Y
Hi from thread X
...
exiting from main program
Thread X done!
Thread Y done!

Завершение и отмена потоков

Отменить поток можно с помощью pthread_cancel, передав идентификатор pthread_t:

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 инициирует запрос на отмену. Поток может обработать этот запрос в определённых точках отмены (cancelation points) — например, при блокирующих вызовах. Для безопасного завершения используйте механизмы очистки (pthread_cleanup_push/pthread_cleanup_pop) и неправильно полагаться на мгновенную остановку.

Зачем создавать потоки

Главные причины создания потоков:

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

Планировщик ОС старается разместить потоки на доступных CPU. Поток может быть заблокирован из-за ввода-вывода, ожидания ответа от другого потока или явного ожидания в коде.

Вы можете менять атрибуты потока: политику планирования или приоритеты (для этого требуются права и корректная поддержка от ядра и C library). Политики планирования: SCHED_FIFO, SCHED_RR и другие — применяются в специализированных сценариях.

Когда потоки не подходят (примеры)

  • Встроенные системы с жёсткими временными ограничениями, где планирование ядра не гарантирует нужную детерминированность.
  • Высокоизолированные задачі, где предпочтительнее процессы, а не разделяемая память (безопасность, fault isolation).
  • Сценарии с миллионами лёгких задач — тут эффективнее зелёные потоки/корутины или асинхронная модель, чтобы уменьшить накладные расходы на системные потоки.

Альтернативы pthread

  • Процессы (fork): для полной изоляции и безопасности.
  • Асинхронные модели (libuv, io_uring, async/await в языках высокого уровня): для масштабируемого I/O без множества потоков.
  • Зелёные потоки/корутины (например, goroutines в Go или пользовательские планировщики в runtime): для лёгкой конкурентности на уровне пользователя.

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

  • Потоки = лёгкие процессы: разделяют память, но управляются ядром.
  • Если нужен быстрый отклик на I/O — используйте неблокирующий ввод/вывод или отдельный пул потоков.
  • Всегда минимизируйте критические секции и фиксируйте порядок блокировок, чтобы избежать дедлоков.
  • Помните о возвращаемых ресурсах: join для контроля завершения; detach, если не нужен контролируемый join.

Decision flow: ждать или сделать detached

flowchart TD
  A[Нужно ли дождаться результата потока?] -->|Да| B[Использовать pthread_join]
  A -->|Нет| C[Создать поток как detached или вызвать pthread_detach]
  B --> D[Получите return value и освободите ресурсы]
  C --> E[Система освободит ресурсы автоматически после завершения]

Чек-лист по ролям

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

  • Используйте mutex/cond для защиты общих данных.
  • Документируйте порядок блокировок.
  • Выбирайте join/detach согласно жизненному циклу задачи.

DevOps/Системный инженер:

  • Контролируйте лимиты процессов/потоков (ulimit, systemd конфигурации).
  • Мониторьте использование памяти на поток (стек).
  • Тестируйте поведение при нехватке ресурсов.

Тестировщик:

  • Проверяйте дедлоки и гонки данных (ThreadSanitizer, helgrind).
  • Симулируйте задержки ввода-вывода и отмену потоков.

Шпаргалка по основным функциям pthread

  • pthread_create — создать поток.
  • pthread_join — дождаться завершения потока и получить return value.
  • pthread_detach / attr_setdetachstate — сделать поток detached.
  • pthread_cancel — отправить запрос на отмену потока.
  • pthread_mutex_init / lock / unlock / destroy — mutex операции.
  • pthread_cond_wait / signal / broadcast — условные переменные.

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

  • Код корректно создаёт и завершает потоки без утечек ресурсов.
  • Отсутствуют гонки данных и дедлоки (проверено инструментами или покрытием тестов).
  • Поведение при отмене потока предсказуемо и безопасно.

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

  • pthread: POSIX Threads, стандартная API для потоков в Unix-подобных системах.
  • thread: поток выполнения внутри процесса, имеющий свой стек и регистры.
  • process: независимый экземпляр программы с собственным адресным пространством.
  • join: ожидание завершения потока и получение его результата.
  • detach: режим, при котором ОС автоматически освобождает ресурсы потока.

Заключение

Потоки через pthread — мощный инструмент для параллельной и асинхронной работы на Linux. Они дают простоту совместного доступа к памяти и гибкость, но требуют аккуратной синхронизации и управления ресурсами. Выбирайте между join и detached в зависимости от потребностей в управлении жизненным циклом и освобождении ресурсов. Для сценариев с миллионами лёгких задач рассмотрите пользовательские планировщики или асинхронные модели.

Важно: тестируйте на реальной нагрузке, используйте анализаторы гонок и следите за лимитами системы.

Ресурсы для изучения

  • man pthreads (man pthread_create)
  • документация glibc по NPTL
  • инструменты: ThreadSanitizer, helgrind
Поделиться: X/Twitter Facebook LinkedIn Telegram
Автор
Редакция

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

RDP: полный гид по настройке и безопасности
Инфраструктура

RDP: полный гид по настройке и безопасности

Android как клавиатура и трекпад для Windows
Гайды

Android как клавиатура и трекпад для Windows

Советы и приёмы для работы с PDF
Документы

Советы и приёмы для работы с PDF

Calibration в Lightroom Classic: как и когда использовать
Фото

Calibration в Lightroom Classic: как и когда использовать

Отключить Siri Suggestions на iPhone
iOS

Отключить Siri Suggestions на iPhone

Рисование таблиц в Microsoft Word — руководство
Office

Рисование таблиц в Microsoft Word — руководство