C++多线程学习(二)线程同步之互斥锁

前言

上一篇文章C++多线程学习(一)线程创建与管理我们初步了解了线程的概念,以及一直与线程一起提出来的进程,两者的意义与区别。文章的末尾给了一个例子,初步学会了如何创建多线程,以及创建多线程,上面的例子,有个问题就是cout显示错乱,本文就会讲解,为何会发生错乱,以及如何解决。

分析原因,先看下多线程的概念

  • 多线程并发:在同一时间段内交替处理多个操作,线程切换时间片是很短的(一般为毫秒级),一个时间片多数时候来不及处理完对某一资源的访问;
  • 线程间通信:一个任务被分割为多个线程并发处理,多个线程可能都要处理某一共享内存的数据,多个线程对同一共享内存数据的访问需要准确有序。

 这样就知道原因了,线程切换的时间片很短,cout可能没有处理完就切另一个线程了,还有就是cout访问的资源都是一个进程下的main线程创建的,也就是同一进程,其访问的资源内存是共享的,不加控制的话,自然就导致输出错乱了。cpu对于线程的切换时间,线程执行的速度我们是很难预估的,那如何让多线程对数据的访问,读写控制在我们想要的逻辑内呢,这个需求就是线程的同步和互斥,其本质是同进程共享内存使用的有序,原谅我把同步和互斥放到这里来讲,就当作是在多线程概念内的狭义互斥和同步吧,便于理解学习吧。实际这个概念应该是广义上升到多进程(非多线程)对某一资源(非某一内存)的访问。

互斥:是指某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。但互斥无法限制访问者对资源的访问顺序,即访问是无序的。
同步:是指在互斥的基础上(大多数情况),通过其它机制实现访问者对资源的有序访问。在大多数情况下,同步已经实现了互斥,特别是所有写入资源的情况必定是互斥的。少数情况是指可以允许多个访问者同时访问资源。
显然,同步是一种更为复杂的互斥,而互斥是一种特殊的同步。

我们可以看到,有序访问的关键是互斥,实现互斥有很多机制,c++提供的最简单的就是互斥量,有一个< mutex >库文件专门支持对共享数据结构的互斥访问。

lock与unlock保护共享资源

Mutex全名mutual exclusion(互斥体),是个object对象,用来协助采取独占排他方式控制对资源的并发访问。这里的资源可能是个对象,或多个对象的组合。为了获得独占式的资源访问能力,相应的线程必须锁定(lock) mutex,这样可以防止其他线程也锁定mutex,直到第一个线程解锁(unlock) mutex。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使用。

C++多线程学习(二)线程同步之互斥锁_第1张图片

 依上图,两个线程的执行顺序和速度是不可控的其中一个线程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时间,测试下,我们看下运行结果,两个线程都被阻塞不再执行了,且其何时被阻塞,也并不可控。

C++多线程学习(二)线程同步之互斥锁_第2张图片

 忘写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;
}

执行结果

C++多线程学习(二)线程同步之互斥锁_第3张图片

 正常显示,并无错乱。

进一步,我们对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;
}

看结果

C++多线程学习(二)线程同步之互斥锁_第4张图片

 同样正常显示,并无异常。

你可能感兴趣的:(开源项目学习,c++,多线程,linux)