C/C++多线程编程

参考链接:
初识C++多线程编程
C++多线程编程学习材料
C++11 并发指南系列

get_id() 获取线程 ID,返回一个类型为 std::thread::id 的对象
join() 等待——主线程暂停,等待子线程运行结束
joinable() 判断线程是否可以加入等待
detach() 分离线程
swap() 线程交换
native_handle() 获取线程句柄

多个线程访问同一资源时,为了保证数据一致性,需要使用互斥锁
std::mutex
lock
unlock

使用mutex的lock()和unlock()可以保证共享资源一致性,但是如果在调用lock()之后unlock()之前,程序出现异常,将会导致无法解锁,使得其中需要此锁的线程阻塞。
解决该问题的方法是使用获取资源即初始化(Resource Acquisition Is Initialization,RAII)技术,c++库提供了std::lock_guardstd::unique_lock来实现。
std::lock_guard 在构造函数中进行加锁,析构函数中进行解锁,不需要手动释放锁。
std::unique_lock:允许延迟锁定、锁定的有时限尝试、递归锁定、所有权转移和与条件变量一同使用。unique_lock对象是可以进行交换的,而lock_guard不行;且std::unique_lock可以调用lock()和unlock()加解锁,而lock_guard不可以。unique_lock比lock_guard使用更加灵活,功能更加强大,也因此需要付出更多的时间、性能成本,使用时应视具体情况使用std::unique_lock和std::lock_guard。 基本使用方法同std::lock_guard。
如果当前互斥量被当前调用的线程锁住,则会产生死锁(deadlock)


分页


0. C语言多线程与C++多线程表示方法有所区别,现将其分别介绍

参考文献一

1. C++11的多线程表示

1.1 创建线程

void proc(int a)
{
	cout<<"son thread "<<a<<endl;
	cout<<"son thread id is " << this_thread::get_id()<<endl;
}
int main()
{
	cout<<"I am main thread"<<endl;
	int a = 9;
	thread th2(proc, a);
	cout<<"the main thread id is "<<th2.get_id()<<endl;
	th2.join();
	// std::shared_ptr<int> p(new int[10], [](int *){delete []p});
    return 0;
}

1.2 互斥量的使用

1.2.1 lock&&unlock

在互斥量锁定到互斥量解锁之间的代码叫做临界区,临界区范围应该尽可能的小,即lock互斥量后应该尽早unlock

mutex m;

void proc1(int a)
{
	m.lock();
	cout << "proc1函数正在改写a" << endl;
    cout << "原始a为" << a << endl;
    cout << "现在a为" << a + 2 << endl;
	m.unlock();
}

void proc2(int a)
{
    m.lock();
    cout << "proc2函数正在改写a" << endl;
    cout << "原始a为" << a << endl;
    cout << "现在a为" << a + 1 << endl;
    m.unlock();
}

int main()
{
	int a = 0;
	thread pro1(proc1, a);
	thread pro2(proc2, a);
	pro1.join();
	pro2.join();
	return 0;
}
1.2.2 lock_guard

创建即加锁,作用域结束解锁。

通过设定作用域,使得std::lock_guard在合适的地方被析构,通过使用{}来调整作用域范围,可使得互斥量m在合适的地方被解锁:

mutex m;//实例化m对象,不要理解为定义变量
void proc1(int a)
{
    lock_guard<mutex> g1(m);//用此语句替换了m.lock();lock_guard传入一个参数时,该参数为互斥量,此时调用了lock_guard的构造函数,申请锁定m
    cout << "proc1函数正在改写a" << endl;
    cout << "原始a为" << a << endl;
    cout << "现在a为" << a + 2 << endl;
}//此时不需要写m.unlock(),g1出了作用域被释放,自动调用析构函数,于是m被解锁

void proc2(int a)
{
    {
        lock_guard<mutex> g2(m);
        cout << "proc2函数正在改写a" << endl;
        cout << "原始a为" << a << endl;
        cout << "现在a为" << a + 1 << endl;
    }//通过使用{}来调整作用域范围,可使得m在合适的地方被解锁
    
    cout << "作用域外的内容3" << endl;
    cout << "作用域外的内容4" << endl;
    cout << "作用域外的内容5" << endl;
}

int main()
{
    int a = 0;
    thread pro1(proc1, a);
    thread pro2(proc2, a);
    pro1.join();
    pro2.join();
    return 0;
}

std::lock_gurad也可以传入两个参数,第二个参数为adopt_lock标识时,表示构造函数中不再进行互斥量锁定,因此此时需要提前手动锁定,可以有选择性控制作用范围。

void proc1(int a)
{
    m.lock();//手动锁定
    lock_guard<mutex> g1(m,adopt_lock);
    cout << "proc1函数正在改写a" << endl;
    cout << "原始a为" << a << endl;
    cout << "现在a为" << a + 2 << endl;
}//自动解锁
1.2.3 unique_lock

std::unique_lock类似于lock_guard,只是std::unique_lock用法更加丰富,同时支持std::lock_guard()的原有功能。 使用std::lock_guard后不能手动lock()与手动unlock();使用std::unique_lock后可以手动lock()与手动unlock(); std::unique_lock的第二个参数,除了可以是adopt_lock,还可以是try_to_lockdefer_lock;

try_to_lock: 尝试去锁定,得保证锁处于unlock的状态,然后尝试现在能不能获得锁;尝试用mutx的lock()去锁定这个mutex,但如果没有锁定成功,会立即返回,不会阻塞在那里,并继续往下执行

defer_lock: 始化了一个没有加锁的mutex;

mutex m;
void proc1(int a)
{
    unique_lock<mutex> g1(m, defer_lock);//始化了一个没有加锁的mutex
    cout << "不拉不拉不拉" << endl;
    g1.lock();//手动加锁,注意,不是m.lock();注意,不是m.lock(),m已经被g1接管了;
    cout << "proc1函数正在改写a" << endl;
    cout << "原始a为" << a << endl;
    cout << "现在a为" << a + 2 << endl;
    g1.unlock();//临时解锁
    cout << "不拉不拉不拉"  << endl;
    g1.lock();
    cout << "不拉不拉不拉" << endl;
}//自动解锁

void proc2(int a)
{
    unique_lock<mutex> g2(m,try_to_lock);//尝试加锁一次,但如果没有锁定成功,会立即返回,不会阻塞在那里,且不会再次尝试锁操作。
    if(g2.owns_lock()){//锁成功
        cout << "proc2函数正在改写a" << endl;
        cout << "原始a为" << a << endl;
        cout << "现在a为" << a + 1 << endl;
    }else{//锁失败则执行这段语句
        cout <<""<<endl;
    }
}//自动解锁

int main()
{
    int a = 0;
    thread proc1(proc1, a);
    thread proc2(proc2, a);
    proc1.join();
    proc2.join();
    return 0;
}

std::unique_lock还可以在对象间进行所有权转移,std::mutex对象的所有权不需要手动转移给std::unique_lock , std::unique_lock对象实例化后会直接接管std::mutex。

mutex m;
{  
    unique_lock<mutex> g2(m,defer_lock);
    unique_lock<mutex> g3(move(g2));//所有权转移,此时由g3来管理互斥量m
    g3.lock();
    g3.unlock();
    g3.lock();
}
1.2.4 condition_variable

#include<condition_variable>

std::condition_variable类搭配std::mutex类来使用,std::condition_variable对象(std::condition_variable cond;)的作用不是用来管理互斥量的,它的作用是用来同步线程,它的用法相当于编程中常见的flag标志.A、B两个人约定notify_one为行动号角,A就等着(调用wait(),阻塞),只要B一调用notify_one,A就开始行动(不再阻塞)。

  • wait(locker) : wait函数需要传入一个std::mutex(一般会传入std::unique_lock对象),即上述的locker。wait函数会自动调用 locker.unlock() 释放锁(因为需要释放锁,所以要传入mutex)并阻塞当前线程,本线程释放锁使得其他的线程得以继续竞争锁。一旦当前线程获得notify(通常是另外某个线程调用 notify_* 唤醒了当前线程),wait() 函数此时再自动调用 locker.lock()上锁。
  • cond.notify_one(): 随机唤醒一个等待的线程
  • cond.notify_all(): 唤醒所有等待的线程

1.3 异步线程

#include<future>

1.3.1 async和future

std::async是一个函数模板,用来启动一个异步任务,它返回一个std::future类模板对象,future对象起到了占位的作用(记住这点就可以了),占位是什么意思?就是说该变量现在无值,但将来会有值(好比你挤公交瞧见空了个座位,刚准备坐下去就被旁边的小伙给拦住了:“这个座位有人了”,你反驳道:”这不是空着吗?“,小伙:”等会人就来了“),刚实例化的future是没有储存值的,但在调用std::future对象的get()成员函数时,主线程会被阻塞直到异步线程执行结束,并把返回结果传递给std::future,即通过FutureObject.get()获取函数返回值。

相当于你去办政府办业务(主线程),把资料交给了前台,前台安排了人员去给你办理(std::async创建子线程),前台给了你一个单据(std::future对象),说你的业务正在给你办(子线程正在运行),等段时间你再过来凭这个单据取结果。过了段时间,你去前台取结果(调用get()),但是结果还没出来(子线程还没return),你就在前台等着(阻塞),直到你拿到结果(子线程return),你才离开(不再阻塞)。

#include <iostream>
#include <thread>
#include <mutex>
#include<future>
#include<windows.h>
using namespace std;
double t1(const double a, const double b)
{
 double c = a + b;
 Sleep(3000);//假设t1函数是个复杂的计算过程,需要消耗3秒
 return c;
}

int main() 
{
 double a = 2.3;
 double b = 6.7;
 future<double> fu = async(t1, a, b);//创建异步线程线程,并将线程的执行结果用fu占位;
 cout << "正在进行计算" << endl;
 cout << "计算结果马上就准备好,请您耐心等待" << endl;
 cout << "计算结果:" << fu.get() << endl;//阻塞主线程,直至异步线程return
 //cout << "计算结果:" << fu.get() << endl;//取消该语句注释后运行会报错,因为future对象的get()方法只能调用一次。
 return 0;
}
1.3.2 shared_future

std::futurestd::shard_future的用途都是为了占位,但是两者有些许差别。std::future的get()成员函数是转移数据所有权;std::shared_future的get()成员函数是复制数据。 因此: future对象的get()只能调用一次;无法实现多个线程等待同一个异步线程,一旦其中一个线程获取了异步线程的返回值,其他线程就无法再次获取。 std::shared_future对象的get()可以调用多次;可以实现多个线程等待同一个异步线程,每个线程都可以获取异步线程的返回值。

1.4 原子类型atomic<>

std::atomic<>用来定义一个自动加锁解锁的共享变量,供多个线程访问而不发生冲突。

2 代码实现

2.1 生产者消费者问题

生产者消费者模型是经典的多线程并发模型。生产者用于生产数据,生产一个就往共享数据区存一个,如果共享数据区已满,生产者就暂停生产;消费者用于消费数据,一个一个从共享数据区存取,如果共享数据区为空的话,消费者就暂停取数据,且生产者和消费者之间不能直接交互。

单生产者、单消费者模型

/*
生产者消费者问题
*/
#include <iostream>
#include <deque>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <windows.h>
using namespace std;

deque<int> q;
mutex mu;
condition_variable cond;
int c = 0;//缓冲区的产品个数

void producer() { 
    int data1;
    while (1) {//通过外层循环,能保证生产不停止
        if(c < 3) {//限流
            {
                data1 = rand();
                unique_lock<mutex> locker(mu);//锁
                q.push_front(data1);
                cout << "存了" << data1 << endl;
                cond.notify_one();  // 通知取
                ++c;
            }
            Sleep(500);
        }
    }
}

void consumer() {
    int data2;//data用来覆盖存放取的数据
    while (1) {
        {
            unique_lock<mutex> locker(mu);
            while(q.empty())
                cond.wait(locker);  // wait()阻塞前先会解锁,解锁后生产者才能获得锁来放产品到缓冲区;生产者notify后,将不再阻塞,且自动又获得了锁。
            data2 = q.back();//取的第一步
            q.pop_back();//取的第二步
            cout << "取了" << data2<<endl;
            --c;
        }
        Sleep(1500);
    }
}

int main() {
    thread t1(producer);
    thread t2(consumer);
    t1.join();
    t2.join();
    return 0;
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值