一、线程管理

 1.1 std::thread

封装系统原生线程,可创建、管理线程生命周期,不可拷贝、可移动

例如:

#include <thread>
#include <iostream>

void func(int x) {
    std::cout << "线程运行: " << x << "\n";
}

int main() {
    std::thread t(func, 10); // 创建线程并启动
    if (t.joinable()) t.join(); // 等待线程结束
    // t.detach(); // 分离线程,后台运行(谨慎使用)
    return 0;
}

常用成员

  • join():阻塞等待线程完成
  • detach():分离线程,生命周期脱离主线程
  • get_id():获取线程 ID
  • joinable():是否可 join(未 join/detach)

1.2 std::this_thread命名空间

注意:this_thread只是一个命名空间

提供当前线程操作:

  • get_id():获取当前线程 ID
  • sleep_for(duration):休眠指定时间(duration是一个类,具体可见官方文档或者问ai)
  • yield():让出 CPU,触发调度

我们可以通过this_thread::get_id()来获取当前线程的id

二、同步与互斥

2.1互斥锁(Mutex)

  • std::mutex:基础互斥锁,独占访问
  • std::recursive_mutex:递归锁,同一线程可多次加锁
  • std::timed_mutex:带超时的锁
  • std::lock_guard<M>:RAII 自动锁,构造加锁、析构解锁(异常安全)
  • std::unique_lock<M>:灵活锁,支持手动 lock/unlock、超时、转移所有权。
2.1.1 std::mutex

mutex是封装的互斥锁的类,用于保护临界区的共享数据。mutex主要提供lock和unlock两个接口函数来获取锁。 mutex 提供排他性非递归所有权语义:(1)调用方线程从它成功调用lock或try_lock开始,到它调用unlock为止占有mutex。(2)线程占有mutex时,其他线程如果试图要求mutex 的所有权,那么就会阻塞(对于lock的调用),对于try_lock就会返回false,但并不阻塞,调用方线程会继续执行下去。

std::mutex不支持递归加锁(即:线程不得对自身已持有的互斥锁再次加锁),如需递归加锁,可使用recursive_mutex替代类。

我们可以通过C++文档来看该类的介绍,例如:

说明了std::mutex是一个不支持拷贝的类,毕竟互斥锁的状态(是否被持有、哪个线程持有)是全局共享、唯一的,一旦拷贝,就会出现 “两个锁实例都指向同一个内核对象” 的混乱状态,导致死锁、数据竞争等问题。

例如:

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

void Print(int n, int& rx, std::mutex& rmtx)
{
    rmtx.lock();
    for (int i = 0; i < n; i++)
    {
        ++rx;
    }
    rmtx.unlock();
}

int main()
{

    // std::ios::sync_with_stdio(false);
    // std::cin.tie(nullptr);
    int x = 0;
    std::mutex mtx;

    std::thread t1(Print, 1000000, std::ref(x), std::ref(mtx));
    std::thread t2(Print, 2000000, std::ref(x), std::ref(mtx));

    t1.join();
    t2.join();

    std::cout << x << std::endl;
    return 0;
}
注意代码中我们没有直接用x和mtx当作形参传入,而是用std::ref()
这是为什么呢?
因为std::thread会默认把所有参数 拷贝 / 移动 一份, 不会直接用引用!如果直接传入x和mtx,它会 传值,而不是传引用,从而程序会 直接报错、编译失败
也就是说我们写
std::thread t1(Print, 1000000, x, mtx);
结果会变成
Print(1000000, 拷贝(x), 拷贝(mtx));
而我们ref的作用是把变量包装成一个引用器对象
即:
std::ref(x) → 变成 int&
std::ref(mtx) → 变成 mutex&
所以std::thread拷贝的参数是引用类型,所以编译成功

在thread源码中,我们可通过这个来分析

    template <class _Fn, class... _Args>
    void _Start(_Fn&& _Fx, _Args&&... _Ax) {
        using _Tuple                 = tuple<decay_t<_Fn>, decay_t<_Args>...>;
        auto _Decay_copied           = _STD make_unique<_Tuple>(_STD forward<_Fn>(_Fx), _STD forward<_Args>(_Ax)...);
        constexpr auto _Invoker_proc = _Get_invoke<_Tuple>(make_index_sequence<1 + sizeof...(_Args)>{});

        _Thr._Hnd =
            reinterpret_cast<void*>(_CSTD _beginthreadex(nullptr, 0, _Invoker_proc, _Decay_copied.get(), 0, &_Thr._Id));

        if (_Thr._Hnd) { // ownership transferred to the thread
            (void) _Decay_copied.release();
        } else { // failed to start thread
            _Thr._Id = 0;
            _Throw_Cpp_error(_RESOURCE_UNAVAILABLE_TRY_AGAIN);
        }
    }

首先是tuple<decay_t<_Fn>, decay_t<_Args>...>
decay表示把复杂类型变成最简单的值类型,所以会导致传入的int &变成int以及mutex &变成mutex,然后tuple把参数全部打包。

然后下面那个make_unique<>相当于把打包好的复制一份给线程
注意:我们的std::ref()是一个包装器返回的是对象,decay只会擦去原生的&,也就是内置类型,但是不会处理,也不认识你的类/对象

上述例子如果不想加std::ref的话我们可以利用lambda表达式
例如:

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


int main()
{
    int x = 0;
    std::mutex mtx;
    // 改成使用lambda捕获外层的对象,也就可以不用传参数

    auto Print = [&x, &mtx](size_t n)
    {
        mtx.lock();
        for (size_t i = 0; i < n; i++)
        {
            ++x;
        }
        mtx.unlock();
    };
    std::thread t1(Print, 1000000);
    std::thread t2(Print, 2000000);
    t1.join();
    t2.join();
    std::cout << x << std::endl;
    return 0;
}

我们利用lambda表达式的特性,让其去捕捉参数就行

2.1.2 std::recursive_mutex:递归锁

这是适合递归函数用的锁

2.1.3 std::timed_mutex:带超时的锁

time_mutex跟mutex完全类似,只是额外提供try_lock_for和try_lock_untile的接口,这两个接口跟try_lock类似,只是他不会马上返回,而是直接进入阻塞,直到时间条件到了或者解锁了就会唤醒试图获取锁资源。
例如

// timed_mutex::try_lock_for example
#include <iostream>       // std::cout
#include <chrono>         // std::chrono::milliseconds
#include <thread>         // std::thread
#include <mutex>          // std::timed_mutex

std::timed_mutex mtx;

void fireworks() {
    // waiting to get a lock: each thread prints "-" every 200ms:
    while (!mtx.try_lock_for(std::chrono::milliseconds(200))) {
        std::cout << "-";
    }
    // got a lock! - wait for 1s, then this thread prints "*"
    std::this_thread::sleep_for(std::chrono::milliseconds(1000));
    std::cout << "*\n";
    mtx.unlock();
}

int main()
{
    std::thread threads[10];
    // spawn 10 threads:
    for (int i = 0; i < 10; ++i)
        threads[i] = std::thread(fireworks);

    for (auto& th : threads) th.join();

    return 0;
}

代码意思是创建10个线程抢锁,然后
while (!mtx.try_lock_for(std::chrono::milliseconds(200))) {
    std::cout << "-";
}
该代码意思是

我尝试抢锁,只等 200 毫秒。

  • 抢到锁 → 返回 true
  • 200ms 都没抢到 → 返回 false

如果没抢到那就返回false一直打印那个"-",抢到了,则等待1s,打印"*",并释放锁,退出函数,让其他线程竞争

2.1.4 std::lock_guard<M>

lock_guard是C++11提供的支持RAII方式管理互斥锁资源的类,这样可以更有效的防止因为异常等 原因导致的死锁问题。
lock_guard的功能简单纯粹,仅仅支持RAII的方式管理锁对象。也可以在构造的时候通过传参 adopt_lock_t的adopt_lock对象管理已经lock的锁对象。其次lock_guard类不支持拷贝构造。
也就是说lock_guard支持RAII会在作用域结束后自己析构释放资源,不用我们自己手写unlock()函数
我们lock_guard有两个构造函数

第一个构造函数直接传入mutex锁就行
第二个我们还有一个参数adopt_lock_t tag表示:空标签类型,相当于告诉 lock_guard:“这个锁已经被我锁住了,你别再锁了,只帮我自动解锁就行 “,也就是说我们用第二个参数不用再用锁这个资源,锁资源已经在之前的代码中被lock了,我们只需要等lock_guard对象生命周期结束后,释放传入的锁就行了

例如:

void print_thread_id_1(int id) {
mtx.lock();
std::lock_guard<std::mutex> lck(mtx, std::adopt_lock);
std::cout << "thread #" << id << '\n';
}

void print_thread_id_2(int id) {
std::lock_guard<std::mutex> lck(mtx);
std::cout << "thread #" << id << '\n';
}
2.1.5 unique_lock

unique_lock也是C++11提供的支持RAII方式管理互斥锁资源的类,相比lock_guard他的功能支持更丰富复杂。

unique_lock首先在构造的时候传不同的tag,用以支持在构造的时候不同的方式处理锁对象。

unique_lock在构造的时候传时间段和时间点,用来管理time_mutex系统,构造时调用try_lock_for和try_lock_until

unique_lock不支持拷贝和赋值,支持移动构造和移动赋值。

2.1.6 lock和try_lock

lock是<mutex>文件提供的⼀个函数模板,可以支持对多个锁对象同时锁定,如果其中⼀个锁对象没有锁住,lock函数 会把已经锁定的对象解锁而进入阻塞,直到锁定所有的所有的对象。

try_lock也是<mutex>文件提供的⼀个函数模板,尝试对多个锁对象进行同时尝试锁定,如果全部锁对象都锁定了,返 回-1,如果某⼀个锁对象尝试锁定失败,把已经锁定成功的锁对象解锁,并则返回这个对象的下标 (第⼀个参数对象,下标从1开始算)。

template <class Mutex1, class Mutex2, class... Mutexes>
void lock (Mutex1& a, Mutex2& b, Mutexes&... cde);
template <class Mutex1, class Mutex2, class... Mutexes>
int try_lock (Mutex1& a, Mutex2& b, Mutexes&... cde);

例如:我们想在同一个作用域用两次锁资源的时候,又怕死锁,我们就可以用lock()和try_lock()

lock()例子:

#include <iostream>
#include <thread>
#include <mutex>
// std::cout
// std::thread
// std::mutex, std::lock
std::mutex foo, bar;
void task_a()
{
    // foo.lock(); bar.lock(); // replaced by:
    std::lock(foo, bar);
    std::cout << "task a\n";
    foo.unlock();
    bar.unlock();
}
void task_b()
{
    // bar.lock(); foo.lock(); // replaced by:
    std::lock(bar, foo);
    std::cout << "task b\n";
    bar.unlock();
    foo.unlock();
}
int main()
{
    foo.lock();
    std::thread th1(task_a);
    std::thread th2(task_b);
    std::cout << "xxxxxx" << std::endl;
    bar.lock();
    foo.unlock();
    std::cout << "yyyyyy" << std::endl;
    bar.unlock();
    th1.join();
    th2.join();
    return 0;
}

try_lock()例子:

#include <iostream> // std::cout
#include <thread>   // std::thread
#include <mutex>    // std::mutex, std::try_lock
std::mutex foo, bar;
void task_a()
{
    foo.lock();
    std::cout << "task a\n";
    bar.lock();
    // ...
    foo.unlock();
    bar.unlock();
}
void task_b()
{
    int x = try_lock(bar, foo);
    if (x == -1)
    {
        std::cout << "task b\n";
        // ...
        bar.unlock();
        foo.unlock();
    }
    else
    {
        std::cout << "[task b failed: mutex " << (x ? "foo" : "bar")
                  << " locked]\n";
    }
}

int main()
{
    std::thread th1(task_a);
    std::thread th2(task_b);
    th1.join();
    th2.join();
    return 0;
}
2.1.7 std::call_once

std::call_once是 C++11 并发支持库提供的线程安全一次性执行工具(本质是一个函数),专门解决多线程环境下,某段代码 / 函数必须执行、且只能执行一次的核心需求。

// 必须包含的头文件
#include <mutex>

// 函数原型
template<class Callable, class... Args>
void call_once(once_flag& flag, Callable&& func, Args&&... args);

注意:once_flag:一个不可拷贝、不可移动的状态标志,用于标记目标函数是否执行完成

           fun:需要仅执行一次的可调用对象(普通函数、lambda、成员函数均可)

           args:传递给func的参数。

核心特性(必知)

  1. 仅执行一次:无论多少线程调用,目标函数只会成功执行一次
  2. 绝对线程安全:无竞态条件,底层用原子操作实现,比手动加锁更可靠;
  3. 异常安全:如果函数执行时抛出异常,不算执行成功,once_flag 不会锁定,其他线程会继续尝试执行,直到有一个线程成功完成;
  4. 阻塞等待:一个线程执行函数时,其他调用 call_once 的线程会阻塞等待,直到函数执行完毕;
  5. 轻量高效:性能远优于 mutex + bool 手动实现方案。

例子:

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

int winner;
void set_winner(int x) { winner = x; }
std::once_flag winner_flag;
void wait_1000ms(int id)
{
    // count to 1000, waiting 1ms between increments:
    for (int i = 0; i < 1000; ++i)
        std::this_thread::sleep_for(std::chrono::milliseconds(1));
    // claim to be the winner (only the first such call is executed):
    std::call_once(winner_flag, set_winner, id);
}
int main()
{
    std::thread threads[10];
    // spawn 10 threads:
    for (int i = 0; i < 10; ++i)
        threads[i] = std::thread(wait_1000ms, i + 1);
    std::cout << "waiting for the first among 10 threads to count 1000 ms...\n";

    for (auto &th : threads)
        th.join();
    std::cout << "winner thread: " << winner << '\n';
    return 0;
}

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐