• 详解C++多线程(二)


    一、线程的同步和互斥

      同步是指散步在不同任务之间的若干程序片断,它们的运行必须严格按照规定的某种先后次序来运行,这种先后次序依赖于要完成的特定的任务。最基本的场景就是:两个或两个以上的进程或线程在运行过程中协同步调,按预定的先后次序运行。比如 A 任务的运行依赖于 B 任务产生的数据

      互斥是指散步在不同任务之间的若干程序片断,当某个任务运行其中一个程序片段时,其它任务就不能运行它们之中的任一程序片段,只能等到该任务运行完这个程序片段后才可以运行。最基本的场景就是:一个公共资源同一时刻只能被一个进程或线程使用,多个进程或线程不能同时使用公共资源

    二、加锁与解锁

    上一章讲过同一个进程里的所有线程是共享资源池的。在多任务操作系统中,同时运行的多个任务可能都需要使用同一种资源。这个过程有点类似于,公司部门里,我在使用着打印机打印东西的同时(还没有打印完),别人刚好也在此刻使用打印机打印东西,如果不做任何处理的话,打印出来的东西肯定是错乱的。

    多个线程同时访问共享资源的时候需要用到互斥量,当一个线程锁住了互斥量后,其他线程必须等待这个互斥量解锁后才能访问它。

    在线程里也有这么一把锁——互斥锁(mutex),互斥锁是一种简单的加锁的方法来控制对共享资源的访问。只要某一个线程上锁了,那么就会强行霸占公共资源的访问权,其他的线程无法访问直到这个线程解锁了。

    互斥锁只有两种状态,即上锁( lock )和解锁( unlock )。例如下面这个例子中,g_page_mutex这把锁保护了g_page这个map。

    #include<iostream>
    #include<thread>
    #include<mutex>
    #include<string>
    #include<map>
    
    using namespace std;
    
    map<string, string> g_page;
    mutex g_page_mutex;
    
    void save_page(const string &url)
    {
        this_thread::sleep_for(chrono::seconds(2));
        string result = "fake content";
    
        g_page_mutex.lock(); //上锁
        g_page[url] = result;
        g_page_mutex.unlock();//解锁
    }
    
    int main()
    {
        thread t1(save_page, "http://foo");
        thread t2(save_page, "http://bar");
        t1.join();
        t2.join();
        for (const auto &pair : g_page)
        {
            cout << pair.first << "=>" << pair.second << endl;
        }
    
    }

    运行结果:

    lock_guard类介绍

    但是上面手动上锁解锁的操作有个隐患是,如果上锁和解锁中间的代码出现了问题,那么这把锁就一直无法解开,成为了死锁。

    c++11也提供了一种更安全更方便的上锁和解锁的方式,就是lock_guard这个类。从名字可以看出,这是一个监视锁的类。

    #include<iostream>
    #include<thread>
    #include<mutex>
    #include<string>
    #include<map>
    
    using namespace std;
    
    map<string, string> g_page;
    mutex g_page_mutex;
    
    void save_page(const string &url)
    {
        this_thread::sleep_for(chrono::seconds(2));
        string result = "fake content";
        
        lock_guard<mutex> lc(g_page_mutex);
        
        //或者也可以写成
        //lock(g_page_mutex);
        //lock_guard<mutex> lc(g_page_mutex, adopt_lock);
      //或者也可以写成
      //lock_guard<mutex> lc(g_page_mutex, defer_lock);
      //lock(g_page_mutex);
    g_page[url] = result; } int main() { thread t1(save_page, "http://foo"); thread t2(save_page, "http://bar"); t1.join(); t2.join(); for (const auto &pair : g_page) { cout << pair.first << "=>" << pair.second << endl; } }

    在 lock_guard 对象构造时,传入的 Mutex 对象(即它所管理的 Mutex 对象)会被当前线程锁住。在lock_guard 对象被析构时,它所管理的 Mutex 对象会自动解锁,由于不需要程序员手动调用 lock 和 unlock 对 Mutex 进行上锁和解锁操作,因此这也是最简单安全的上锁和解锁方式,尤其是在程序抛出异常后先前已被上锁的 Mutex 对象可以正确进行解锁操作,极大地简化了程序员编写与 Mutex 相关的异常处理代码。值得注意的是,lock_guard 对象并不负责管理 Mutex 对象的生命周期,lock_guard 对象只是简化了 Mutex 对象的上锁和解锁操作,方便线程对互斥量上锁,即在某个 lock_guard 对象的声明周期内,它所管理的锁对象会一直保持上锁状态;而 lock_guard 的生命周期结束之后,它所管理的锁对象会被解锁。

    lock_guard构造时还可以传入一个参数adopt_lock或者defer_lock。adopt_lock表示是一个已经锁上了锁,defer_lock表示之后会上锁的锁。

    unique_lock介绍

    上文介绍的lock_guard类最大的缺点也是简单,没有给程序员提供足够的灵活度,因此C++11定义了另一个unique_guard类。这个类和lock_guard类似,也很方便线程对互斥量上锁,但它提供了更好的上锁和解锁控制。

    顾名思义,unique_lock以独占所有权的方式管理mutex的mutex对象的上锁和解锁操作。所谓独占所有权,就是指没有其他的unique_lock对象同时拥有某个mutex对象。在构造(或移动(move)赋值)时,unique_lock 对象需要传递一个 Mutex 对象作为它的参数,新创建的 unique_lock 对象负责传入的 Mutex 对象的上锁和解锁操作。

    unique_lock 对象也能保证在其自身析构时它所管理的 Mutex 对象能够被正确地解锁(即使没有显式地调用 unlock 函数)。因此,和 lock_guard 一样,这也是一种简单而又安全的上锁和解锁方式,尤其是在程序抛出异常后先前已被上锁的 Mutex 对象可以正确进行解锁操作,极大地简化了程序员编写与 Mutex 相关的异常处理代码。

    #include<thread>
    #include<iostream>
    #include<mutex>
    
    using namespace std;
    
    struct Box
    {
        explicit Box(int num) : num_things{num} {}
        int num_things;
        mutex m;
    };
    
    
    void transfer(Box &from, Box &to, int num)
    {
        unique_lock<mutex> lg1(from.m);
        unique_lock<mutex> lg2(to.m);
        
        //lock(lg1, lg2);
        
        from.num_things -= num;
        to.num_things  += num;
        cout<< "from.num_things ="<<from.num_things<<endl;
        cout<<"to.num_things = "<<to.num_things<<endl;
    }
    
    int main()
    {
        Box a(100);
        Box b(50);
        
        thread t1(transfer,ref(a) ,ref(b),5);
        thread t2(transfer, ref(a), ref(b), 10);
        
        t1.join();
        t2.join();
    }

    运行结果:

    需要注意的是,mutex锁住的对象,其实就是mutex变量能够访问到的变量。只有当mutex为全局变量,那么所有的全局变量都会被锁住。如果mutex为局部变量,那么mutex的作用范围也仅限于{}范围内的变量。比如上面的例子中,m是每个对象都关联的一个mutex变量,所以m可以访问到每个对象的num_things这个变量。假如对象a的m上锁以后,一个时刻就只能有一个线程可以访问对象a。注意这种情况下是可以有两个线程可以同时分别去访问a和b的,因为a的m无法访问到b的任何变量,所以a的m是锁不住b的,同理,b也无法锁住a。

    参考:

      https://www.cnblogs.com/code-wangjun/p/7476559.html

      https://blog.csdn.net/daaikuaichuan/article/details/82950711

      https://en.cppreference.com/w/cpp/thread/mutex

      http://www.cnblogs.com/haippy/p/3346477.html

  • 相关阅读:
    EOJ二月月赛补题
    cf401d
    cf628d
    cf55d
    HDU 6148 Valley Number
    洛谷 P3413 SAC#1
    洛谷 P4127[AHOI2009]同类分布
    洛谷 P2602 [ZJOI2010]数字计数
    bzoj 3679
    函数和循环闭包的理解
  • 原文地址:https://www.cnblogs.com/corineru/p/10847930.html
Copyright © 2020-2023  润新知