C++ 11 多线程:锁

原创 2016年08月30日 15:42:46

前言

在C++11新标准发行之前,C++并发编程往往依靠第三方的函数库,如:Linux下的pthread。多线程环境下,线程安全问题往往是程序员关注的重点。然而,古人云:线程安全问题单单依靠库函数是没办法完全解决的。Threads Cannot be Implemented as a Library 这篇文章比较详细的叙述了其中的原因,有兴趣的童鞋可以去看看。C++ 11开始引入了多线程的标准,这是多么欢欣鼓舞的事情,今天来谈谈多线程中很重要的一个事情,就是锁。

正文

新标准下的关于锁的内容几乎都在头文件mutex 中。常用的锁,包括:mutexrecursive_lock 都为我们提供如下函数:

  • void lock() //阻塞版本的上锁,失败则阻塞。属于空闲等待锁
  • bool try_lock() //非阻塞版本的上锁,成功返回true,失败返回false。属于忙等待锁
  • void unlock() //解锁

从字面意思就可以看出,mutex是普通的非递归锁,而recursive_mutex为递归锁。他们的区别在于,同一个线程对多次上锁后的行为。对于前者,这种操作是不被允许的,而对于后者,这种行为是被允许的。来看如下示例1-4:

void main()
{
    // 示例1:同一个mutex对象连续调拥两次lock
    mutex m1;
    m1.lock();
    m1.lock();
    m1.unlock();
}

执行的结果是抛出异常并执行abort()。

void main()
{
    // 示例2:对同一个mutex对象调用两次try_lock
    mutex m1;
    cout << m1.try_lock() << endl;
    cout << m1.try_lock() << endl;
    m1.unlock();
}

结果:

1
0

程序能够正常结束,说明try_lock() 并没有阻塞线程而进行等待,也不会抛出异常。前面提到try_lock是忙等待锁,因为它无法阻塞自己,想要获取锁,必须不停的尝试上锁。

void main()
{
    // 示例3:对同一个recursive_mutex调用两次lock
    recursive_mutex rm;
    rm.lock();
    rm.lock();
    rm.unlock();
    rm.unlock();
}

结果是程序正常的执行结束,没有出现死锁的情况。然而,值得注意的是:程序中需要两次释放锁,否则抛出异常

void main()
{
    //示例4:对同一个recursive_mutex对象调用两次try_lock
    recursive_mutex rm;
    cout << rm.try_lock() << endl;
    cout << rm.try_lock() << endl;
    rm.unlock();
    rm.unlock();
}

程序执行的结果是:

1
1

两次上锁都成功了。同样的,需要两次释放锁。个人认为,对于递归锁,try_lock 的意义没有像mutex来的那么大。毕竟,忙等待的行为对CPU并不友好。
从刚才的实例中,我们发现,一旦某个互斥量被锁住,程序员必须记得自己把他解锁,否则会程序将会抛出异常。看看实例代码5:

void main()
{
    // 实例5:忘记解锁
    mutex m1;
    m1.lock();
}

控制台会打印出如下信息:mutex destroyed while busy,并抛出异常执行abort()。这样也是危险的,同样导致程序异常终止。

上述的行为类似于内存泄露,程序员要手动释放自己new出来的对象。很多情况下会忘记解锁,这就对程序的正常运行造成了隐患。对于裸指针的管理我们通常用到的智能指针,通过只能指针对裸指针进行封装,利用智能指针的析构自动析构裸指针。这样的思路同样沿用至多线程中锁的管理机制中。11新标准为我们提供了locklock_guard以及unique_lock 作为锁的管理工具。与此配套使用的还有一系列的标签:std::adopt_lock 以及std::defer_lock

首先看看lock_guard

// 实例6:lock_guard
template<class _Mutex>
    class lock_guard
    {   // class with destructor that unlocks mutex
public:
    typedef _Mutex mutex_type;
    // 
    explicit lock_guard(_Mutex& _Mtx)
        : _MyMutex(_Mtx)
        {   // construct and lock
        _MyMutex.lock();
        }

    lock_guard(_Mutex& _Mtx, adopt_lock_t)
        : _MyMutex(_Mtx)
        {   // construct but don't lock
        }

    ~lock_guard() _NOEXCEPT
        {   // unlock
        _MyMutex.unlock();
        }

    lock_guard(const lock_guard&) = delete;
    lock_guard& operator=(const lock_guard&) = delete;

private:
    _Mutex& _MyMutex;
    };

lock_guard 有如下特性:

  1. 在构造函数中关联一个互斥量并对其上锁。在析构函数中释放锁。
  2. 不能被拷贝
  3. 对于构造函数lock_guard(_Mutex& _Mtx, adopt_lock_t),其第二个参数adopt_lock_t表明其当前关联的互斥量已经上锁。该构造函数不对关联的互斥量上锁。
  4. 使用了泛型,因此,可以支持多种不同类型的互斥量,如:mutex、recursive_mutex。
// 实例7
void do_something_in_critical_area()
{
    cout << "In critical section" << endl;
}
void do_otherthing() {}
void main()
{
    mutex mLock;
    {
        lock_guard<mutex> lg(mLock);
        cout << "Try lock: " << mLock.try_lock() << endl;
        do_something_in_critical_area();
    }
    cout << "Try lock: " << mLock.try_lock() << endl;
    do_otherthing();
    mLock.unlock();
}

结果如下:

Try lock: 0
In critical section
Try lock: 1

也可以将实例7中的主函数该成这样:

// 示例8
void main()
{
    mutex mLock;
    {
        mLock.lock();
        lock_guard<mutex> lg(mLock, std::adopt_lock); // 没有上锁,仅仅起到托管作用
        cout << "Try lock: " << mLock.try_lock() << endl;
        do_something_in_critical_area();
    }
    cout << "Try lock: " << mLock.try_lock() << endl;
    do_otherthing();
    mLock.unlock();
}

结果是一样的。使用lock_guard 后,妈妈再也不用担心忘记释放锁了。

接下来再说说unique_lock 它比lock_guard 功能更加强大:

  • unique_lock 更加多样化的构造函数。除了与lock_guard 相同的两种构造形式,unique_lock 为我们提供更多的标签来知道构造函数的行为,如:
    // 示例9:
    unique_lock(_Mutex& _Mtx, defer_lock_t) _NOEXCEPT
        : _Pmtx(&_Mtx), _Owns(false)
        {   // construct but don't lock
        }
    // 示例10:
    unique_lock(_Mutex& _Mtx, try_to_lock_t)
        : _Pmtx(&_Mtx), _Owns(_Pmtx->try_lock())
        {   // construct and try to lock
        }

defer_lock 意味着上锁的工作交给程序员来做;而try_to_lock_t 会在构造函数中以try_lock 的形式上锁,这样做显然更加安全。

  • unique_lock 同样不支持拷贝,但是支持移动语义:
    // 示例11
    unique_lock(unique_lock&& _Other) _NOEXCEPT
        : _Pmtx(_Other._Pmtx), _Owns(_Other._Owns)
        {   // destructive copy
        _Other._Pmtx = 0;
        _Other._Owns = false;
        }
  • unique_lock 对于托管对象具有唯一的托管权,这点类似于unique_ptr。从示例11我们可以看到,unique_lock 有两个成员变量:
_Mutex *_Pmtx; //指向托管互斥量的指针
bool _Owns; //标示托管的状态

unique_lock 就是通过_Owns 来标记托管状态的。

最后简单介绍一下lock。其定义如下:

// 示例12
template<class _Lock0,
    class _Lock1,
    class... _LockN> inline
    void lock(_Lock0& _Lk0, _Lock1& _Lk1, _LockN&... _LkN)
    {   // lock N mutexes
    int _Res = 0;
    while (_Res != -1)
        _Res = _Try_lock(_Lk0, _Lk1, _LkN...);
    }

该函数的作用是同时对传入的N个互斥量_LK0 _LK1 _LK2 ... _LKN上锁,其执行的结果是要么全部上锁,要么全部没有上锁。这样做的意义在于防止死锁,对于某个过程,需要占用多把锁的。多个线程执行该过程的时候往往容易造成死锁。lock 函数的引入很大程度上解决了这个问题。下面的示例示例13实现了一个线程安全的swap函数。并模拟10个线程同时试图调用该函数去交换两个全局变量的值。示例中用到了unique_lockdefer_lock标签。同时,为了防止死锁,使用lock 函数试图同时获取两个待交换变量对应的锁。

class Obj {
public:
    Obj(int val): value(val) {}
    int value;
    mutex m;
};
void mySwap(Obj& o1, Obj& o2)
{
    unique_lock<mutex> ulock1(o1.m, std::defer_lock);
    unique_lock<mutex> ulock2(o2.m, std::defer_lock);
    lock(ulock1, ulock2);
    int tmp(move(o1.value));
    o1.value = move(o2.value);
    o2.value = move(tmp);
    cout << "In thread: " << std::this_thread::get_id() << endl;
    cout << "o1 = " << o1.value << ", o2 = " << o2.value << endl;
}
Obj o1(1);
Obj o2(2);
void th_fn()
{
    mySwap(o1, o2);
}
void main()
{
    thread threads[10];
    for (int i = 0; i < 10; ++i)
    {
        threads[i] = thread(th_fn);
    }
    for (int i = 0; i < 10; ++i)
    {
        threads[i].join();
    }
}

程序执行结果如下:

In thread: 10916
o1 = 2, o2 = 1
In thread: 8456
o1 = 1, o2 = 2
In thread: 7028
o1 = 2, o2 = 1
In thread: 9800
o1 = 1, o2 = 2
In thread: 3696
o1 = 2, o2 = 1
In thread: 6480
o1 = 1, o2 = 2
In thread: 11744
o1 = 2, o2 = 1
In thread: 6376
o1 = 1, o2 = 2
In thread: 7164
o1 = 2, o2 = 1
In thread: 12148
o1 = 1, o2 = 2

先写到这里,以后再慢慢补充!

版权声明:本文为博主原创文章,未经博主允许不得转载。 举报

相关文章推荐

C++11:原子操作

在多线程开发中,为了确保数据安全性,经常需要对数据进行加锁、解锁处理。C++11中引入了原子的概念,简而言之就是访问它时它自动加锁解锁,从而使软件开发更为简便。 原子可谓一个既简单又复杂的概念。简单...

在Win32下用C++实现多线程读写锁

读写锁实际是一种特殊的自旋锁,它把对共享资源的访问者划分成读者和写者,读者只对共享资源进行读访问,写者则需要对共享资源进行写操作。这种锁相对于自旋锁而言,能提高并发性,因为在多处理器系统中,它允许同时...

我是如何成为一名python大咖的?

人生苦短,都说必须python,那么我分享下我是如何从小白成为Python资深开发者的吧。2014年我大学刚毕业..

C++ 11 并发控制(锁)

在 《C++11 并发指南三(std::mutex 详解)》一文中我们主要介绍了 C++11 标准中的互斥量(Mutex),并简单介绍了一下两种锁类型。本节将详细介绍一下 C++11 标准的锁类型...

C++11:多线程与锁

多线程是小型软件开发必然的趋势。C++11将多线程相关操作全部集成到标准库中了,省去了某些坑库的编译,真是大大的方便了软件开发。多线程这个库简单方便实用,下面给出简单的例子 #include #in...

C++ 11 多线程--线程管理

说到多线程编程,那么就不得不提并行和并发,多线程是实现并发(并行)的一种手段。并行是指两个或多个独立的操作同时进行。注意这里是同时进行,区别于并发,在一个时间段内执行多个操作。在单核时代,多个线程是并...

Java多线程系列--“JUC锁”11之 Semaphore信号量的原理和示例

概要 本章,我们对JUC包中的信号量Semaphore进行学习。内容包括: Semaphore简介 Semaphore数据结构 Semaphore源码分析(基于JDK1.7.0_40) ...

黑马程序员——11,多线程,同步函数,死锁,一些零散的小知识点

黑马程序员——11,多线程,同步函数,死锁,一些零散的小知识点 //同步函数   class  A              &#...

JAVA基础 day11 多线程 同步代码块 死锁问题

进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位。线程 是进程中的一个独立控制单元。线程控制着进程的执行。当一个程序启动时,就有一个进程被操作系...

多线程11_张孝祥 java5的线程锁技术

转载地址http://www.cnblogs.com/laj12347/p/4403903.html 本例子因为两个线程公用同线程中,使用同一个对象,实现了他们公用一把锁,实现了同一...
返回顶部
收藏助手
不良信息举报
您举报文章:深度学习:神经网络中的前向传播和反向传播算法推导
举报原因:
原因补充:

(最多只允许输入30个字)