多线程受限可以利用多核,其次可以在一定程度上避免阻塞产生,当然 cpp中的async 对io类的阻塞可能开销更小一点。

kiraYuukiAsuna的个人空间, 本文大部分内容来源是这位 up 主, 讲的我觉得挺好的.

std::thread

这里要说的不多, 主要就是 std::thread 这个类, 初始化的话需要传入函数名和参数列表, 当然还有其他构造函数, 可以看 cppreference 针对这个类有两个重要的概念, 假如说当前有一个线程叫 thread1

  • join : 或者叫 wait 更合适一点, 如果 thread1.join() 被调用, 就会阻塞当前的线程, 直到 thread1 执行完毕, 子线程还没做完事, 主线程结束了不是很尴尬嘛.
  • detach : 将当前的线程和 thread1 完全割裂

一个线程在现代的机器上对应一个 cpu 核心, 当我关掉一个线程, 对应核心的利用率就会飞速下降.

如果线程函数有引用类型的参数, 需要用 std::ref 包装一下, 详情可以看: c++ - Passing object by reference to std::thread in C++11 - Stack Overflow 如果多个线程访问公共资源, 记得加锁

性能优化 condition_variable

之前我们看到了, 一个线程跑满的情况下对 cpu 的占用率是十分恐怖的, 一直处于自旋转状态的 mutex 会造成 cpu 资源的极大浪费, 所以为了缓解这种情况, 有以下两种措施可供选择:

  1. sleep 一段时间, 但问题是这段时间该有多长呢? 所以这种方式并不好
  2. condition_variable 与锁(如 mutex)配合使用,其核心在于利用了操作系统的内核级等待/唤醒机制。当条件不满足时,等待的线程会被置于休眠(或阻塞)状态并释放锁,完全不占用 CPU 时间。直到其他线程满足了条件并通过 notify 发出通知,操作系统才会将其唤醒。这种机制从根本上避免了因循环检查(忙等待)导致的 CPU 资源浪费。
#include <iostream>
#include <condition_variable>
#include <mutex>
#include <deque>
#include <thread>
#include <random>
using namespace std::literals::chrono_literals;
 
std::condition_variable cv;
std::mutex m;
int i = 0;
 
[[noreturn]] void producer(std::deque<int>& v)
{
    while (true)
    {
        // std::this_thread::sleep_for(1s);
        std::unique_lock<std::mutex> lock(m);
        v.push_back(i++);
        cv.notify_one();//唤醒第一个进入等待状态的线程
    }
}
 
[[noreturn]] void consumer(std::deque<int>& v, std::string cid)
{
    while (true)
    {
	    // 条件变量都是与锁配合使用的
        std::unique_lock<std::mutex> lock(m);
        while (v.empty()) //注意这里一定要是while,不能是if(会产生虚假唤醒),wait_for自带while
        {
            cv.wait(lock);
        }
 
        const int k = v.front();
        v.pop_front();
        std::cout << cid << " get num: " << k << std::endl;
    }
};
 
int main(int argc, char* argv[])
{
    std::deque<int> que;
    std::thread p(producer, std::ref(que));
    std::thread c1(consumer, std::ref(que), "c1");
    std::thread c2(consumer, std::ref(que), "c2");
 
 
    p.join();
    c1.join();
    c2.join();
}

上面提到的虚假唤醒会使得程序 crash, 因为另一个线程已经提前取走了被唤醒线程本来应该拿到的资源, 具体报错如下:

cpp20 标准中还有 semaphore 这种东西, 有兴趣可以了解一下

线程同步 promise&future

上面我们为了等待子线程处理完所有数据得到正确的 interface 数组的过程中,写了很多臃肿的代码,事实上通过 promise 和 future 这两个模板类,就可以方便的异步获取变量了。

 
void DoSomething(const int idx, std::vector<int>& interface, std::promise<void>& in)
{
    for (int i = 0; i < idx; i++)
    {
        std::this_thread::sleep_for(200ms);
        interface.push_back(i);
    }
    in.set_value();
}
 
int main()
{
    std::promise<void> pro;
    auto  f = pro.get_future();
    std::vector<int> interface;
    std::thread td(DoSomething, 10, std::ref(interface), std::ref(pro));
    f.wait();
    for (const int value : interface) std::cout << value << " ";
    std::cout << std::endl;
    td.join();
    return 0;
}

packaged_task

这个模板类可以帮助我们将函数于返回值在多线程之间实现交互,譬如:

double compute(double num)
{
    return std::sin(num);
}
int main()
{
    std::packaged_task<double(double)> task(compute);
    std::thread t([&] { task(3.1415926); });
    // 没错,就是与future联系在了一起
    std::cout << task.get_future().get() << std::endl;
    t.join();
}

死锁问题

可以用 RAII 的方式来解决, 利用 std::lock_guard 可以针对某种情况下忘记 unlock 而产生的死锁.

而由加锁顺序产生的死锁可以用 std::lock 统一加锁来解决. 也可以将资源改成 atomic 类型来避免使用锁.

事实上死锁问题很多时候是由于协同开发产生的,你不能掌控其他人写的代码。