详解C++多线程

Posted corineru

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了详解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

 

以上是关于详解C++多线程的主要内容,如果未能解决你的问题,请参考以下文章

20160226.CCPP体系详解(0036天)

C++并发与多线程 3_线程传参数详解,detach 注意事项

C++ 多线程std::thread 详解

C++ 多线程std::thread 详解

c++ thread创建与多线程同步详解

详解c++11多线程