互斥量的基本概念

  • 临界资源:每次只允许一个线程进行访问(读/写)的资源
  • 线程间的互斥(竞争):多个线程在同一时刻都需要访问临界资源
  • mutex 类(互斥量)是一把线程锁,保证线程间的互斥

    • 利用线程锁能够保证临界资源的安全

互斥量的用法

lock

  • mutex.lock()

    • 当锁空闲时,获取并继续执行
    • 当锁被获取时,阻塞并等待锁释放
  • mutex.unlock()

    • 释放锁(同一把锁的获取和释放必须在同一线程中成对出现)

    如果 mutex 在调用 unlock() 时处于空闲状态,那么程序的行为是未定义的

lock_guard

  • 在 lock_guard 对象构造时,传入的 mutex 对象(即它管理的 mutex 对象)会被当前线程锁住。
  • 在 lock_guard 对象被析构时,它所管理的 mutex 对象会自动解锁,不再需要手动调用 lock 和 unlock 对mutex 进行上锁和解锁操作;
  • lock_guard 对象并不负责 mutex 对象的生命周期,只是简化了上锁和解锁操作;
  • 这种采用“资源分配时初始化”(RAII)方法来加锁、解锁,避免了在临界区中因为抛出异常或return等操作导致没有解锁就退出的问题。这极大的简化了编写mutex相关的异常处理代码。
#include <iostream>
#include <thread>
#include <queue>
#include <mutex>

using namespace std;

class Handle {
public:
    void inMsgRevQueue()
    {
        for (int i=0; i<100000; ++i)
        {
            m_mutex.lock();                     // 注意这里 !!!

            cout << "inMsgRevQueue() : " << i << endl;
            m_queue.push(i);

            m_mutex.unlock();                   // 注意这里 !!!
        }
    }

    void outMsgRevQueue()
    {
        for (int i=0; i<100000; ++i)
        {
            lock_guard<mutex> lck(m_mutex);     // 注意这里 !!!

            if (!m_queue.empty())
            {
                cout << "outMsgRevQueue() : " << m_queue.front() << endl;
                m_queue.pop();
            }
        }
    }

private:
    queue<int> m_queue;
    mutex m_mutex;
};

int main()
{
    cout << "main begin" << endl;

    Handle handler;

    thread th1(&Handle::inMsgRevQueue, std::ref(handler));
    thread th2(&Handle::outMsgRevQueue, std::ref(handler));

    th1.join();
    th2.join();

    cout << "main end" << endl;

    return 0;
}

输出:

......

outMsgRevQueue() : 98267
outMsgRevQueue() : 98268
outMsgRevQueue() : 98269
outMsgRevQueue() : 98270
main end

死锁

  • 概念

    • 线程间互相等待临界资源而造成彼此无法继续向下运行
  • 发生死锁的条件

    • 系统存在多个临界资源且临界资源不可抢占
    • 线程需要多个临界资源才能继续运行

image.png
image.png

死锁的演示

#include <iostream>
#include <thread>
#include <queue>
#include <mutex>

using namespace std;

class Handle {
public:
    void inMsgRevQueue()
    {
        for (int i=0; i<100000; ++i)
        {
            m_mutex1.lock();
            m_mutex2.lock();

            cout << "inMsgRevQueue() : " << i << endl;
            m_queue.push(i);

            m_mutex2.unlock();
            m_mutex1.unlock();
        }
    }

    void outMsgRevQueue()
    {
        for (int i=0; i<100000; ++i)
        {
            m_mutex2.lock();
            m_mutex1.lock();

            if (!m_queue.empty())
            {
                cout << "outMsgRevQueue() : " << m_queue.front() << endl;
                m_queue.pop();
            }

            m_mutex1.unlock();
            m_mutex2.unlock();
        }
    }

private:
    queue<int> m_queue;
    mutex m_mutex1;
    mutex m_mutex2;
};

int main()
{
    cout << "main begin" << endl;

    Handle handler;

    thread th1(&Handle::inMsgRevQueue, std::ref(handler));
    thread th2(&Handle::outMsgRevQueue, std::ref(handler));

    th1.join();
    th2.join();

    cout << "main end" << endl;

    return 0;
}

输出:

inMsgRevQueue() : 63
inMsgRevQueue() : 64
inMsgRevQueue() : 65
inMsgRevQueue() : 66
inMsgRevQueue() : 67

不再输出 ...

死锁的避免

死锁的一般解决方案

  • 对所有的临界资源都分配一个唯一的编号 (n1, n2, n3)
  • 对应的线程锁也分配同样的序号 (m1, m2, m3)
  • 系统中的每个线程按照严格递增(递减)的次序请求资源

image.png

#include <iostream>
#include <thread>
#include <queue>
#include <mutex>

using namespace std;

class Handle {
public:
    void inMsgRevQueue()
    {
        for (int i=0; i<100000; ++i)
        {
            m_mutex1.lock();
            m_mutex2.lock();

            cout << "inMsgRevQueue() : " << i << endl;
            m_queue.push(i);

            m_mutex2.unlock();
            m_mutex1.unlock();
        }
    }

    void outMsgRevQueue()
    {
        for (int i=0; i<100000; ++i)
        {
            m_mutex1.lock();
            m_mutex2.lock();

            if (!m_queue.empty())
            {
                cout << "outMsgRevQueue() : " << m_queue.front() << endl;
                m_queue.pop();
            }

            m_mutex2.unlock();
            m_mutex1.unlock();
        }
    }

private:
    queue<int> m_queue;
    mutex m_mutex1;
    mutex m_mutex2;
};

int main()
{
    cout << "main begin" << endl;

    Handle handler;

    thread th1(&Handle::inMsgRevQueue, std::ref(handler));
    thread th2(&Handle::outMsgRevQueue, std::ref(handler));

    th1.join();
    th2.join();

    cout << "main end" << endl;

    return 0;
}

输出:

inMsgRevQueue() : 99995
inMsgRevQueue() : 99996
inMsgRevQueue() : 99997
inMsgRevQueue() : 99998
inMsgRevQueue() : 99999
main end

std::lock 函数模板

lock(mutex1, mutex2, ...); 一次锁定两个或多个锁,用于处理多个互斥量

  • 当全部锁空闲时,获取并继续执行
  • 当全部锁未被获取时,阻塞并等待锁释放

    • 当只获取其中一部分锁,另一部分锁被其它线程锁定时,则释放当前已获取的锁,继续等待并重新尝试获取全部锁,以此防止死锁
#include <iostream>
#include <thread>
#include <queue>
#include <mutex>

using namespace std;

class Handle {
public:
    void inMsgRevQueue()
    {
        for (int i=0; i<100000; ++i)
        {
            m_mutex1.lock();
            m_mutex2.lock();

            cout << "inMsgRevQueue() : " << i << endl;
            m_queue.push(i);

            m_mutex2.unlock();
            m_mutex1.unlock();
        }
    }

    void outMsgRevQueue()
    {
        for (int i=0; i<100000; ++i)
        {
            lock(m_mutex1, m_mutex2);       // 注意这里 !!!

            if (!m_queue.empty())
            {
                cout << "outMsgRevQueue() : " << m_queue.front() << endl;
                m_queue.pop();
            }

            m_mutex2.unlock();
            m_mutex1.unlock();
        }
    }

private:
    queue<int> m_queue;
    mutex m_mutex1;
    mutex m_mutex2;
};

int main()
{
    cout << "main begin" << endl;

    Handle handler;

    thread th1(&Handle::inMsgRevQueue, std::ref(handler));
    thread th2(&Handle::outMsgRevQueue, std::ref(handler));

    th1.join();
    th2.join();

    cout << "main end" << endl;

    return 0;
}

输出:

outMsgRevQueue() : 99599
outMsgRevQueue() : 99600
outMsgRevQueue() : 99601
outMsgRevQueue() : 99602
main end

std::lock_guard 的 std::adopt_lock 参数

使用 adopt_lock 构造的 unique_lock 和 lock_guard 对象在构造时不会锁定mutex对象,而是假定它已被当前线程锁定。

#include <iostream>
#include <thread>
#include <queue>
#include <mutex>

using namespace std;

class Handle {
public:
    void inMsgRevQueue()
    {
        for (int i=0; i<100000; ++i)
        {
            m_mutex.lock();

            cout << "inMsgRevQueue() : " << i << endl;
            m_queue.push(i);

            m_mutex.unlock();
        }
    }

    void outMsgRevQueue()
    {
        for (int i=0; i<100000; ++i)
        {
            m_mutex.lock();

            lock_guard<mutex> lck(m_mutex, adopt_lock);     // 注意这里 !!!

            if (!m_queue.empty())
            {
                cout << "outMsgRevQueue() : " << m_queue.front() << endl;
                m_queue.pop();
            }
        }
    }

private:
    queue<int> m_queue;
    mutex m_mutex;
};

int main()
{
    cout << "main begin" << endl;

    Handle handler;

    thread th1(&Handle::inMsgRevQueue, std::ref(handler));
    thread th2(&Handle::outMsgRevQueue, std::ref(handler));

    th1.join();
    th2.join();

    cout << "main end" << endl;

    return 0;
}

输出:

......

outMsgRevQueue() : 97515
outMsgRevQueue() : 97516
outMsgRevQueue() : 97517
outMsgRevQueue() : 97518
main end

TianSong
734 声望138 粉丝

阿里山神木的种子在3000年前已经埋下,今天不过是看到当年注定的结果,为了未来的自己,今天就埋下一颗好种子吧