一文读懂 C++ 并发与多线程 独家原创

目录

一、基本概念

二、线程创建以及 join() detach() 的区别

三、mutex\lock_guard\unique_lock\...

1. mutex

2. recursive_mutex、time_mutex

3. lock_guard

4. unique_lock

四、条件变量 condition_variable

1. wait 类函数

2. notify 类函数 

3. 虚假唤醒问题

五、死锁

六、async 和 future


网上说的不清不楚的,自己写一篇。

一、基本概念

并发,就是几个任务同时进行。单核 CPU 的“并发”是假的,只是任务快速切换给人感觉像并行,实际是串行。多核 CPU 才会真正并行。当然,任务数 > CPU 核数还是会有切换的情况,比如 windows 上的任务(线程)都是上千个。

一个可执行程序执行起来就是一个进程,一个进程只能有一个主线程。线程也不是越多越好,每个线程需要独立的堆栈空间,线程切换也要保存很多中间状态。

如何实现并发?多进程或多线程。但优先使用多线程,因为一个进程中所有线程共享地址空间,还有诸如全局变量、指针、引用都可以在线程间传递,开销远小于多进程。

二、线程创建以及 join() detach() 的区别

线程创建直接使用 thread 库:

thread thread1(fun1);

这样会创建名字叫 thread1 的线程,调用函数 fun1(),注意,立即执行。 不像 java 里还要 start() 哦!

join() 和 detach() 的区别见下面代码注释,不赘述。

#include 
#include 

using namespace std;

void fun1()
{
    cout << "开始执行fun1()" << endl;
}

void fun2(int x)
{
    cout << "开始执行fun2(), " << "x=" << x << endl;
}


int main() {
    cout << "主线程已经开始" << endl;

    // 创建线程,会立即执行
    thread thread1(fun1);
    thread thread2(fun2, 666);

    thread1.join(); // join() 代表汇合,阻塞的作用,主线程在这里等待子线程执行完毕
    thread2.join();

    // 如果使用detach(),则主线程与子线程分离,主线程不会等待子线程
    // 使用detach()主线程执行完了return 0, 程序终止子线程也没了,因此join()阻塞的方式更常用
    // thread1.detach();

    cout << "主线程马上结束" << endl;
    return 0;
}

上述代码执行结果:

主线程已经开始
开始执行fun1()
开始执行fun2(), x=666
主线程马上结束

thread1 是否一定比 thread2 先执行完毕,不一定。thread1 是比 thread2 先启动,但两者是并行执行的,理解这个就理解了多线程核心。

detach() 的方式可能会出现局部变量失效导致线程被内存非法访问的问题,因为主线程执行完了退出,子线程还没执行完,这时一些传入参数的地址在子线程中访问就出问题了。

还有一个 joinable() 函数也有一定作用,可以判断你是否调用过 join() 或者 detach()。

一个线程一个id,查看方式

std::this_thread::get_id()

三、mutex\lock_guard\unique_lock\...

多线程访问同一全局变量时,比如同时读写,可能出现时序问题,因此多线程一般都要加锁。此小节包含互斥量 mutex 普通上锁解锁,以及常用的 lock_guard、unique_lock 等用法举例。

1. mutex

std::mutex 是 C++11 中最基本的互斥量。

  • lock(),加锁(锁住互斥量)。本线程一直拥有该锁直到 unlock()。
  • unlock(),解锁。
  • try_lock(),尝试加锁。如果其他线程已加锁,该线程不阻塞。
#include        // std::cout
#include          // std::thread
#include           // std::mutex, std::lock_guard

using namespace std;

volatile int cnt(0);  // non-atomic counter
mutex mtx;

void ten_thousand_increase() {
    for (int i = 0; i < 10000; ++i) {
        mtx.lock();
        ++cnt;
        mtx.unlock();
    }
}

int main(int argc, const char *argv[]) {
    thread threads[10];
    for (auto &th: threads)
        th = thread(ten_thousand_increase);

    for (auto &th: threads)
        th.join();
    cout << "cnt=" << cnt << endl;

    return 0;
}

程序运行结果:

cnt=100000

该程序创建了10个 thread,每个都会运行 ten_thousand_increase() 函数,如果不加锁,那最后 cnt 可能不是10000,因为可能发现同时写的情况,比如现在 cnt 是100,thread1 和 thread2 同时执行 cnt++,那么 cnt 变成了 101 而不是 102 。加锁就可以保证只能一个线程在写,其他线程在 mtx.lock() 位置等待。

不加锁运行,cnt 结果不一定:

cnt=96925

如果换成 try_lock():

#include        // std::cout
#include          // std::thread
#include           // std::mutex, std::lock_guard

using namespace std;

volatile int cnt(0);  // non-atomic counter
mutex mtx;

void ten_thousand_increase() {
    for (int i = 0; i < 10000; ++i) {
        if (mtx.try_lock()) {   // 仅在其他线程未加锁时执行
            ++cnt;
            mtx.unlock();
        }
    }
}

int main(int argc, const char *argv[]) {
    thread threads[10];
    for (auto &th: threads)
        th = thread(ten_thousand_increase);

    for (auto &th: threads)
        th.join();
    cout << "cnt=" << cnt << endl;

    return 0;
}

运行结果:

cnt=16347

可见,cnt 远小于 10w。

2. recursive_mutex、time_mutex

recursive_mutex 允许同一线程对互斥量递归上锁(多次上锁),来获取多层控制权。lock() 和 unlock() 次数要相同。

time_mutex 一段时间 try_lock_for() 或截止时间 try_lock_until() 加锁,也就是多了时间控制。

3. lock_guard

lock_guard 在实际开发中非常常用。在 lock_guard 类模板的构造函数里,调用了 mutex 的 lock() 成员函数,而在析构函数里,调用了mutex 的 unlock() 成员函数,仅此而已。因为 lock_guard 又是局部变量,出了作用域立马析构,所以经常被使用防止开发者忘记写 unlock()。

#include        // std::cout
#include          // std::thread
#include           // std::mutex, std::lock_guard

using namespace std;

volatile int cnt(0);  // non-atomic counter
mutex mtx;

void ten_thousand_increase() {
    for (int i = 0; i < 10000; ++i) {
        lock_guard lck(mtx);
        ++cnt;
    }
}

int main(int argc, const char *argv[]) {
    thread threads[10];
    for (auto &th: threads)
        th = thread(ten_thousand_increase);

    for (auto &th: threads)
        th.join();
    cout << "cnt=" << cnt << endl;

    return 0;
}

运行结果:

cnt=100000

4. unique_lock

相比 lock_guard,unique_lock 更灵活,但效率差一点,内存占用多一点。

首先,它可完全替代 lock_guard。

第二,它可携带第二个参数 std::unique_lock my_lock (my_mutex, 参数二):

  • std::adopt_lock:表示这个互斥量已经 lock 了,构造函数里不需要再 lock。如果你没有提前 lock 就加这个参数,会报异常。
  • std::try_to_lock:尝试锁,不阻塞。
  • std::defer_lock:表示这个互斥量没有 lock。如果你提前 lock 了还加这个参数,会报异常。

第三,它有精细控制的成员函数:

一文读懂 C++ 并发与多线程 独家原创_第1张图片

比如说,unique_lock 可以提前 unlock,不是必须等到析构。这也是和 lock_guard 的一个重要区别。

四、条件变量 condition_variable

条件变量不是锁,往往跟互斥量同时出现。条件变量是利用线程间共享的全局变量进行同步的一种机制。

condition_variable 对象的某个 wait 类函数被调用时,当前线程阻塞,进入 blocking thread 队列。直到其他线程在 同一个 condition_variable 对象上调用 notify 类函数去唤醒它。

#include          // std::thread
#include           // std::mutex, std::lock_guard
#include  // std::conditional_variable

#include 

using namespace std;

mutex mtx; // 全局互斥量
condition_variable cv; // 全局条件变量
bool ready = false; // 全局标志位

void printId(int id) {
    unique_lock lck(mtx);
    printf("enter thread: %d\n", id);
    // 暂停2秒,方便看规律
    this_thread::sleep_for(chrono::milliseconds(2000));
    while (!ready) {
        cv.wait(lck);
    }
    // 线程被唤醒
    printf("done thread: %d\n", id);

}

void go() {
    unique_lock lck(mtx);
    printf("go()\n");
    ready = true; // 改变全局标志位
    cv.notify_all(); // 唤醒所有线程
}

int main(int argc, const char *argv[]) {
    thread threads[10];
    for (int i = 0; i < 10; ++i)
        threads[i] = thread(printId, i);
    printf("create done.\n");

    go();

    for (auto &th: threads)
        th.join();
    printf("process done.\n");

    return 0;
}

运行结果:

enter thread: 0
create done.
enter thread: 3
enter thread: 4
enter thread: 2
enter thread: 5
enter thread: 6
enter thread: 1
enter thread: 7
enter thread: 8
enter thread: 9
go()
done thread: 9
done thread: 8
done thread: 7
done thread: 4
done thread: 2
done thread: 5
done thread: 3
done thread: 0
done thread: 1
done thread: 6
process done.

从运行结果可以看出,10个线程都阻塞在 wait 处,当调用 notify_all() 时,所有线程被唤醒。

一文读懂 C++ 并发与多线程 独家原创_第2张图片

1. wait 类函数

普通 wait() 函数第一个参数为 unique_lock&,也可以带第二个参数 Predicate,含义等同

while (!pred()) wait(lck);

同样它有 wait_for 和 wait_until。

2. notify 类函数 

ontify_one 随机唤醒一个等待线程。

ontify_all 唤醒所有等待线程。

如果没有等待线程,什么也不做。

3. 虚假唤醒问题

网上一堆误人子弟的说法,实际上 wait 设计的目的是防止一直 while 判断条件变量效率太低,并不会保证一定是 notify 才会唤醒,因此会有虚假唤醒的可能。解决方式就是上面例子中那样:

    while (!ready) {
        cv.wait(lck);
    }

这样即使虚假唤醒,ready 状态没变还是会重新 wait。

五、死锁

举个例子:

#include 
#include 

using namespace std;

mutex mtx;

void B() {
    mtx.lock();
    printf("process B");
    mtx.unlock();
}

void A() {
    mtx.lock();
    printf("process A");
    B();  //这里调用B方法
    mtx.unlock();
}

int main(int argc, const char *argv[]) {
    thread threadA(A);
    thread threadB(B);
    threadA.join();
    threadB.join();
    printf("process done.\n");
    return 0;
}

执行结果:

process A

A 函数里调用 B 函数,但是 mtx 在A函数里加了锁,还没解锁 B 卡在 mtx.lock()。B 等待着 A 解锁,A 也等待 B 解锁才会往下执行。

另外一种就是很常见的忘记 unlock。这种情况当然哪个线程都会卡在 lock 的地方。

死锁的排查方式后面有时间专门写。

六、async 和 future

上面的例子线程入口函数返回值都是 void,也就是线程不会返回结果。那如果入口函数返回值是 int,我们需要线程返回结果该怎么做?可能你会想到不必返回,用一个全局变量。那有没有其他更好的方式?那就要用到函数模板 async 和类模板 future。

async 创建并执行异步任务,future 封装返回值。举例:

#include 
#include 
#include 

using namespace std;

int fun(int input) {
    cout << "fun thread id=" << this_thread::get_id() << endl;
    int output = input + 1;
    return output;
}

int main(int argc, const char *argv[]) {
    cout << "main thread id=" << this_thread::get_id() << endl;
    int input = 666;
    future result = async(fun, input);
    int output = result.get();
    cout << "process done. result=" << output << endl;
    return 0;
}

执行结果:

main thread id=1
fun thread id=2
process done. result=667

更复杂的用法不展开叙述,只要你有个概念这个 future 是干啥的就可以。

你可能感兴趣的:(C和Cpp学习之路,开发语言,c++)