关于c++:C-mutex-类及方法介绍

头文件介绍

Mutex 系列类(四种)

  • std::mutex,最根本的 Mutex 类。
  • std::recursive_mutex,递归 Mutex 类。
  • std::time_mutex,定时 Mutex 类。
  • std::recursive_timed_mutex,定时递归 Mutex 类。

Lock 类(两种)

  • std::lock_guard,与 Mutex RAII 相干,不便线程对互斥量上锁。
  • std::unique_lock,与 Mutex RAII 相干,不便线程对互斥量上锁,但提供了更好的上锁和解锁管制。

其余类型

  • std::once_flag 标记位,与函数std::call_once 配合应用
  • std::adopt_lock_t
  • std::defer_lock_t
  • std::try_to_lock_t

函数

  • std::try_lock,尝试同时对多个互斥量上锁。
  • std::lock,能够同时对多个互斥量上锁。
  • std::scoped_lock RALL,能够同时对多个互斥量上锁并保障解锁。
  • std::call_once,如果多个线程须要同时调用某个函数,call_once 能够保障多个线程对该函数只调用一次。

mutex

mutex 的全名为 mutual exclusion(互斥体),其 object 用来帮助采取独占且排他的形式管制“对资源(object或多个Object的组合)的并发拜访”。

  • 构造函数,std::mutex不容许拷贝结构,也不容许 move 拷贝,最后产生的 mutex 对象是处于 unlocked 状态的。
  • lock(),调用线程将锁住该互斥量。线程调用该函数会产生上面 3 种状况:

    • (1). 如果该互斥量以后没有被锁住,则调用线程将该互斥量锁住,直到调用 unlock之前,该线程始终领有该锁。
    • (2). 如果以后互斥量被其余线程锁住,则以后的调用线程被阻塞住。
    • (3). 如果以后互斥量被以后调用线程锁住,则会产生死锁(deadlock)。
  • unlock(), 解锁,开释对互斥量的所有权。
  • try\_lock(),尝试锁住互斥量,如果互斥量被其余线程占有,则以后线程也不会被阻塞。线程调用该函数也会呈现上面 3 种状况,

    • (1). 如果以后互斥量没有被其余线程占有,则该线程锁住互斥量,直到该线程调用 unlock 开释互斥量。
    • (2). 如果以后互斥量被其余线程锁住,则以后调用线程返回 false,而并不会被阻塞掉。
    • (3). 如果以后互斥量被以后调用线程锁住,则会产生死锁(deadlock)。

上面给出一个与 std::mutex 的小例子:

#include <iostream>  // std::cout
#include <mutex>     // std::mutex
#include <thread>    // std::thread

volatile int counter(0);  // non-atomic counter
std::mutex mtx;           // locks access to counter

void attempt_10k_increases() {
    for (int i = 0; i < 10000; ++i) {
        if (mtx.try_lock()) {  // only increase if currently not locked:
            ++counter;
            mtx.unlock();
        }
    }
}

int main(int argc, const char* argv[]) {
    std::thread threads[10];
    for (int i = 0; i < 10; ++i)
        threads[i] = std::thread(attempt_10k_increases);

    for (auto& th : threads) th.join();
    std::cout << counter << " successful increases of the counter.\n";
    return 0;
}

std::recursive\_mutex 介绍

std::recursive_mutexstd::mutex 一样,也是一种能够被上锁的对象,然而和 std::mutex 不同的是,std::recursive_mutex 容许同一个线程对互斥量屡次上锁(即递归上锁),来取得对互斥量对象的多层所有权,std::recursive_mutex 开释互斥量时须要调用与该锁档次深度雷同次数的 unlock(),可了解为 lock() 次数和 unlock() 次数雷同,除此之外,std::recursive_mutex 的个性和 std::mutex 大致相同。

recursive_mutex g_mutex;
void print123() {
    g_mutex.lock();
    g_mutex.lock();
    for (int i = 0; i < 3; i++) {
        this_thread::sleep_for(chrono::milliseconds(100));
        cout << i + 1;
    }
    g_mutex.unlock();
    g_mutex.unlock();
}

std::time\_mutex 介绍

std::time_mutexstd::mutex 多了两个成员函数,try_lock_for()try_lock_until()

try_lock_for 函数承受一个工夫范畴,示意在这一段时间范畴之内线程如果没有取得锁则被阻塞住(与 std::mutextry_lock() 不同,try_lock 如果被调用时没有取得锁则间接返回 false),如果在此期间其余线程开释了锁,则该线程能够取得对互斥量的锁,如果超时(即在指定工夫内还是没有取得锁),则返回 false。

try_lock_until 函数则承受一个工夫点作为参数,在指定工夫点未到来之前线程如果没有取得锁则被阻塞住,如果在此期间其余线程开释了锁,则该线程能够取得对互斥量的锁,如果超时(即在指定工夫内还是没有取得锁),则返回 false。

上面的小例子阐明了 std::time_mutex 的用法:

#include <chrono>    // std::chrono::milliseconds
#include <iostream>  // std::cout
#include <mutex>     // std::timed_mutex
#include <thread>    // std::thread

std::timed_mutex mtx;

void fireworks() {
    // waiting to get a lock: each thread prints "-" every 200ms:
    while (!mtx.try_lock_for(std::chrono::milliseconds(200))) {
        std::cout <<"-";
    }
    // got a lock! - wait for 1s, then this thread prints "*"
    std::this_thread::sleep_for(std::chrono::milliseconds(1000));
    std::cout << "*\n";
    mtx.unlock();
}

int main() {
    std::thread threads[10];
    // spawn 10 threads:
    for (int i = 0; i < 10; ++i) threads[i] = std::thread(fireworks);

    for (auto& th : threads) th.join();

    return 0;
}

std::recursive\_timed\_mutex 介绍

std:recursive_mutexstd::mutex 的关系一样,std::recursive_timed_mutex 的个性也能够从 std::timed_mutex 推导进去,感兴趣的同鞋能够自行查阅。

std::lock\_guard 介绍

与 Mutex RAII 相干,不便线程对互斥量上锁和开释。例子

#include <iostream>   // std::cout
#include <mutex>      // std::mutex, std::lock_guard
#include <stdexcept>  // std::logic_error
#include <thread>     // std::thread

std::mutex mtx;

void print_even(int x) {
    if (x % 2 == 0)
        std::cout << x << " is even\n";
    else
        throw(std::logic_error("not even"));
}

void print_thread_id(int id) {
    try {
        // 应用 lock_guard 锁定 mtx 可确保在销毁/异样时解锁:
        std::lock_guard<std::mutex> lck(mtx);
        print_even(id);
    } catch (std::logic_error&) {
        std::cout << "[exception caught]\n";
    }
}

int main() {
    std::thread threads[10];
    // spawn 10 threads:
    for (int i = 0; i < 10; ++i)
        threads[i] = std::thread(print_thread_id, i + 1);

    for (auto& th : threads) th.join();

    return 0;
}

std::unique\_lock 介绍

与 Mutex RAII 相干,不便线程对互斥量上锁,但提供了更好的上锁和解锁管制。

unique_lock 是通用互斥包装器,容许提早锁定、锁定的有时限尝试、递归锁定、所有权转移和与条件变量一起应用。

unique_lock 可挪动,但不可复制——它满足可挪动结构 (MoveConstructible) 和可挪动赋值 (MoveAssignable) 但不满足可复制结构 (CopyConstructible) 或可复制赋值 (CopyAssignable) 。

类 unique\_lock 满足根本可锁定 (BasicLockable) 要求。若 Mutex 满足可锁定 (Lockable) 要求,则 unique\_lock 亦满足可锁定 (Lockable) 要求(例如:能用于 std::lock ) ;若 Mutex 满足可定时锁定 (TimedLockable) 要求,则 unique\_lock 亦满足可定时锁定 (TimedLockable) 要求。

性能与 std::lock_guard 相似

#include <iostream>  // std::cout
#include <mutex>     // std::mutex, std::unique_lock
#include <thread>    // std::thread

std::mutex mtx;  // mutex for critical section

void print_block(int n, char c) {
    // 独占std::cout:
    std::unique_lock<std::mutex> lck(mtx);
    for (int i = 0; i < n; ++i) {
        std::cout << c;
    }
    std::cout << '\n';
}

int main() {
    std::thread th1(print_block, 50, '*');
    std::thread th2(print_block, 50, '$');

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

    return 0;
}

std::lock 介绍

template< class Lockable1, class Lockable2, class... LockableN >
void lock( Lockable1& lock1, Lockable2& lock2, LockableN&... lockn );

锁定给定的可锁定 (Lockable) 对象 lock1lock2...lockn用免死锁算法防止死锁

以对 lock 、 try_lock 和 unlock 的未指定系列调用锁定对象。若调用 lock 或 unlock 导致异样,则在重抛前对任何已锁的对象调用 unlock 。

#include <chrono>
#include <functional>
#include <iostream>
#include <mutex>
#include <string>
#include <thread>
#include <vector>

struct Employee {
    Employee(std::string id) : id(id) {}
    std::string id;
    std::vector<std::string> lunch_partners;
    std::mutex m;  // 每一个对象都有一个锁
    std::string output() const {
        std::string ret = "Employee " + id + " has lunch partners: ";
        for (const auto &partner : lunch_partners) ret += partner + " ";
        return ret;
    }
};

void send_mail(Employee &, Employee &) {
    // 模仿耗时的发信操作
    std::this_thread::sleep_for(std::chrono::seconds(1));
}

void assign_lunch_partner(Employee &e1, Employee &e2) {
    static std::mutex io_mutex;
    {
        std::lock_guard<std::mutex> lk(io_mutex);
        std::cout << e1.id << " and " << e2.id << " are waiting for locks"
                  << std::endl;
    }

    // 用 std::lock 取得二个锁,而不放心对 assign_lunch_partner
    // 的其余调用会死锁咱们
    {
        std::lock(e1.m, e2.m);
        std::lock_guard<std::mutex> lk1(e1.m, std::adopt_lock);// adopt_lock示意曾经获取锁了,看上一行代码,这样的程序是保障不死锁
        std::lock_guard<std::mutex> lk2(e2.m, std::adopt_lock);
        // 等价代码(若须要 unique_locks ,例如对于条件变量)
        //        std::unique_lock<std::mutex> lk1(e1.m, std::defer_lock); // defer_lock 示意提早上锁
        //        std::unique_lock<std::mutex> lk2(e2.m, std::defer_lock);
        //        std::lock(lk1, lk2);
        // C++17 中可用的较优解法
        //        std::scoped_lock lk(e1.m, e2.m);
        {
            std::lock_guard<std::mutex> lk(io_mutex);
            std::cout << e1.id << " and " << e2.id << " got locks" << std::endl;
        }
        e1.lunch_partners.push_back(e2.id);
        e2.lunch_partners.push_back(e1.id);
    }
    send_mail(e1, e2);
    send_mail(e2, e1);
}

int main() {
    Employee alice("alice"), bob("bob"), christina("christina"), dave("dave");

    // 在平行线程指派,因为发邮件给用户告知午餐指派,会耗费长时间
    std::vector<std::thread> threads;
    threads.emplace_back(assign_lunch_partner, std::ref(alice), std::ref(bob));
    threads.emplace_back(assign_lunch_partner, std::ref(christina),
                         std::ref(bob));
    threads.emplace_back(assign_lunch_partner, std::ref(christina),
                         std::ref(alice));
    threads.emplace_back(assign_lunch_partner, std::ref(dave), std::ref(bob));

    for (auto &thread : threads) thread.join();
    std::cout << alice.output() << '\n'
              << bob.output() << '\n'
              << christina.output() << '\n'
              << dave.output() << '\n';
}

std::try\_lock

emplate< class Lockable1, class Lockable2, class... LockableN>
int try_lock( Lockable1& lock1, Lockable2& lock2, LockableN&... lockn);

尝试锁定每个给定的可锁定 (Lockable) 对象 lock1 、 lock2 、 ... 、 lockn ,通过以从头开始的顺序调用 try_lock 。

若调用 try_lock 失败,则不再进一步调用 try_lock ,并对任何已锁对象调用 unlock ,返回锁定失败对象的 0 底下标。

若调用 try_lock 抛出异样,则在重抛前对任何已锁对象调用 unlock 。

锁定胜利的话返回 -1,锁定失败的话返回加锁对象的索引值。

#include <chrono>
#include <functional>
#include <iostream>
#include <mutex>
#include <thread>
#include <vector>

void increment(int &counter, std::mutex &m, const char *desc) {
    for (int i = 0; i < 10; ++i) {
        std::unique_lock<std::mutex> lock(m);
        ++counter;
        std::cout << desc << ": " << counter << '\n';
        lock.unlock();
        std::this_thread::sleep_for(std::chrono::seconds(1));
    }
}

int main() {
    int foo_count = 0;
    std::mutex foo_count_mutex;
    int bar_count = 0;
    std::mutex bar_count_mutex;
    int overall_count = 0;
    bool done = false;
    std::mutex done_mutex;

    std::thread increment_foo(increment, std::ref(foo_count),
                              std::ref(foo_count_mutex), "foo");
    std::thread increment_bar(increment, std::ref(bar_count),
                              std::ref(bar_count_mutex), "bar");

    std::thread update_overall([&]() {
        done_mutex.lock();
        while (!done) {
            done_mutex.unlock();
            int result = std::try_lock(foo_count_mutex, bar_count_mutex);
            if (result == -1) { // 对两个 mutex 都上了锁
                overall_count += foo_count + bar_count; // 读取两个值
                foo_count = 0;
                bar_count = 0;
                std::cout << "overall: " << overall_count << '\n';
                foo_count_mutex.unlock(); // 开释
                bar_count_mutex.unlock(); // 开释
            }
            std::this_thread::sleep_for(std::chrono::seconds(2));
            done_mutex.lock();
        }
        done_mutex.unlock();
    });

    increment_foo.join();
    increment_bar.join();
    done_mutex.lock();
    done = true;
    done_mutex.unlock();
    update_overall.join();

    std::cout << "Done processing\n"
              << "foo: " << foo_count << '\n'
              << "bar: " << bar_count << '\n'
              << "overall: " << overall_count << '\n';
}

std::scoped\_lock

类 scoped_lock 是提供便当 RAII 格调机制的互斥包装器,它在作用域块的存在期间占有一或多个互斥。

创立 scoped_lock 对象时,它试图获得给定互斥的所有权。管制来到创立 scoped_lock 对象的作用域时,析构 scoped_lock 并以逆序开释互斥。若给出数个互斥,则应用免死锁算法,如同以 std::lock 。

scoped_lock 类不可复制。

上一节中的代码节选,其中三种加锁成果雷同:

{    
    // 应用 lock_guard, 这样的程序是保障不死锁
    // std::lock(e1.m, e2.m);
    // std::lock_guard<std::mutex> lk1(e1.m, std::adopt_lock);
    // std::lock_guard<std::mutex> lk2(e2.m, std::adopt_lock);

    // 等价代码(若须要 unique_locks ,例如对于条件变量)
    //        std::unique_lock<std::mutex> lk1(e1.m, std::defer_lock);
    //        std::unique_lock<std::mutex> lk2(e2.m, std::defer_lock);
    //        std::lock(lk1, lk2);

    // C++17 中可用的较优解法:
    std::scoped_lock lk(e1.m, e2.m);
    {
        std::lock_guard<std::mutex> lk(io_mutex);
        std::cout << e1.id << " and " << e2.id << " got locks" << std::endl;
    }
    e1.lunch_partners.push_back(e2.id);
    e2.lunch_partners.push_back(e1.id);
}

std::defer\_lock, std::try\_to\_lock, std::adopt\_lock

配合 std::lock_guardstd::unique_lockstd::shared_lock 指定锁定策略。

类型 成果
defer_lock_t 不取得互斥的所有权。示意该互斥元在结构时放弃未被锁定,这个锁就能够在这之后通过lock来上锁,即提早上锁。
try_to_lock_t 尝试取得互斥的所有权而不阻塞,相当于调用try\_lock。
adopt_lock_t 假如调用方线程已领有互斥的所有权,此时不再上锁,只会转移所有权。

std::adopt_lock 示意已拿到锁:

std::mutex test5_mutex;
void test5() {
    test5_mutex.lock(); // 获取锁
        // 交给 lock_guard 来保存,保障锁的开释,ps:有点向go中的defer
    lock_guard<std::mutex> lg(test5_mutex, std::adopt_lock); 
    
    cout << "hello test5" << endl;
}
void test5(int) {
    test5_mutex.lock(); // 获取所
    cout << "hello test5(int)" << endl;
    test5_mutex.unlock(); // 开释锁
}

int main() {
    thread t1([]() { test5(); });
    thread t2([]() { test5(2); });

    t1.join();
    t2.join();
    return 0;
}

std::call\_once

template< class Callable, class... Args >
void call_once( std::once_flag& flag, Callable&& f, Args&&... args );

精确执行一次可调用 (Callable) 对象 f ,即便同时从多个线程调用。

细节为:

  • 若在调用 call_once 的时刻, flag 批示曾经调用了 f ,则 call_once 立刻返回(称这种对 call_once 的调用为\_消极\_)。
  • 否则, call_once 以参数 std​::​forward<Args>(args)... 调用 ​std​::​forward<Callable>(f) (如同用 std::invoke )。不同于 std::thread 构造函数或 std::async ,不挪动或复制参数,因为不须要转移它们到另一执行线程(称这种对 call_once 的调用为\_踊跃\_)。
  • 若该调用抛异样,则流传异样给 call_once 的调用方,并且不翻转 flag ,以令其余调用将失去尝试(这种对 call_once 的调用被称为\_异样\_)。
  • 若该调用失常返回(这种对 call_once 的调用被称为\_返回\_),则翻转 flag ,并保障以同一 flag 对 call_once 的其余调用为\_消极\_。

同一 flag 上的所有\_踊跃\_调用组成独自全序,它们由零或多个\_异样\_调用后随一个\_返回\_调用组成。该程序中,每个\_踊跃\_调用的结尾同步于下个\_踊跃\_调用。

从\_返回\_调用的返回同步于同一 flag 上的所有\_消极\_调用:这示意保障所有对 call_once 的同时调用都察看到\_踊跃\_调用所做的任何副效应,而无需额定同步。

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

std::once_flag flag1, flag2;

void simple_do_once() {
    std::call_once(flag1,
                   []() { std::cout << "Simple example: called once\n"; });
}

void may_throw_function(bool do_throw) {
    if (do_throw) {
        // 这会呈现多于一次, 因为产生异样后会重置 flag
        std::cout << "throw: call_once will retry\n";  
        throw std::exception();
    }
    std::cout
        << "Didn't throw, call_once will not attempt again\n";  // 保障一次
}

void do_once(bool do_throw) {
    try {
        std::call_once(flag2, may_throw_function, do_throw);
    } catch (...) {
    }
}

int main() {
    std::thread st1(simple_do_once);
    std::thread st2(simple_do_once);
    std::thread st3(simple_do_once);
    std::thread st4(simple_do_once);
    st1.join();
    st2.join();
    st3.join();
    st4.join();

    std::thread t1(do_once, true);
    std::thread t2(do_once, true);
    std::thread t3(do_once, false);
    std::thread t4(do_once, true);
    t1.join();
    t2.join();
    t3.join();
    t4.join();
}

很多代码示例来自 C++ 参考手册

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理