【C++学习笔记】多线程编程(3)

文章目录

【C++学习笔记】多线程编程(3)1. C++11多线程类thread2. 线程互斥2.1 thread线程类库的互斥锁mutex2.2 thread线程类库基于CAS的原子类(无锁操作)

3. 线程同步4. lock_guard和unique_lock的区别std::lock_guardstd::unique_lock

1. C++11多线程类thread

C++11提供了语言级别的线程库,实现了跨平台的多线程编程。实际在底层是调用了Windows上的CreateThread创建线程,或者是Linux下pthread线程库的接口函数pthread_create来创建线程。

#include

#include

#include

using namespace std;

void task1(int i) {

// 线程1睡眠2秒

std::this_thread::sleep_for(std::chrono::seconds(2));

cout << "print: " << i << endl;

}

void task2(int i, string name) {

cout << name << " " << "print: " << i << endl;

}

int main() {

thread t1(task1, 1);

thread t2(task2, 2, "guamo");

t1.join();

t2.join();

return 0;

}

用thread创建线程,可以传入任务函数,该任务函数需要的参数作为参数。 thread t2(task2, 2, "guamo"); task2为任务函数名, 剩下两个参数是传入任务函数的参数。 此外如果在函数task1中没有使用sleep_for 函数会造成下面的结果: 两个线程会争抢资源进行输出,造成乱序。本质是CPU快速切换线程,造成的并发效果。所以也可以看出cout并不是线程安全的。

2. 线程互斥

在上面并不复杂的情况下,可以让其中一个线程睡眠,达到一个看似是线程安全的效果。但是当线程数增加,任务函数的复杂程度变大,我们无法确定该让哪个线程睡眠以及睡眠多久。所以我们需要另一个操作来实现线程安全,也就是线程互斥。

2.1 thread线程类库的互斥锁mutex

下面的代码模拟三个窗口售卖一百张票,由于整数的–操作并不是线程安全的,所以需要用到互斥锁。

// 车票总数是100张

volatile int tickets = 100;

// 全局的互斥锁

std::mutex mtx;

// 线程函数

void sellTicketTask(std::string wndName)

{

while (tickets > 0)

{

// 获取互斥锁资源

mtx.lock();

if (tickets > 0)

{

std::cout << wndName << " 售卖第" << tickets << "张票" << std::endl;

tickets--;

}

// 释放互斥锁资源

mtx.unlock();

// 每卖出一张票,睡眠100ms,让每个窗口都有机会卖票

std::this_thread::sleep_for(std::chrono::milliseconds(100));

}

}

// 模拟车站窗口卖票,使用C++11 线程互斥锁mutex

int main()

{

// 创建三个模拟窗口卖票线程

std::thread t1(sellTicketTask, "车票窗口一");

std::thread t2(sellTicketTask, "车票窗口二");

std::thread t3(sellTicketTask, "车票窗口三");

// 等待三个线程执行完成

t1.join();

t2.join();

t3.join();

// 如果要使用detach,主线程需要睡眠几秒来等待子线程

//t1.detach();

//t2.detach();

//t3.detach();

//std::this_thread::sleep_for(std::chrono::seconds(5));

return 0;

}

join方法使主线程等待子线程,因此三个子线程运行结束后主线程才会结束。而detach方法是将主线程与子线程分离,也就是主线程不会等待主线程。

// 全局的互斥锁

std::mutex mtx;

// 线程函数

void sellTicketTask(std::string wndName)

{

while (tickets > 0)

{

// 获取互斥锁资源

std::lock_guard guard(mtx);

if (tickets > 0)

{

std::cout << wndName << " 售卖第" << tickets << "张票" << std::endl;

tickets--;

}

// 每卖出一张票,睡眠100ms,让每个窗口都有机会卖票

std::this_thread::sleep_for(std::chrono::milliseconds(100));

}

}

此外也可以用lock_guard来代替普通的互斥锁,其优点是不需要手动释放互斥锁,当锁离开作用域,自动释放锁。类似于智能指针。

2.2 thread线程类库基于CAS的原子类(无锁操作)

实际上,上面代码中因为tickets车票数量是整数,因此它的- -操作需要在多线程环境下添加互斥操作,但是mutex互斥锁毕竟比较重,对于系统消耗有些大,C++11的thread类库提供了针对简单类型的原子操作类,如std::atomic_int,atomic_long,atomic_bool等,它们值的增减都是基于CAS操作的,既保证了线程安全,效率还非常高。 下面代码示例开启10个线程,每个线程对整数增加1000次,保证线程安全的情况下,应该加到10000次,这种情况下,可以用atomic_int来实现。

#include

#include // C++11线程库提供的原子类

#include // C++线程类库的头文件

#include

// 原子整形,CAS操作保证给count自增自减的原子操作

std::atomic_int mycount = 0;

// 线程函数

void sumTask()

{

// 每个线程给count加1000次

for (int i = 0; i < 1000; ++i)

{

mycount++;

}

}

int main()

{

// 创建10个线程放在容器当中

std::vector vec;

for (int i = 0; i < 10; ++i)

{

vec.push_back(std::thread(sumTask));

}

// 等待线程执行完成

for (int i = 0; i < vec.size(); ++i)

{

vec[i].join();

}

// 所有子线程运行结束,count的结果每次运行应该都是10000

std::cout << "mycount : " << mycount << std::endl;

return 0;

}

3. 线程同步

多线程在运行过程中,各个线程都是随着OS的调度算法,占用CPU时间片来执行指令做事情,每个线程的运行完全没有顺序可言。但是在某些应用场景下,一个线程需要等待另外一个线程的运行结果,才能继续往下执行,这就需要涉及线程之间的同步通信机制。 线程间同步通信最典型的例子就是生产者-消费者模型,生产者线程生产出产品以后,会通知消费者线程去消费产品;如果消费者线程去消费产品,发现还没有产品生产出来,它需要通知生产者线程赶快生产产品,等生产者线程生产出产品以后,消费者线程才能继续往下执行。

C++11 线程库提供的条件变量condition_variable,就是Linux平台下的Condition Variable机制,用于解决线程间的同步通信问题,下面通过代码演示一个生产者-消费者线程模型

#include // std::cout

#include // std::thread

#include // std::mutex, std::unique_lock

#include // std::condition_variable

#include

// 定义互斥锁(条件变量需要和互斥锁一起使用)

std::mutex mtx;

// 定义条件变量(用来做线程间的同步通信)

std::condition_variable cv;

// 定义vector容器,作为生产者和消费者共享的容器

std::vector vec;

// 生产者线程函数

void producer()

{

// 生产者每生产一个,就通知消费者消费一个

for (int i = 1; i <= 10; ++i)

{

// 获取mtx互斥锁资源

std::unique_lock lock(mtx);

// 如果容器不为空,代表还有产品未消费,等待消费者线程消费完,再生产

while (!vec.empty())

{

// 判断容器不为空,进入等待条件变量的状态,释放mtx锁,

// 让消费者线程抢到锁能够去消费产品

cv.wait(lock);

}

vec.push_back(i); // 表示生产者生产的产品序号i

std::cout << "producer生产产品:" << i << std::endl;

/*

生产者线程生产完产品,通知等待在cv条件变量上的消费者线程,

可以开始消费产品了,然后释放锁mtx

*/

cv.notify_all();

// 生产一个产品,睡眠100ms

std::this_thread::sleep_for(std::chrono::milliseconds(100));

}

}

// 消费者线程函数

void consumer()

{

// 消费者每消费一个,就通知生产者生产一个

for (int i = 1; i <= 10; ++i)

{

// 获取mtx互斥锁资源

std::unique_lock lock(mtx);

// 如果容器为空,代表还有没有产品可消费,等待生产者生产,再消费

while (vec.empty())

{

// 判断容器为空,进入等待条件变量的状态,释放mtx锁,

// 让生产者线程抢到锁能够去生产产品

cv.wait(lock);

}

int data = vec.back(); // 表示消费者消费的产品序号i

vec.pop_back();

std::cout << "consumer消费产品:" << data << std::endl;

/*

消费者消费完产品,通知等待在cv条件变量上的生产者线程,

可以开始生产产品了,然后释放锁mtx

*/

cv.notify_all();

// 消费一个产品,睡眠100ms

std::this_thread::sleep_for(std::chrono::milliseconds(100));

}

}

int main()

{

// 创建生产者和消费者线程

std::thread t1(producer);

std::thread t2(consumer);

// main主线程等待所有子线程执行完

t1.join();

t2.join();

return 0;

}

条件变量condition_variable需要配合unique_lock 来使用。在生产者消费者模式中,如果没有使用线程同步机制,会产生一个问题:如果消费线程比生产线程执行得快,那么就会导致容器内的元素不够消费,所以需要一个机制:当容器内的元素不够时,消费者需要通知生产者去生产;当元素充足时,生产者需要通知消费者去消费。

下面的代码是三个线程轮流打印a, b, c.

#include

#include

#include

#include

#include

std::mutex mtx;

std::condition_variable cv;

char arr[] = {'a', 'b', 'c'};

char message = 'a';

void test(int i){

for (int j = 0; j < 10; ++j) {

std::unique_lock lk(mtx);

// 当while条件语句成立,执行别的线程

while (message != arr[i]) {

cv.wait(lk);

}

std::cout << arr[i];

message = arr[(i + 1) % 3];

// 通知别的线程去执行

cv.notify_all();

}

}

int main(int argc, char **argv)

{

std::vector l;

for (int i = 0; i < 3; ++i) {

l.push_back(std::thread(test, i));

}

for (int i = 0; i < 3; ++i) {

l[i].join();

}

}

while (message != arr[i]) { cv.wait(lk); } 等价于 cv.wait(lk, [=]{ return message == arr[i];});表达的意思是如果满足while中的条件, 该线程就阻塞,并且释放锁,这样就会转移到别的线程运行。

4. lock_guard和unique_lock的区别

lock_guard:不能作为函数参数和返回值使用。 unique_lock:可以在函数调用过程中使用。 std::unique_lock 和 std::lock_guard 都是 C++ 标准库中的互斥锁包装器,用于简化互斥锁的管理,但它们提供了不同层次的灵活性。

std::lock_guard

std::lock_guard 是一个轻量级的互斥锁包装器,它提供了作用域内的互斥锁所有权管理。当 std::lock_guard 对象被创建时,它会尝试获取所包装的互斥锁(mutex)的所有权(即锁定互斥锁),并在 std::lock_guard 对象被销毁时自动释放互斥锁(即解锁)。它不能显式地解锁和重新锁定互斥锁,因此适用于那些生命周期明确且作用域内需要简单锁管理的场景。

std::unique_lock

std::unique_lock 是一个更加灵活的互斥锁包装器。与 std::lock_guard 相比,它提供了更多的功能,如:

可以在不同的时间点手动锁定和解锁互斥锁。可以被移动(move),因此可以从函数返回或传递给其他函数。支持条件变量(std::condition_variable),可以用于等待某个条件。提供了多种构造函数,支持延迟锁定(即创建时不立即锁定),尝试锁定(try-lock)等策略。

由于 std::unique_lock 提供了更大的灵活性,它也略微复杂和重量级一些。如果你需要在某个作用域内多次锁定和解锁互斥锁,或者与条件变量一起使用,那么 std::unique_lock 是更合适的选择。如果你只需要简单地保护某个作用域内的代码段,并且对性能有严格要求,那么 std::lock_guard 可能是更好的选择。

简单来说:

使用 std::lock_guard 当你只需要简单的作用域锁定。使用 std::unique_lock 当你需要更复杂的锁定策略,或者需要与条件变量一起使用。

在选择使用哪一个时,应该根据实际需求和上下文来决定。 以上内容来自GPT-4

推荐链接

评论可见,请评论后查看内容,谢谢!!!评论后请刷新页面。