目录
一.thread构造函数
二.移动构造,移动赋值
小结
三.获取线程id的方法
四.thread与lambda表达式联用
五.Mutexs的总览
六.互斥锁
七.Locks的总览
八. 条件变量总览
九.条件变量的wait和notify
十.典型例题
十一.原子类
十二.智能指针和单例模式的线程安全问题
一.thread构造函数
void Print(int n)
{for (int i = 0; i < n; i++){cout << "I am a thread: " << i << endl;}
}
int main()
{thread t(Print, 1000);t.join();return 0;
}
如果想要传递引用,需要使用ref()函数将对象包装成引用以便向下传递。原因在于参数并不是直接传递给执行的方法,中间经过了thread的构造函数,为了完美转发保持属性而产生副作用,生成了一个拷贝的临时对象,则执行方法中引用的是临时对象,非const左值引用来引用临时对象是不被允许的。(简单说一说,底层非常复杂,不值得深究)。
如果用const引用来接收就不必使用ref
bind也是如此,如果想要传递引用就必须ref
二.移动构造,移动赋值
thread类的拷贝构造被禁用,但是有移动构造和移动赋值,以应对需要同时创建多个线程的场景
void Print(int j, int n)
{for (int i = j; i < n; i++){cout << "thread id: " << this_thread::get_id() << ", " << i << endl;}
}
int main()
{const int threadNum = 5;vector<thread> threads(threadNum);for (int i = 0; i < threads.size(); i++){threads[i] = thread(Print, 0, 1000); //匿名对象右值,调用移动赋值,减少拷贝}for (int i = 0; i < threads.size(); i++){threads[i].join();}return 0;
}//移动构造:
/*
for (int i = 0; i < threadNum; i++)
{threads.emplace_back(thread(Print, 0, 1000)); //emplace_back调用移动构造}
*/
思考:thread禁用了拷贝构造,有办法把用一个thread对象构造另一个对象吗?
//使用移动构造:副作用:t1将不可用
thread t1(Print, 0, 1000);
thread t2(move(t1));
小结
创建线程的方法:
- 用有参的构造传递方法和参数
- 创建多个线程用容器集中管理时,使用移动构造或者移动赋值
- 将一个线程的资源转移给另一个线程,move+移动构造
三.获取线程id的方法
//线程的执行方法中获取:
this_thread::get_id();
//线程外获取:
t.get_id(); //t是创建的线程对象
四.thread与lambda表达式联用
lambda表达式的优势在于能捕获局部的变量,而无需传参
int x = 0;
int main()
{int n1 = 10000;int n2 = 10000;thread t1([n1](){for (int i = 0; i < n1; i++){x++;}});thread t2([n2](){for (int i = 0; i < n2; i++){x++;}});t1.join();t2.join();cout << x << endl;return 0;
}//注意:以上代码有线程安全问题
五.Mutexs的总览
六.互斥锁
mutex不支持拷贝,只能引用
int x = 0;
void Add(int n, mutex& mtx) //这里的互斥锁只能用引用接收,因为mutex禁用了拷贝构造
//mtx不能加const,否早无法lock,unlock
{mtx.lock();for (int i = 0; i < n; i++){x++;}mtx.unlock();
}
int main()
{mutex mtx;thread t1(Add, 10000, ref(mtx)); //想要传锁必须使用ref把mutex包装成一个引用对象,thread t2(Add, 10000, ref(mtx));
//因为mtx不是直接传给Add的,中间经过thread的构造函数,
//最终Add引用的是mtx的拷贝(完美转发保持属性的同时生成了一个拷贝的临时对象,简单说一说,不必深究)t1.join();t2.join();cout << x << endl;return 0;
}
七.Locks的总览
八. 条件变量总览
九.条件变量的wait和notify
十.典型例题
int main()
{//让t1和t2两个线程,从1开始,交替打印奇数和偶数int x = 1;mutex mtx;condition_variable cond;thread t1([&](){while (true){unique_lock<mutex> lck(mtx);while (x % 2 == 0){cond.wait(lck);}cout << "thread 1:" << x << endl;x++;Sleep(1000);cond.notify_one();}});thread t2([&](){while (true){unique_lock<mutex> lck(mtx);while (x % 2 == 1){cond.wait(lck);}cout << "thread 2:" << x << endl;x++;Sleep(1000);cond.notify_one();}});t1.join();t2.join();return 0;
}
十一.原子类
临界区操作非常少的情况,不适合加互斥锁。因为这会导致线程状态频繁切换,不断地阻塞和唤醒。此时就可以使用C++多线程库提供的atomic类来封装内置类型,使用atomic类提供的“原子”接口。这里的原子二字打了引号,因为严格来说它并非真正意义上的原子,只不过表现出来的是原子性的效果。当然日常交流中说它是原子操作也没问题。
首先需要明确一个概念:被保护后的临界区,也并不是原子的。原子操作是不可中断的操作,要么全部执行成功,要么全部不执行,没有中间状态。而临界区操作涉及多个指令或操作步骤,收线程调度影响,可能会被中断。只不过采用互斥的机制保护,使得任意时刻只有一个线程进入临界区,从而避免了数据不一致问题,达到了原子性的效果。
CAS操作:CAS即Comparre and Swap,它是CPU提供的原子操作。这才是真正意义上的原子操作,它由硬件支持,X86下对应的是CMPXCHG 汇编指令。
这个操作所做的工作是:先比较目标内存的值是否和预期的值一样(Compare),如果一样就进行赋值(Swap->把寄存器的值和目标内存的值进行交换),如果不一样就啥都不做
bool compare_and_swap (int *addr, int oldval, int newval) {if ( *addr != oldval ) {return false;}*addr = newval;return true; }
atomic的接口底层就是使用的CAS操作。
例如:
int a = 0; a++;
atomic<int> a(0); a++;
atomic或者说CAS操作适用于对公共资源操作非常简单的情况,例如对一个整型变量进行++,那么CAS的成功率就比较高,相较于加锁,可以减少线程状态切换的开销。但是如果操作比较多,还是加锁合适。
十二.智能指针和单例模式的线程安全问题
智能指针是线程安全的吗?
首先,unique_ptr通常用于独占所有权的情况,它被设计为独占所有权的智能指针,通常情况下只能由一个线程拥有和访问。因此,不建议多个线程使用同一个unique_ptr,其内部并没有提供线程安全保护机制,使用结果是未定义的。
其次,shared_ptr中的引用计数是会被公共访问的资源,涉及到++,--,判断等操作。shared_ptr内部提供了保护措施,要么是加锁保护,要么使用atomic这样的原子操作。
所以,虽然unique_ptr没有保护机制,但是人家设计的初衷就是用于独占资源的情况,不按要求使用那是你自讨苦吃,而shared_ptr内部有保护机制,可以供多个线程使用。
综上,智能指针是线程安全的,但是智能指针管理的资源不是线程安全的,需要用户自己保护。
单例模式是线程安全的吗?
饿汉模式在main函数之前就创建了对象,没有线程安全问题。但是懒汉模式就有,调用获取实例的接口,首先会判断指向实例的指针是否为空,如果为空就创建实例。那么就有可能多个线程同时通过if判断,而创建出多个实例。
所以需要在判断之前加锁保护。
但是线程安全问题仅仅是第一次调用接口时才会出现,后续每次获取实例都要申请锁太消耗时间了,所以外层再套一个if判断,如果是空指针就加锁,否则直接返回。
其实懒汉模式还有另一种简单巧妙的实现方式,不把实例创建在堆区,而是创建静态对象。
这份代码在C++11之后是线程安全的,静态对象不管如何只会初始化一次,初始化静态对象是原子的。
但C++11之前它是线程不安全的,静态对象在多线程情况下初始化动作式未定义的。