上一篇文章C++多线程学习(一)线程创建与管理我们初步了解了线程的概念,以及一直与线程一起提出来的进程,两者的意义与区别。文章的末尾给了一个例子,初步学会了如何创建多线程,以及创建多线程,上面的例子,有个问题就是cout显示错乱,本文就会讲解,为何会发生错乱,以及如何解决。
分析原因,先看下多线程的概念
这样就知道原因了,线程切换的时间片很短,cout可能没有处理完就切另一个线程了,还有就是cout访问的资源都是一个进程下的main线程创建的,也就是同一进程,其访问的资源内存是共享的,不加控制的话,自然就导致输出错乱了。cpu对于线程的切换时间,线程执行的速度我们是很难预估的,那如何让多线程对数据的访问,读写控制在我们想要的逻辑内呢,这个需求就是线程的同步和互斥,其本质是同进程共享内存使用的有序,原谅我把同步和互斥放到这里来讲,就当作是在多线程概念内的狭义互斥和同步吧,便于理解学习吧。实际这个概念应该是广义上升到多进程(非多线程)对某一资源(非某一内存)的访问。
互斥:是指某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。但互斥无法限制访问者对资源的访问顺序,即访问是无序的。
同步:是指在互斥的基础上(大多数情况),通过其它机制实现访问者对资源的有序访问。在大多数情况下,同步已经实现了互斥,特别是所有写入资源的情况必定是互斥的。少数情况是指可以允许多个访问者同时访问资源。
显然,同步是一种更为复杂的互斥,而互斥是一种特殊的同步。
我们可以看到,有序访问的关键是互斥,实现互斥有很多机制,c++提供的最简单的就是互斥量,有一个< mutex >库文件专门支持对共享数据结构的互斥访问。
Mutex全名mutual exclusion(互斥体),是个object对象,用来协助采取独占排他方式控制对资源的并发访问。这里的资源可能是个对象,或多个对象的组合。为了获得独占式的资源访问能力,相应的线程必须锁定(lock) mutex,这样可以防止其他线程也锁定mutex,直到第一个线程解锁(unlock) mutex。mutex类的主要操作函数见下表:
从上表可以看出,mutex不仅提供了常规锁,还为常规锁可能造成的阻塞提供了尝试锁(带时间的锁需要带时间的互斥类timed_mutex支持,具体见下文)。下面先给出一段示例代码:
// mutex1.cpp 通过互斥体lock与unlock保护共享全局变量
#include
#include
#include
#include
std::chrono::milliseconds interval(100);
std::mutex mutex;
int job_shared = 0; //两个线程都能修改'job_shared',mutex将保护此变量
int job_exclusive = 0; //只有一个线程能修改'job_exclusive',不需要保护
//此线程只能修改 'job_shared'
void job_1()
{
mutex.lock();
std::this_thread::sleep_for(5 * interval); //令‘job_1’持锁等待
++job_shared;
std::cout << "job_1 shared (" << job_shared << ")\n";
mutex.unlock();
}
// 此线程能修改'job_shared'和'job_exclusive'
void job_2()
{
while (true) { //无限循环,直到获得锁并修改'job_shared'
if (mutex.try_lock()) { //尝试获得锁成功则修改'job_shared'
++job_shared;
std::cout << "job_2 shared (" << job_shared << ")\n";
mutex.unlock();
return;
} else { //尝试获得锁失败,接着修改'job_exclusive'
++job_exclusive;
std::cout << "job_2 exclusive (" << job_exclusive << ")\n";
std::this_thread::sleep_for(interval);
}
}
}
int main()
{
std::thread thread_1(job_1);
std::thread thread_2(job_2);
thread_1.join();
thread_2.join();
getchar();
return 0;
}
简单分析下代码,创建thread_1,thread_2,在thread_2创建后才join阻塞main线程,所以thread_1、thread_2会同时并发执行。job_share对象两个线程都会使用、job_exclusive只有thread_2使用。
依上图,两个线程的执行顺序和速度是不可控的其中一个线程lock后,只有当前线程能访问job_shared。lock()的概念是mutex锁lock后,在其unlock前,再次调用lock()会阻塞这里,知道其被unlock()才会继续往下执行。try_lock()是判断其是否可lock(),也就是是否被lock()后没有unlock(),非锁定状态下就会执行lock()并返回true继续往下执行,否则返回false继续往下执行,不会阻塞住。理解lock()的使用机制,我们就知道了,如果有超过一个线程访问同一个资源,尤其是改变其值的操作时,需要加lock(),这样就保证了同一时间只有一个线程在执行lock()到unlock()之间的命令段。
lock()之后必须unlock(),否则其他线程调用lock()时就会阻塞住,这就要求lock()和unlock()必须一一对应,否则很有可能会出现死锁的情况。凭什么说不一一对应就会死锁呢?我们可以写个错误例子验证下。
// mutex1.cpp 通过互斥体lock与unlock保护共享全局变量
#include
#include
#include
#include
std::chrono::milliseconds interval(100);
std::mutex mutex;
int job_shared = 0; //两个线程都能修改'job_shared',mutex将保护此变量
int job_exclusive = 0; //只有一个线程能修改'job_exclusive',不需要保护
//此线程只能修改 'job_shared'
void job_1()
{
while (true) {
mutex.lock();
std::this_thread::sleep_for(interval); //令‘job_1’持锁等待
++job_shared;
std::cout << "job_1 shared (" << job_shared << ")\n";
//mutex.unlock(); //故意不释放锁,让两个线程死锁阻塞
}
}
// 此线程能修改'job_shared'和'job_exclusive'
void job_2()
{
while (true) {
mutex.lock();
std::this_thread::sleep_for(interval); //令‘job_2’持锁等待
++job_shared;
std::cout << "job_2 shared (" << job_shared << ")\n";
mutex.unlock();
}
}
int main()
{
//std::thread thread_1(job_1);
std::thread thread_2(job_2);
std::thread thread_1(job_1);
thread_1.join();
thread_2.join();
getchar();
return 0;
}
减少了下锁控制区域代码的执行时间,这样更容易两个线程同时死锁,大家可以改变sleep时间,测试下,我们看下运行结果,两个线程都被阻塞不再执行了,且其何时被阻塞,也并不可控。
忘写lock(),unlock()使用不当会带来很严重的后果,这和我们使用的c++指针new和delete是不是很类似,为了结绝作为开发员的人类的粗心大意问题,c++很贴心的提供了智能指针shared_ptr与unique_ptr,对应的c++也提供了很多功能更强大的智能锁lock_guard与unique_lock。针对我们的需求,还有可设定时间的锁timed_mutex,嵌套锁recursive_mutex,目前就不深入研究了,先把功能概念过一遍。
类模板 | 描述 |
std::mutex | 同一时间只可被一个线程锁定。如果它被锁住,任何其他lock()都会阻塞(block),直到这个mutex再次可用,且try_lock()会失败。 |
std::recursive_mutex |
允许在同一时间多次被同一线程获得其lock。其典型应用是:函数捕获一个lock并调用另一函数而后者再次捕获相同的lock。 |
std::timed_mutex |
额外允许你传递一个时间段或时间点,用来定义多长时间内它可以尝试捕获一个lock。为此它提供了try_lock_for(duration)和try_lock_until(timepoint)。 |
std::recursive_timed_mutex |
允许同一线程多次取得其lock,且可指定期限。 |
现在我们已经初步会使用互斥体
C++多线程学习(一)线程创建与管理里面最后的显示错乱问题,错乱原因是多线程同时调用cout导致的,那我们对应的把所有调用cout地方加上锁看下。
//thread2.cpp 增加对cout显示终端资源并发访问的互斥锁保护
#include
#include
#include
#include
using namespace std;
std::mutex mutex1;
void thread_function(int n)
{
std::thread::id this_id = std::this_thread::get_id(); //获取线程ID
for(int i = 0; i < 5; i++){
mutex1.lock();
cout << "Child function thread " << this_id<< " running : " << i+1 << endl;
mutex1.unlock();
std::this_thread::sleep_for(std::chrono::seconds(n)); //进程睡眠n秒
}
}
class Thread_functor
{
public:
// functor行为类似函数,C++中的仿函数是通过在类中重载()运算符实现,使你可以像使用函数一样来创建类的对象
void operator()(int n)
{
std::thread::id this_id = std::this_thread::get_id();
for(int i = 0; i < 5; i++){
{
mutex1.lock();
cout << "Child functor thread " << this_id << " running: " << i+1 << endl;
mutex1.unlock();
}
std::this_thread::sleep_for(std::chrono::seconds(n)); //进程睡眠n秒
}
}
};
int main()
{
thread mythread1(thread_function, 1); // 传递初始函数作为线程的参数
if(mythread1.joinable()) //判断是否可以成功使用join()或者detach(),返回true则可以,返回false则不可以
mythread1.join(); // 使用join()函数阻塞主线程直至子线程执行完毕
Thread_functor thread_functor;
thread mythread2(thread_functor, 3); // 传递初始函数作为线程的参数
if(mythread2.joinable())
mythread2.detach(); // 使用detach()函数让子线程和主线程并行运行,主线程也不再等待子线程
auto thread_lambda = [](int n){
std::thread::id this_id = std::this_thread::get_id();
for(int i = 0; i < 5; i++)
{
mutex1.lock();
cout << "Child lambda thread " << this_id << " running: " << i+1 << endl;
mutex1.unlock();
std::this_thread::sleep_for(std::chrono::seconds(n)); //进程睡眠n秒
}
};
thread mythread3(thread_lambda, 4); // 传递初始函数作为线程的参数
if(mythread3.joinable())
mythread3.join(); // 使用join()函数阻塞主线程直至子线程执行完毕
unsigned int n = std::thread::hardware_concurrency(); //获取可用的硬件并发核心数
mutex1.lock();
std::cout << n << " concurrent threads are supported." << endl;
mutex1.unlock();
std::thread::id this_id = std::this_thread::get_id();
for(int i = 0; i < 5; i++){
{
mutex1.lock();
cout << "Main thread " << this_id << " running: " << i+1 << endl;
mutex1.unlock();
}
std::this_thread::sleep_for(std::chrono::seconds(1));
}
getchar();
return 0;
}
执行结果
正常显示,并无错乱。
进一步,我们对join和detach()的理解,其实会同时并行的线程只有mythread2,mythread3,我们其实只需要对这两个线程加锁限制
//thread2.cpp 增加对cout显示终端资源并发访问的互斥锁保护
#include
#include
#include
#include
using namespace std;
std::mutex mutex1;
void thread_function(int n)
{
std::thread::id this_id = std::this_thread::get_id(); //获取线程ID
for(int i = 0; i < 5; i++){
cout << "Child function thread " << this_id<< " running : " << i+1 << endl;
std::this_thread::sleep_for(std::chrono::seconds(n)); //进程睡眠n秒
}
}
class Thread_functor
{
public:
// functor行为类似函数,C++中的仿函数是通过在类中重载()运算符实现,使你可以像使用函数一样来创建类的对象
void operator()(int n)
{
std::thread::id this_id = std::this_thread::get_id();
for(int i = 0; i < 5; i++){
{
mutex1.lock();
cout << "Child functor thread " << this_id << " running: " << i+1 << endl;
mutex1.unlock();
}
std::this_thread::sleep_for(std::chrono::seconds(n)); //进程睡眠n秒
}
}
};
int main()
{
thread mythread1(thread_function, 1); // 传递初始函数作为线程的参数
if(mythread1.joinable()) //判断是否可以成功使用join()或者detach(),返回true则可以,返回false则不可以
mythread1.join(); // 使用join()函数阻塞主线程直至子线程执行完毕
Thread_functor thread_functor;
thread mythread2(thread_functor, 3); // 传递初始函数作为线程的参数
if(mythread2.joinable())
mythread2.detach(); // 使用detach()函数让子线程和主线程并行运行,主线程也不再等待子线程
auto thread_lambda = [](int n){
std::thread::id this_id = std::this_thread::get_id();
for(int i = 0; i < 5; i++)
{
mutex1.lock();
cout << "Child lambda thread " << this_id << " running: " << i+1 << endl;
mutex1.unlock();
std::this_thread::sleep_for(std::chrono::seconds(n)); //进程睡眠n秒
}
};
thread mythread3(thread_lambda, 4); // 传递初始函数作为线程的参数
if(mythread3.joinable())
mythread3.join(); // 使用join()函数阻塞主线程直至子线程执行完毕
unsigned int n = std::thread::hardware_concurrency(); //获取可用的硬件并发核心数
std::cout << n << " concurrent threads are supported." << endl;
std::thread::id this_id = std::this_thread::get_id();
for(int i = 0; i < 5; i++){
{
cout << "Main thread " << this_id << " running: " << i+1 << endl;
}
std::this_thread::sleep_for(std::chrono::seconds(1));
}
getchar();
return 0;
}
看结果
同样正常显示,并无异常。