Перейти к содержанию

Последнее обновление:2026-10-12

Введение в многопоточность C++

Весь код из этой статьи находится в репозитории «C++那些事».

https://github.com/Light-City/CPlusPlusThings

1. Понимание потоков и процессов

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

Например: браузер — это процесс, а каждая открытая вкладка — поток.

Краткие различия между ними:

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

О связи через IPC и обмене между потоками будет отдельная статья.

2. Пять способов создания потоков

  • указатель на функцию
  • лямбда-функция
  • Functor (функциональный объект)
  • нестатическая функция-член
  • статическая функция-член

2.1 Указатель на функцию

// 1.函数指针
void fun(int x) {
    while (x-- > 0) {
        cout << x << endl;
    }
}
// 调用
std::thread t1(fun, 10);
t1.join();

2.2 Лямбда-функция

// 注意:如果我们创建多线程 并不会保证哪一个先开始
int main() {
    // 2.Lambda函数
    auto fun = [](int x) {
        while (x-- > 0) {
            cout << x << endl;
        }
    };
//    std::1.thread t1(fun, 10);
    // 也可以写成下面:
    std::thread t1_1([](int x) {
        while (x-- > 0) {
            cout << x << endl;
        }
    }, 11);
//    std::1.thread t2(fun, 10);
//    t1.join();
    t1_1.join();
//    t2.join();
    return 0;
}

2.3 Функциональный объект

// 3.functor (Funciton Object)
class Base {
public:
    void operator()(int x) {
        while (x-- > 0) {
            cout << x << endl;
        }
    }
};
// 调用
thread t(Base(), 10);
t.join();

2.4 Нестатическая функция-член

// 4.Non-static member function
class Base {
public:
    void fun(int x) {
        while (x-- > 0) {
            cout << x << endl;
        }
    }
};
// 调用
thread t(&Base::fun,&b, 10);
t.join();

2.5 Статическая функция-член

// 4.Non-static member function
class Base {
public:
    static void fun(int x) {
        while (x-- > 0) {
            cout << x << endl;
        }
    }
};
// 调用
thread t(&Base::fun, 10);
t.join();

3. join и detach

3.1 join

  • как только поток запущен, чтобы дождаться его завершения, нужно вызвать на объекте join()
  • двойной join приведёт к завершению программы
  • перед join следует проверить, можно ли его выполнить, с помощью joinable()
void run(int count) {
    while (count-- > 0) {
        cout << count << endl;
    }
    std::this_thread::sleep_for(chrono::seconds(3));
}

int main() {
    thread t1(run, 10);
    cout << "main()" << endl;
    t1.join();
    if (t1.joinable()) {
        t1.join();
    }
    cout << "main() after" << endl;
    return 0;
}

3.2 detach

  • это используется для отделения вновь созданного потока от родительского
  • перед отделением потока обязательно проверьте, является ли он joinable, иначе возможно двойное отделение, а двойной detach() приведёт к завершению программы
  • если у нас есть отделённый поток и функция main возвращается, выполнение отделённого потока будет приостановлено
void run(int count) {
    while (count-- > 0) {
        cout << count << endl;
    }
    std::this_thread::sleep_for(chrono::seconds(3));
}

int main() {
    thread t1(run, 10);
    cout << "main()" << endl;
    t1.detach();
    if(t1.joinable())
        t1.detach();
    cout << "main() after" << endl;
    return 0;

4. Критические секции и мьютексы

4.1 Что такое критическая секция (Critical Sections)?

Критическая секция — это участок кода, который для корректной работы программы должен выполняться одновременно только одним потоком. Если два потока (или процесса) одновременно выполняют код внутри критической секции, программа может перестать работать корректно.

4.2 Является ли простое увеличение переменной критической секцией?

Пожалуй, да.

Процесс увеличения переменной (i++) состоит из трёх шагов:

  • копирование содержимого памяти в регистр CPU. load
  • увеличение значения в CPU. increment
  • сохранение нового значения в памяти. store

Если к этой ячейке памяти (например, к переменной i ниже) возможен доступ только одного потока, гонки не возникает и связанной с i критической секции нет. Но переменная sum — глобальная, к ней могут обращаться два потока. Два потока могут попытаться увеличить переменную одновременно.

#include <iostream>
#include <mutex>
#include <thread>

using namespace std;

int sum = 0; //shared

mutex m;

void *countgold() {
    int i; //local to each thread
    for (i = 0; i < 10000000; i++) {
        sum += 1;
    }
    return NULL;
}

int main() {
    thread t1(countgold);
    thread t2(countgold);

    //Wait for both threads to finish
    t1.join();
    t2.join();

    cout << "sum = " << sum << endl;
    return 0;
}
Типичный вывод приведённого выше кода: сумма sum равна 20000000. Из-за наличия условия гонки каждый запуск программы будет печатать разную сумму. Этот код не препятствует одновременному чтению и записи суммы двумя потоками. Например, оба потока копируют текущее значение sum в CPU, на котором выполняется каждый из них (пусть будет 123). Оба потока увеличивают на единицу свою копию. Оба потока записывают значение обратно (124). Если бы потоки обращались к сумме в разное время, счёт был бы 125.

4.3 Как гарантировать, что только один поток одновременно обращается к глобальной переменной?

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

Наглядная метафора мьютекса:

bg2013042408

Простой способ не пустить других внутрь — повесить на дверь замок. Пришедший первым запирает дверь, пришедшие позже видят замок и выстраиваются у двери, дожидаясь, когда замок откроют. Это и называется "мьютексом" (Mutual exclusion, сокращённо Mutex) — он не даёт нескольким потокам одновременно читать и писать одну область памяти.

m.lock();
sum += 1;
m.unlock();
Приведённый выше код выводит корректно: sum = 20000000.

Справочные материалы

http://www.ruanyifeng.com/blog/2013/04/processes_and_threads.html?utm_source=com.ideashower.readitlater.pro&utm_medium=social&utm_oi=35626384621568

https://www.youtube.com/watch?v=eZ8yKZo-PGw&list=PLk6CEY9XxSIAeK-EAh3hB4fgNvYkYmghp&index=4

Похожие темы

Комментарии