基本概念
并发:一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行,但任一个时刻点上只有一个程序在处理机上运行。
进程:可执行程序运行,便创建了一个进程。
线程:
- 就是代码的执行通路
- 每个进程都有一个主线程,且唯一,与进程一起产生、结束。
- 多线程,不是越多越好,每个线程需要一个独立的堆栈空间,线程之间切换需切要保存很多中间的状态,切换过多降低程序的运行时间。
开启一个线程
整个进程是否执行完毕,取决于主线程是否执行完毕,即如果主线程执行完毕整个进程也结束,此时如果子线程没有结束,也会被强行结束。因此如果需要让子线程一保持正常运行,主线程一般会保持一致运行,但是也有例外(即detach)。
开启线程
1.需要用到的头文件: #include <thread>,since supported by c++11;
2.线程,就是开辟一个新的通道去执行另一个函数,对象如下:
1)普通函数
2)仿函数(functor):类里面实现operator()
3)Lambda表达式
3.使用的函数:
1)join()
2)detach()
区别:
join()函数,让主线程等待子线程执行完成,相当于阻塞,join执行完毕,join()后面的主线程才会继续执行。这样可以有效防止主程序已经停止运行,而子程序没有运行结束,子程序不得不中断。
detach()函数,让子线程和主线程分开独自执行,无法控制,该子线程由c++运行时库进行控制管理,驻留后台运行,运行结束也是由c++运行时库进行释放相关资源(守护线程)。但是当主线程结束,该子线程也会随即中断结束。
注意事项:
1)在使用detach函数时,注意子线程和主线程之间不要使用“引用”方式来传递参数,否则某一个先结束,释放变量,会导致另一个线程出现bug。
2)join()函数一般置于return语句前面,等待所有的子线程运行结束,主线程再结束。
4.简单案例
#include <thread>
#include <iostream>
void foo() {
std::cout << "subthread" << std::endl;
}
int main(int argc, char const *argv[]){
std::thread thread(foo); // 启动线程foo,并且已经开始执行
thread.join(); // 让主线程等待子线程执行完成,相当于阻塞,join执行完毕,主线程再继续执行
std::cout<<"I am main thread.\n";
return 0;
}
上面关键的两步:
std::thread thread(foo);
thread.join();
首先要创建线程的实例,然后进行阻塞。由于Lambda表达式也可以创建函数,因此也可以用Lambda表达式代替foo,同理functor。
线程传参
1.detach引发的问题
由于detach使得主线程和该子线程进行了分离,主线程不再等待该子线程完成就结束,就可能会导致如下问题:
如果从主线程以引用的方式传参数给子线程,当主线程运行结束,该参数也被释放,就会导致子线程在接受参数时接收到的是一个被释放的内存区域,引发不可预料的问题。
解决办法:
a)若传递int这种简单类型参数,建议都是值传递,不要用引用。防止节外生枝。
b)如果传递类对象,避免隐式类型转换。全部都在创建线程这一行就构建出临时对象来,然后在函数参数里用引用来接;否则系统还会构造一次对象
终极结论:
c)建议不使用detach(),只使用join():这样就不存在局部变量失效导致线程对内存的非法引用问题。
2.获取当前线程id
std::this_thread::get_id()
3.传递类对象的引用到线程函数,std::ref()函数
4.线程函数参数
- 普通函数
- 仿函数(functor)
- 智能指针作为参数
- 类的成员函数指针作为线程参数
(1)functor需要实现operator() ,调用如下,可见只是需要传入一个类的实例即可,自动的调用functor.
输入:
class baz{
public:
void operator()(int n) {
std::cout<<"var address in sub: "<<&n<<"|var value in sub: "<<n<<std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
int n = 0;
};
int main(int argc, char const *argv[]){
baz b;
int n=10;
std::thread t6(b,n); // t6 runs baz::operator() on object b
std::cout<<"var address in main: "<<&n<<"|var value in main: "<<n<<std::endl;
t6.join();
return 0;
}
输出:
var address in main: 0x61fe38|var value in main: 10
var address in sub: 0x286fd38|var value in sub: 10
(2)智能指针作为函数参数
// 传入一个智能指针
void smartptr(std::unique_ptr<int> ptr){}
std::unique_ptr<int> smptr(new int(100)); //不能detach
std::thread threadPtr(smartptr, std::move(smptr));
threadPtr.join();
if (smptr == nullptr)
std::cout<<"null\n";
当使用智能指针作为参数时,只能使用std::move的方式将其传参。并且传参结束,主线程中的智能指针就变成nullptr。因此此时不能使用detach,只能使用join().
(3)使用类的成员函数
std::thread threadPtr(&class_name::foo, &class_instance, foo_args);
mutex
创建和等待多个线程
(a)创建多个线程时,各个线程之间是顺序不定的执行,由系统调度。
(b)推荐join的写法,更容易写出稳定的程序。
(c)把thread对象放入到容器管理,看起来像个thread对象数组,这对我们一次创建大量的线程并对大量线程进行很方便。
数据共享问题分析
(1)只读的数据:是安全稳定的不需要特别什么处理手段,直接读就可以;
(2)有读有写:2个线程写,8个线程读,如果代码没有特别的处理,那程序肯定崩溃;
最简单的不崩溃处理,读的时候不能写,写的时候不能读。2个线程不能同时写,8个线程不能同时读;
写的动作分10小步;由于任务切换,导致各种诡异事情发生(最可能的诡异事情还是崩溃);
保护共享数据过程
(1)操作共享数据时候,为了保护共享数据,需要锁住共享数据。
(2)某个线程A,最先占用线程时,先lock()操作共享数据的代码。其他使用共享数据的线程必须等待当前占用共享数据的线程完程。
(3)A操作完成后,解锁unlock()。其他线程上。
(4)重复上述步骤操作共享数据。
实现保护共享数据
1.互斥量:std::mutex
- std::mutex mtx;
2.unlock()与lock()成对使用。
- mutex理解成一把锁。当线程占用共享数据时,先用mtx.lock()函数加锁。
- 对于同一个mutex对象,一次只有一处mtx.lock()能加锁成功,谁先抢占这分数据,就先lock()。只要lock()函数返回表示上锁成功,否则堵塞直到lock()成功。
- 当该占用数据的线程结束时,应该使用unlock(),否则整个程序堵塞无法运行。
注意:需要保护的数据,lock()和unlock()应该设置为合适的范围。少了达不到效果,多了耗时。
3.为防止大家忘记unlock(),引入了一个叫std::lock_guard的类模板:你忘记unlock不要紧,我替你unlock;
std::lock_guard可直接取代lock()和unlock();std::lock_guard构造函数里执行了mutex::lock(),析构函数里执行了mutex::unlock();
std::lock_guard<std::mutex> lock_grd(mtx);自动的加锁/解锁
死锁
(1)概念
当有多个mutex对象,至少两个,lock()的处理不当容易死锁。顺序一致不会死锁,相反才会死锁。
比如在两个线程里,分别如下对两份代码,进行lock()/ublock()。A线程里mtx_1.lock()后,若B线程mtx_2.lock()执行,那么A线程mtx_2.lock()要想执行成功,必须等待B线程mtx_2.unlock(),可是B中的mtx_1.lock()因为A线程的mtx_1.lock()已经执行又没unlock()无法执行,导致无法lock()。因此相互锁死。
线程A | 线程B |
mtx_1.lock(); mtx_2.lock(); //deal with somethig mtx_2.unlock(); mtx_1.unlock(); |
mtx_2.lock(); mtx_1.lock(); //deal with sth mtx_2.unlock(); mtx_1.unlock(); |
(2)预防死锁
-
不同线程的的不同对象lock()顺序一致
- 使用std::lock()模板
std::lock()函数模板
互斥量可以实现一次2个及其以上,但是不会存在死锁问题。如果有一个互斥量没有锁住,就会等待,同时把锁住的互斥量释放,等待所有的互斥量都锁住,才会返回。即,要么都锁住,要么都没有锁住。
std::lock(mtx_1, mtx_2);
std::lock()函数模板让编译器自己lock(),怎么才能编译器自己unlock():配合std::lock_guard<std::mutex>
(a)这里需要给lock_guard传入第二个参数,告诉编译器不再需要lock,因为前面函数模板std::lock()已经lock一次了,lock_guard是为了编译期自己unlock。
(b)需要传入第二个参数:std::adopt_lock
std::lock(mtx_1, mtx_2);
std::lock_guard<std::mutex> lockGrd1(mtx_1,std::adopt_lock);
std::lock_guard<std::mutex> lockGrd2(mtx_2,std::adopt_lock);
unique_lock 类模板
unique_lock 也是为了能实现编译器自己lock/unlock。
(1)相比较lock_guard,更加灵活,但是效率差,内存占用多。
(2)unique_lock构造函数的的第二个参数(理解为标志位):
(a)std::adopt_lock:使用提前必须已经lock,避免二次lock()。这样主要是不想自己手动unlock()。这一点和lock_guard()一致。
(b)std::try_to_lock:使用前提不能lock。尝试去锁定mutex,但是如果没有成功,不会阻塞在那,而是会立即返回。确定是否lock成功,可以通过owns_lock()判断。
例如:
std::unique_lock<std::mutex> uniGrd(mtx, std::try_to_lock); if(uniGrd.owns_lock()) { //拿到了lock() //处理一些没有共享数据的代码 } else{ std::cout<<"thread fail to lock.\n"; } |
(c)std::defer_lock:使用前提不能lock。它初始化一个没有lock过的unique_lock对象。
unique_lock重要的成员函数配合defer_lock()
+ lock()
+ unlock() 主要是用于处理一些非共享数据的代码。需要不断地lock、unlock。以保证效率。
std::unique_lock<std::mutex> uniGrd(mtx_1, std::defer_lock); //需要处理共享数据的代码 uniGrd.lock();
//处理一些没有共享数据的代码 uniGrd.unlock();
//下面又是一些需要处理共享数据的代码 uniGrd.lock(); msg.push_back(i); std::cout<<"generator: "<<i<<std::endl |
+ try_lock():尝试给互斥量加锁,如果拿到了就返回true,否则返回false.不阻塞。类似上面的try_to_lock。
+ release():返回它所管理的mutex对象指针,并且释放所有权。即unique_lock对象和mutex对象不再绑定在一起。一旦解除了关系,那么最终需要自己对这个互斥量对象unlock。
std::unique_lock<std::mutex> uniLock(mtx_1); // 编译器帮你lock auto mtxPtr = uniLock.release(); //解除绑定 // do something mtxPtr->unlock(); //解除绑定后,必须自己去unlock |
注意:为什么有时需要unlock():因为你lock锁住的代码段越少,执行越快,整个程序运行效率越高。
有人也把锁头锁住的代码多少称为锁的 粒度,粒度一般用粗细来描述;
> 锁住的代码少,这个粒度叫细,执行效率高;
> 锁住的代码多,这个粒度叫粗,执行效率低;
要学会尽量选择合适的粒度,是高级程序员的能力和实力的体现;
(3)unique_lock所有权的传递
- 使用移动语义,std::move()。
- mutex对象的所有权的传递只能移动不能复制,像极了unique_ptr