1. 线程和进程的概念
线程和进程的区别:
- 线程 是CPU调度的基本单位,是进程的基本执行单元。
- 进程 是资源分配的基本单位,拥有一个完整的虚拟地址空间.
- 一个进程至少有一个线程;它们共享进程的地址空间;而进程有自己独立的地址空间;
单核处理器一个时间片只运行一个线程,是假并发;多核处理器一个时间片可以运行多个线程,达到真正意义上的并发。
多线程同步和互斥有何异同:
- 线程的互斥: 是指对于某一资源只允许一个访问者对它进行访问,具有唯一性和排他性。但是并不能控制访问者对资源的访问顺序,即访问是无序的。
- 线程的同步: 是指在互斥的基础上(大多数情况),通过其它机制(临界区,信号量,互斥量,事件)实现访问者对资源的有序访问。
同步其实已经实现了互斥,所以同步是一种更为复杂的互斥。
总的来说,两者的区别就是:
互斥是通过竞争对资源的独占使用,彼此之间不需要知道对方的存在,执行顺序是一个乱序。
同步是协调多个相互关联线程合作完成任务,彼此之间知道对方存在,执行顺序往往是有序的。
线程创建:(Windows API)
线程的启动,结束,创建线程方法(函数,类,lambda表达式),thread(), join(),detach(),joinable()。
join() :主线程等待子线程退出,再退出主线程。
detach(): 主线程不等待子线程退出。
获取线程Id:
std::this_thread::get_id()
std::ref()
虽然线程使用引用传递参数,但是编译器在传参时都会拷贝一份,这样的话,在函数内部修改参数的值,不会影响外部的值。为了解决这个问题,在传参时,可以使用std::ref()包裹起来。(如,std::thread mythread(func, std::ref(myObj));)
传递临时对象作为线程参数:
使用detach()时:
如果传递简单数据类型(如 int )建议使用值传递,不要用引用。
如果传递类对象,避免隐式类型转换,要在创建线程对象时,显示地创建参数, 在线程函数参数中用引用传参, 避免再多一次拷贝构造。(如,void fun(class & A); std::thread mythread (func, A(val));)
所以,建议非必要,不使用detach,使用join,这样就不会存在局部变量失效导致子线程对内存的非法访问
传递 智能指针
std::unique_ptr<int> myp(new int(100));
std::thread mythread(func, std::move(myp));
mythread.join();
注意,由于unique_ptr的特殊性,这里要用move方法传递。由于myp传递给func之后,本身被释放了,所以后面只能用join不能用detach。
用成员函数作为线程的入口函数
A myobj(20);//生成一个类对象
std::thread mythread(&A::thread_work, myobj, 15);
mythread.join();
2. 线程同步
互斥量:
互斥量使用加锁的方法来实现对共享资源的访问控制。线程必须获得锁之后才能访问共享资源,访问完之后释放锁。如果无法获得锁,线程将阻塞等待,直到获得锁。
```cpp
std::mutex _myMutex; // 需要声明成全局的,或者类成员,以便多个函数使用。
_myMutex.lock();
//...
_myMutex.unlock();
注意: lock()、unlock() 必须成对使用, 否则会造成死锁。
std::lock_guard类模板:
为了防止,lock了以后忘了unlock,引入了lock_guard的概念。可同时取代lock()、 unlock()
用法:在要加锁的代码块内定义一个lock_guard即可,在代码块结束时,自动释放锁。其原理是,在lock_guard的构造函数中执行了std::lock(),在析构的时候执行了std::unlock()。
std::mutex my_mutex;
std::lock_guard<std::mutex> mtx(my_mutex);
死锁:
死锁产生的条件:
1). 同一个线程先后两次锁同一个锁:同一个线程先后调用两次lock,第二次调用时要挂起等待锁被释放,而这个锁正是被当前线程占用,没有机会释放,导致死锁。
2). 在多个线程中交叉锁:线程A获得了锁1,线程B获得了锁2,这时线程A试图获得锁2,就要挂起等待线程B释放锁2, 同时线程B也试图获得锁1,同样要挂起等待线程A释放锁1。导致死锁。
死锁的一般解决方案:
1). 尽量避免同时获得多个锁
2). 对多线程死锁,要保证锁的调用顺序保持一致。(比如,线程A,B,锁1,锁2,调用的时候,保证线程A访问锁的顺序是锁1,锁2, 线程B访问锁的顺序也是锁1,锁2。)
3). 使用 std::lock()函数模板:
用来处理多个互斥量。 可以一次锁住两个或者两个以上的互斥量,它不存在因为在多个线程中,因为锁的顺序问题导致死锁的风险问题。
std::lock(): 如果互斥量中一个都没锁住,就等待,如果锁住了一个,另外一个没锁成功,则立即把已经锁住的解锁。用法:
std::mutex my_mutex1;
std::mutex my_mutex_2;//放在全局或者类中。
std::lock(my_mutex1,my_mutex2);//同时锁住两个互斥量
//...
my_mutex1.unlock();//分别手动释放
my_mutex2.unlock();
这种方法的缺点是仍然需要手动释放锁,容易出错; 同时锁多个互斥量,谨慎使用。
4). 使用std::lock_guard()的std::adopt_lock参数
std::adopt_lock是一个结构体对象,起一个标记作用,表示这个互斥量已经lock了,不需要再再lock_guard中再进行lock了。
用法:
std::mutex my_mutex1;
std::mutex my_mutex_2;//放在全局或者类中。
std::lock(my_mutex1,my_mutex2);//同时锁住两个互斥量
std::lock_guard<std::mutex> mtx1(my_mutex1, std::adopt_lock);
std::lock_guard<std::mutex> mtx2(my_mutex2, std::adopt_lock);//会在离开作用域的时候自动释放my_mutex,后面就不用再手工释放锁了。
信号量
信号量本质上是一个非负的整数计数器,通过PV原子操作来控制资源的访问权限。一次P操作,信号量的计数加一,一次V操作,信号量的计数减一。进程、线程通过信号量的值来判断是否有访问权限,当信号量>0时,允许访问,否则, 阻塞等待知道信号量>0为止。
-
用于互斥(无序):
如图1,定义一个信号量,赋初始值为1,线程A和线程B同时进行P操作申请访问资源,但是谁先抢到,顺序不确定。假设A抢到,A执行,B阻塞,当A执行V操作释放信号量时,B线程获得资源访问权限,开始执行,执行完之后,进行V操作释放信号量。
-
用于同步(顺序):
如图2,设置2个信号量,并赋不同的初值来实现有序执行。sem1为1,sem2为0,若想让线程A先执行,B后执行:
a. 线程A对sem1执行P操作(sem1>0,线程A执行),线程B对sem2执行P操作(sem2=0,线程B阻塞),当线程A访问完资源,对sem2进行V操作,sem2加一,这时线程B被唤醒,访问资源,结束后再对sem1进行V操作。
互斥量更适用于同时可用的资源是唯一的情况;信号量更适用于同时可用的资源为多个的情况。
息
共享内存
共享内存区域是被多个进程共享的一部分物理内存。
不同进程把该区域内存映射到自己的虚拟地址空间,通过自己线程的虚拟地址,访问、操作共享内存。
共享内存的特点是,访问速度快,但是没有提供同步机制,要配合信号量等使用。
条件变量
条件变量通常和互斥量一起使用。
条件变量可以通过允许线程阻塞和等待另一个线程发送信号来弥补互斥锁的不足
为什么需要互斥锁和条件变量一起协作来线程同步?
因为互斥量是一种锁,一旦某个共享资源区被加锁之后,另一个线程想要访问该资源区,如果没有条件变量的辅助,那么它就不得不频繁的去尝试申请访问,这回形成一种自旋锁的状态,他会占用 cpu 的资源。然而有了条件变量之后,条件变量会让另外的线程休眠等待,直到资源区被解锁之后直接通知另外的线程,这就节省了CPU的资源,条件变量的优势也更好的弥补了互斥锁的不足。