C++11多线程基础

目录

1.多线程基本概念

2.多线程基本操作

2.1创建线程

2.2互斥量

2.3条件变量

2.4原子变量

2.5异步线程


1.多线程基本概念

线程:线程是操作系统能够进行CPU调度的最小单位,它被包含在进程之中,一个进程可包含单个或者多个线程。可以用多个线程去完成一个任务,也可以用一个进程去完成一个任务,则是常说的主线程。

多线程并发:多线程是实现并发(双核的真正并行或者单核机器的任务切换都叫并发)的一种手段,多线程并发即多个线程同时执行,一般而言,多线程并发就是把任务的不同功能交由多个函数分别实现,创建多个线程,每个线程执行一个函数,一个任务就这样同时分由不同线程执行了。

使用场合:运行越多的线程,操作系统需要为每个线程分配独立的栈空间,需要越多的上下文切换,这会消耗很多操作系统资源,如果在线程上的任务完成得很快,那么实际执行任务的时间要比启动线程的时间小很多,所以在某些时候,增加一个额外的线程实际上会降低,而非提高应用程序的整体性能,此时收益就比不上成本。

2.多线程基本操作

2.1创建线程

首先导入#include<thread>---用于创建线程

#include <iostream>
#include <thread>
using namespace std;
using namespace chrono;

void fun1()
{
    cout << "fun1 id:"<<this_thread::get_id()<< endl; //  获取当前线程id
    this_thread::sleep_for(milliseconds(100)); //需要导入using namespace chrono;
    cout << "Fun1 Sleep 100" <<endl;
    Sleep(1000);//需导入#include<windows.h>
    cout << "Fun1 Sleep 1000" << endl;
}

void fun2(int n)
{
    cout << "fun2 id:"<< this_thread::get_id() << endl;
    Sleep(n);
    cout << "Fun2 Sleep 100" << endl;
}


int main()
{
    thread t1(fun1);
    thread t2(fun2,100);

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

    return 0;

}

join()就是阻塞线程,直到创建线程函数执行完毕,让Main主线程等待一下创建的线程,免得函数还在跑,程序就直接结束了。调用join的行为还会清理线程相关储存部分,所以只能调用一次,能否调用join可用joinable来判断。

如果不想阻塞在这里就将join()换成使用线程的detach()方法,将线程与线程对象分离,主线程就可以继续运行下去,并且不会造成影响。但之后不能访问已销毁的变量所以对于指针和引用要谨慎处理,可将数据复制到线程中,使线程函数的功能齐全能减小此风险。

2.2互斥量

这样比喻:单位上有一台打印机(共享数据a),你要用打印机(线程1要操作数据a),同事老王也要用打印机(线程2也要操作数据a),但是打印机同一时间只能给一个人用,此时,规定不管是谁,在用打印机之前都要向领导申请许可证(lock),用完后再向领导归还许可证(unlock),许可证总共只有一个,没有许可证的人就等着在用打印机的同事用完后才能申请许可证(阻塞,线程1lock互斥量后其他线程就无法lock,只能等线程1unlock后,其他线程才能lock)。那么,打印机就是共享数据,访问打印机的这段代码就是临界区,这个必须互斥使用的许可证就是互斥量

互斥量是为了解决数据共享过程中可能存在的访问冲突的问题。这里的互斥量保证了使用打印机这一过程不被打断。

互斥量怎么使用?

首先需要#include<mutex>;

然后需要实例化std::mutex对象;

最后需要在进入临界区之前对互斥量加锁,退出临界区时对互斥量解锁。(需要互斥访问共享资源的那段代码称为临界区)

#include<iostream>
#include<thread>
#include<mutex>
using namespace std;
mutex m;//实例化m对象
int a = 0;
void fun1()
{
    m.lock();
    cout << "fun1函数正在改写a" << endl;
    cout << "现在a为" << ++a << endl;
    m.unlock();
}

void fun2()
{
    m.lock();
    cout << "fun2函数正在改写a" << endl;
    cout << "现在a为" << ++a << endl;
    m.unlock();
}
int main()
{
    thread t1(fun1);
    thread t2(fun2);
    t1.join();
    t2.join();
    return 0;
}

 

这种方式需要在每个函数的出口或者异常都去调用unlock,如果忘记unlock(),将导致锁无法释放,而使用lock_guard或者unique_lock则能避免忘记解锁带来的问题。

在lock_guard对象构造时,传入的mutex对象(即它所管理的mutex对象)会被当前线程锁住。在lock_guard对象被析构时,它所管理的mutex对象会自动解锁。lock_guard对象并不负责管理mutex对象的生命周期,lock_guard对象只是简化了mutex对象的上锁和解锁操作,方便线程对互斥量上锁。即在某个lock_guard对象的生命周期内,它所管理的锁对象会一直保持上锁状态;而lock_guard的生命周期结束之后,它所管理的锁对象会被解锁。程序员可以非常方便地使用lock_guard,而不用担心异常安全问题。通过使用{}来调整作用域范围,可使得互斥量m在合适的地方被解锁。

lock_gurad也可以传入第二个参数adopt_lock标识时,表示构造函数中不再进行互斥量锁定,因此此时需要提前手动锁定

#include<iostream>
#include<thread>
#include<mutex>
using namespace std;
mutex m;//实例化m对象
int a = 0;
void fun1()
{
    m.lock();//手动锁定
    lock_guard<mutex> g1(m,adopt_lock);
    cout << "fun1函数正在改写a" << endl;
    cout << "现在a为" << ++a << endl;
}//自动解锁

void fun2()
{
    lock_guard<mutex> g2(m);//自动上锁
    cout << "fun2函数正在改写a" << endl;
    cout << "现在a为" << ++a << endl;
}//自动解锁
int main()
{
    thread t1(fun1);
    thread t2(fun2);
    t1.join();
    t2.join();
    return 0;
}

 

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

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

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

#include<iostream>
#include<thread>
#include<mutex>
using namespace std;
mutex m;//实例化m对象
int a = 0;
void fun1()
{
    unique_lock<mutex> g1(m,defer_lock);//始化了一个没有加锁的mutex
    g1.lock();//手动加锁,注意,不是m.lock();注意,不是m.lock(),m已经被g1接管了;
    cout << "fun1函数正在改写a" << endl;
    g1.unlock();//临时解锁
    g1.lock();
    cout << "现在a为" << ++a << endl;    
}//自动解锁

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

int main()
{
    thread t1(fun1);
    thread t2(fun2);
    t1.join();
    t2.join();
    return 0;
}

锁的粒度用来描述通过一个锁保护着的数据量大小。由于这个被保护的数据量规模不太好定量,所以这是一个hand-waving term。在这个问题上一般粗略的分为两类:细粒度锁(a fine-grained lock)和粗粒度锁(a coarse-grained lock)。一个细粒度锁能够保护较小的数据量,一个粗粒度锁能够保护较多的数据量。

比如lock_guard和unique_lock相比,粒度更粗,他在构造实例时上锁,在作用域(函数)结束时解锁,导致很多和invariants无关的操作也被上锁。与之相反,unique_lock对粒度的控制更精细:

互斥量所有权转移

互斥量可以通过移动语义在不同的实例中传递。

mutex mu;
unique_lock<mutex> locker(mu);
unique_lock<mutex> locker2 = move(locker);

2.3条件变量

condition_variable 是为了解决死锁而生的。当互斥操作不够用而引入的。比如,线程可能需要等待某个条件为真才能继续执行,而一个忙等待循环中可能会导致所有其他线程都无法进入临界区使得条件为真时,就会发生死锁。所以,condition_variable实例被创建出现主要就是用于唤醒等待线程从而避免死锁。condition_variable的 notify_one()用于唤醒一个线程;notify_all() 则是通知所有线程。

#include<deque>
#include<condition_variable>
#include<mutex>
#include<thread>
#include<iostream>

using namespace std;
using namespace chrono;

mutex mu;
deque<int> q;
condition_variable cond;

void fun1() 
{
	int cnt = 10;
	while(cnt>0)
	{
		unique_lock<mutex> locker(mu);
		q.push_front(cnt);
		locker.unlock();//必须先解锁,若在跳出{}后解锁的话,fun2无法使用队列q
		cond.notify_one();  //发送任务达成信号,会激活一个等待这个条件的线程
		//如果存在多个像线程2这样的等待线程,想要同时激活 调用cond.notify_all()
		this_thread::sleep_for(seconds(1));
		cnt--;
	}
}


void fun2() 
{
	int data = 0;
	while (data != 1)
    {
		unique_lock<mutex> locker(mu);
		cond.wait(locker, []() {return !q.empty(); });  
		//这条语句会将线程2休眠,直到线程1调用nodify_one 才会激活
		//条件变量可以避免线程2的很多无用的循环,或者说条件变量可以部分控制线程之间的访问顺序
		//为什么wait函数需要locker作为它的参数呢?
		//因为在wait之前,互斥对象mu已经被线程2锁住,线程休眠的时候线程里不能有东西被锁住
		//wait会先解锁互斥对象mu,然后休眠;激活时对mu重新加锁
		//由于需要重复的加解锁,所以只能使用unique_lock,而不能使用lock_guard
		//此外线程2可能被自己激活,称之为伪激活。如果碰到伪激活不想让线程2运行,需要传入一个检查条件
		//如果q不为空,才会激活,q为空就不会激活
		data = q.back();
		q.pop_back();
		cout << "t2 get a value from t1:" << data << endl;
	}
}

int main() {
	thread t1(fun1);
	thread t2(fun2);
	t1.join();
	t2.join();
	return 0;
}

2.4原子变量

在新标准C++11,引入了原子操作的概念,原子操作更接近内核,并通过这个新的头文件提供了多种原子操作数据类型,例如,atomic_bool,atomic_int等等,如果我们在多个线程中对这些类型的共享资源进行操作,编译器将保证这些操作都是原子性的,也就是说,确保任意时刻只有一个线程对这个资源进行访问,编译器将保证,多个线程访问这个共享资源的正确性。从而避免了锁的使用,提高了效率。

#include<iostream>
#include<thread>
#include<atomic>
#include<mutex>
using namespace std;
mutex m;//实例化m对象
atomic<int> a(0);
void fun1()
{  
    cout << "fun1函数正在改写a" << endl;
    cout << "现在a为" << ++a << endl;
}

void fun2()
{
    cout << "fun2函数正在改写a" << endl;
    cout << "现在a为" << ++a << endl;
}

int main()
{
    thread t1(fun1);
    thread t2(fun2);
    t1.join();
    t2.join();
    return 0;
}

2.5异步线程

async与future:

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

相当于你去办政府办业务(主线程),把资料交给了前台,前台安排了人员去给你办理(async创建子线程),前台给了你一个单据(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;
}

shared_future

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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值