简介

  • C++ 标准库

C++ 标准库

<mutex> 是 C++ 标准库提供的头文件,其中定义了一些用于多线程编程的基本互斥量和锁(mutexes和locks)。

主要类和功能:

  1. std::mutex
    • 互斥量,提供了基本的互斥访问,用于保护共享资源。
    • 可以使用 lock()unlock() 方法来手动锁定和解锁。
    • 通常与 std::lock_guardstd::unique_lock 结合使用,实现自动的锁定和解锁。
  2. std::lock_guard
    • 提供了一种简单的方式来管理互斥量的锁定和解锁。
    • 在构造时锁定互斥量,在作用域结束时自动解锁。
  3. std::unique_lock
    • 类似于 std::lock_guard,提供对互斥量的锁定和解锁操作。
    • 拥有更多的灵活性,可以手动控制锁的范围,也可以手动解锁并重新锁定。
  4. std::recursive_mutex
    • 递归互斥量,允许同一线程多次对其进行加锁,避免死锁。
  5. std::timed_mutex
    • 有超时功能的互斥量,在尝试锁定超时后会返回失败。
  6. std::recursive_timed_mutex
    • 有超时功能的递归互斥量。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
#include <thread>
#include <mutex>

std::mutex mtx;
int shared_data = 0;

void increment_shared_data() {
    std::lock_guard<std::mutex> lock(mtx);
    ++shared_data;
}

int main() {
    std::thread t1(increment_shared_data);
    std::thread t2(increment_shared_data);

    t1.join();
    t2.join();

    std::cout << "Shared data: " << shared_data << std::endl;

    return 0;
}

在此示例中,std::mutex 用于保护共享资源 shared_datastd::lock_guard 用于在作用域内锁定互斥量,确保对 shared_data 的增加操作是线程安全的。通过使用互斥量,可以确保多个线程不会同时访问共享资源,避免了竞态条件(race condition)的发生。

C++ 标准库 详解

<mutex> 是 C++ 标准库中的头文件,提供了用于多线程编程的互斥量(mutex)和锁(lock)的基本支持。

主要类和功能:

  1. std::mutex
    • 互斥量类,用于保护共享资源,防止多个线程同时访问。
    • 成员函数有 lock()(锁定互斥量)、try_lock()(尝试锁定互斥量,避免阻塞)、unlock()(解锁互斥量)等。
  2. std::lock_guard
    • 提供自动管理互斥量的锁定和解锁。
    • 在构造时锁定互斥量,在作用域结束时自动解锁。
  3. std::unique_lock
    • std::lock_guard 类似,提供对互斥量的锁定和解锁。
    • 拥有更多的灵活性,允许手动控制锁的范围,也可以手动解锁和重新锁定。
  4. std::recursive_mutex
    • 递归互斥量,允许同一线程多次对其进行加锁,避免死锁。
  5. std::timed_mutex
    • 有超时功能的互斥量,在尝试锁定超时后会返回失败。
  6. std::recursive_timed_mutex
    • 有超时功能的递归互斥量。

锁类型选择:

  • std::lock_guard 适用于简单的互斥量锁定,作用域结束时自动释放锁。
  • std::unique_lock 更灵活,允许手动控制锁的生命周期和多次锁定和解锁。
  • std::timed_mutexstd::recursive_timed_mutex 适用于需要超时特性的情况。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
#include <thread>
#include <mutex>

std::mutex mtx;
int shared_data = 0;

void increment_shared_data() {
    std::lock_guard<std::mutex> lock(mtx);
    ++shared_data;
}

int main() {
    std::thread t1(increment_shared_data);
    std::thread t2(increment_shared_data);

    t1.join();
    t2.join();

    std::cout << "Shared data: " << shared_data << std::endl;

    return 0;
}

在此示例中,std::mutex 用于保护共享资源 shared_datastd::lock_guard 用于在作用域内锁定互斥量,确保对 shared_data 的增加操作是线程安全的。通过使用互斥量,可以确保多个线程不会同时访问共享资源,避免了竞态条件(race condition)的发生。

std::mutex

  • std::mutex 是 C++ 标准库 <mutex> 中定义的类,用于提供互斥量功能,确保在多线程环境中对共享资源的安全访问。

std::mutex 详解

  • 构造函数:
    • std::mutex不允许拷贝构造,也不允许move拷贝,最初产生的mutex对象是处于unlocked状态的
  • lock()
    • 调用线程将锁住该互斥量。
    • 线程调用该函数会发生下面三种情况
      • 如果该互斥量当前没有被锁住,则调用线程将该互斥量锁住,直到调用unlock之前,该线程一直用于该锁
      • 如果当前互斥量被其他线程锁住,则当前的调用线程被阻塞住
      • 如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)
  • unlock()
    • 解锁,释放对互斥量的所有权
  • try_lock()
    • 尝试锁住互斥量,如果互斥量被其他线程占有,则当前线程也不会被阻塞。
    • 线程调用该函数也会出现下面三种情况
      • 如果当前互斥量没有被其他线程占有,则该线程锁住互斥量,直到该线程调用unlock释放互斥量
      • 如果当前互斥量被其他线程锁住,则当前调用线程返回false,而且并不会被阻塞
      • 如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)

主要操作和函数:

  1. lock() 和 unlock()std::mutex 提供了 lock()unlock() 函数用于手动锁定和解锁互斥量。

    1
    2
    3
    4
    5
    
     std::mutex mtx;
    
     mtx.lock(); // 锁定互斥量
     // 对共享资源进行操作
     mtx.unlock(); // 解锁互斥量
    
  2. std::lock_guardstd::lock_guard 是一个 RAII(资源获取即初始化)类,用于自动管理 std::mutex 的锁定和解锁。

    1
    2
    3
    4
    5
    6
    
     std::mutex mtx;
    
     {
         std::lock_guard<std::mutex> lock(mtx); // 在作用域中自动锁定互斥量
         // 对共享资源进行操作
     } // 在作用域结束时自动解锁互斥量
    
  3. try_lock():尝试锁定互斥量,如果不能立即获得锁,try_lock() 函数会立即返回,而不会阻塞线程。

    1
    2
    3
    4
    5
    6
    7
    8
    
     std::mutex mtx;
    
     if (mtx.try_lock()) {
         // 成功获得锁,对共享资源进行操作
         mtx.unlock(); // 解锁互斥量
     } else {
         // 未能立即获得锁,执行其他操作
     }
    
  4. 递归互斥量std::recursive_mutex 类是 std::mutex 的变种,允许同一线程多次对互斥量进行加锁操作,适用于特定场景。

示例用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>
#include <thread>
#include <mutex>

std::mutex mtx;

void threadFunction(int id) {
    mtx.lock(); // 锁定互斥量
    std::cout << "Thread " << id << " is executing..." << std::endl;
    mtx.unlock(); // 解锁互斥量
}

int main() {
    std::thread t1(threadFunction, 1);
    std::thread t2(threadFunction, 2);

    t1.join();
    t2.join();

    return 0;
}

在这个示例中,std::mutex 被用来保护对共享资源(在这里是标准输出流 std::cout)的访问。线程函数通过调用 mtx.lock()mtx.unlock() 来手动锁定和解锁互斥量,确保每个线程在访问共享资源时的独占性。

std::lock_guard

std::lock_guard 是 C++ 标准库中的一个模板类,用于管理互斥量的锁定和解锁操作。它提供了一种简单的方式,在作用域内锁定互斥量,并在作用域结束时自动释放锁。

std::lock_guard 详解

  • std::lock_guard 是C++11中定义的模板类。定义如下:
    1
    
    template <class Mutex> class lock_guard;
    
  • std::lock_guard 对象通常用于管理某个锁对象,因此与Mute RAII相关,方便线程对互斥量上锁,即在某个 std::lock_guard对象的生命周期内,它所管理的锁对象会一直保持上锁状态;而std::lock_guard的生命周期结束之后,它所管理的锁对象会被解锁(类似 std::shared_ptr等智能指针管理动态分配的内存资源)

  • 模板参数Mutex代表互斥量类型,例如std::mutex类型,它应该是一个基本的BasicLockable类型。
  • 标准库中定义几种基本的BasicLockable类型,分别是
    • std::mutex
    • std::recursive_mutex
    • std::timed_mutex
    • std::recursive_timed_mutex
    • std::unique_lock
  • 在lock_guard对象构造时,传入的Mutex对象(即它所管理的Mutex对象)会被当前线程锁住。在lock_guard对象被析构时,它所管理的Mutex对象会自动解锁,由于不需要程序员手动调用lock和unlock对Mutex进行上所和解锁操作,因此这也是最简单安全的上所和解锁方式,尤其是在程序抛出异常后先前已被上锁的Mutex对象可以正确进行解锁操作,极大的简化了程序员编写与Mutex相关的异常处理代码

  • 值的注意的是,lock_guard对象并不负责管理Mutex对象的生命周期,lock_guard对象只是简化了Mutex对象的上锁和解锁操作,方便线程对互斥量上锁,即在某个lock_guard对象的生命周期内,它所管理的锁对象会一直保持上锁状态;而lock_guard的声明周期结束之后,它所管理的锁对象会被解锁。

  • std::lock_guard构造函数如下所示
    1
    2
    3
    
    locking(1) explicit lock_guard(mutex_type& m);
    adopting(2) lock_guard(mutex_type& m, adopt_lock_t tag);
    copy [deleted](3) lock_guard(const lock_guard&) = delete;
    
  • locking初始化
    • lock_guard对象管理Mutex对象m,并在构造函数对m进行上锁(调用m.lock())
  • adopting初始化
    • lock_guard对象管理Mutex对象m,与locking初始化(1)不同的是,Mutex对象m已被当前线程锁住
  • 拷贝构造
    • lock_guard对象的拷贝构造和移动构造(move construction)均被禁用,因此lock_guard对象不可被拷贝构造和移动构造

主要特性和用法:

  1. 自动锁定和解锁std::lock_guard 在构造时锁定互斥量,在作用域结束时自动释放锁。
  2. 简化代码:使用 std::lock_guard 可以避免手动调用 mutexlock()unlock() 方法。
  3. RAII(资源获取即初始化):充分利用了 RAII 的机制,确保在异常或作用域提前退出时,锁都能正确释放,防止死锁和资源泄漏。

示例:

以下是一个简单的示例,展示了 std::lock_guard 的使用方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <iostream>
#include <thread>
#include <mutex>

std::mutex mtx;
int shared_data = 0;

void increment_shared_data() {
    std::lock_guard<std::mutex> lock(mtx); // 在此处锁定互斥量

    // 对共享资源进行操作
    ++shared_data;
    std::cout << "Shared data incremented by 1." << std::endl;

    // 在 lock 对象超出作用域时,自动解锁互斥量
}

int main() {
    std::thread t1(increment_shared_data);
    std::thread t2(increment_shared_data);

    t1.join();
    t2.join();

    std::cout << "Final shared data value: " << shared_data << std::endl;

    return 0;
}

在此示例中,std::lock_guard 类型的对象 lock 在构造时锁定了互斥量 mtx,在作用域结束时,lock 对象超出作用域范围,自动调用析构函数,从而自动解锁了 mtx 互斥量。这确保了 shared_data 的增加操作是在互斥访问下进行的,避免了多线程环境下的竞态条件。

std::unique_lock

std::unique_lock 是 C++ 标准库中提供的一个模板类,用于管理互斥量(std::mutex)的锁定和解锁操作。它提供了更灵活的锁管理机制,相比于 std::lock_guardstd::unique_lock 提供了更多的功能。

std::unique_lock 详解

  • unique_lock 对象以独占所有权的方式管理mutex对象的上锁和解锁操作,所谓独占所有权,就是没有其他的unique_lock对象同时拥有某个mutex对象的所有权

  • 在构造(或移动赋值)时,unique_lock对象需要传递一个Mutex对象作为它的参数,新创建的unique_lock对象负责传入的Mutex对象的上锁和解锁操作。

主要特性和用法:

  1. 灵活性std::unique_lock 对象的锁定和解锁不仅限于作用域的进入和退出,可以手动控制锁的范围。
  2. 构造和析构std::unique_lock 对象在构造时锁定互斥量,在析构时自动解锁。
  3. 锁定和解锁:可通过 lock()unlock() 方法手动锁定和解锁互斥量。
  4. 所有权转移:支持将 std::unique_lock 对象的所有权转移给另一个对象,提供了移动语义。

示例:

以下是一个简单示例,演示了 std::unique_lock 的基本用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>
#include <thread>
#include <mutex>

std::mutex mtx;

void someFunction() {
    std::unique_lock<std::mutex> lock(mtx); // 在此处锁定互斥量

    // 做一些工作
    std::cout << "Doing some work within a critical section." << std::endl;
    
    // 在 lock 超出作用域时自动解锁
}

int main() {
    std::thread t(someFunction);

    // 执行其他操作

    t.join();

    return 0;
}

在这个示例中,std::unique_lock 对象 lock 在构造时锁定了互斥量 mtx,当 lock 超出作用域时,会自动解锁互斥量。这种机制保证了在 std::unique_lock 对象生命周期内,互斥量的锁定状态始终得到正确管理。

std::unique_lock 的另一个优点是,可以手动控制锁的范围,例如使用 lock()unlock() 方法,允许在需要时手动进行锁定和解锁操作,提供更多灵活性。

此外,std::unique_lock 也支持将其所有权转移给另一个 std::unique_lock 对象,这种特性可用于实现更复杂的锁管理场景,比如资源的安全移交。

std::recursive_mutex

std::recursive_mutex 是 C++ 标准库中提供的一个互斥量类,它与 std::mutex 类似,但允许同一个线程对互斥量多次进行锁定。这种特性允许同一线程在已经锁定的情况下再次对其进行锁定,避免了死锁的可能性。 std::recursive_mutex 释放互斥量时需要调用与该锁层次深度相同次数的unlock(),可以理解为lock()次数和unlock()次数相同,除此之外,std::recursive_mutex的特性和std::mutex大致相同

主要特点和用法:

  1. 递归锁定std::recursive_mutex 允许同一个线程对它进行多次锁定。
  2. 避免死锁:递归锁可以使得同一个线程在持有锁的情况下再次对其进行锁定,避免了由于同一线程多次锁定导致的死锁。
  3. 使用方法:与 std::mutex 类似,使用 lock()unlock() 方法进行锁定和解锁。

示例:

以下是一个简单的示例,演示了 std::recursive_mutex 的基本用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <iostream>
#include <thread>
#include <mutex>

std::recursive_mutex r_mtx;
int shared_data = 0;

void increment_shared_data() {
    std::lock_guard<std::recursive_mutex> lock(r_mtx);
    ++shared_data;
    std::cout << "Shared data incremented by 1." << std::endl;
}

void recursive_function(int count) {
    std::lock_guard<std::recursive_mutex> lock(r_mtx);
    if (count > 0) {
        std::cout << "Count: " << count << std::endl;
        recursive_function(count - 1); // 递归调用
    }
}

int main() {
    std::thread t1(increment_shared_data);
    std::thread t2(recursive_function, 3);

    t1.join();
    t2.join();

    std::cout << "Final shared data value: " << shared_data << std::endl;

    return 0;
}

在此示例中,std::recursive_mutex 类型的对象 r_mtx 被用于保护共享资源 shared_data。在函数 recursive_function() 中,r_mtx 被同一线程多次锁定,并通过递归调用自身来展示 std::recursive_mutex 的递归特性。通过递归锁定,同一线程可以多次对 r_mtx 进行加锁,避免了死锁的可能性。

std::timed_mutex

std::timed_mutex 是 C++ 标准库提供的一个互斥量类,它是 std::mutex 的变种,提供了超时功能。std::timed_mutex 允许线程尝试锁定互斥量,但在一段时间内无法成功锁定时,可以设置超时机制,以避免无限期等待。

std::timed_mutex 详解

  • try_lock_for 函数接受一个时间范围,表示在这一段时间范围之内线程如果没有获得锁则被阻塞住(与std::mutex的try_lock()不同,try_lock如果被调用时没有获得锁则直接返回false),如果在此期间其他线程释放了锁,则该线程可以获得对互斥量的锁,如果超时(即在指定时间内还是没有获得锁),则返回false

  • try_lock_until 函数接受一个时间点作为参数,在指定时间点未到来之前线程如果没有获得锁则被阻塞住,如果在此期间其他线程释放了锁,则该线程可以获得对互斥量的锁,如果超时(即在指定时间还是没有获得锁),则返回false

主要特点和用法:

  1. 超时锁定std::timed_mutex 允许线程尝试锁定互斥量,并在超时后返回结果,避免线程无限期等待。
  2. try_lock_for()try_lock_until():提供了超时版本的 try_lock(),允许线程在一段时间内尝试锁定互斥量。
  3. lock()unlock():与 std::mutex 类似,用于手动锁定和解锁互斥量。
  4. 超时策略:可以使用 std::chrono 中的时间点或时间段,设置超时时间。

示例:

以下是一个简单的示例,演示了 std::timed_mutex 的基本用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>

std::timed_mutex t_mtx;
int shared_data = 0;

void try_lock_function() {
    if (t_mtx.try_lock_for(std::chrono::milliseconds(100))) {
        // 成功锁定互斥量
        ++shared_data;
        std::cout << "Shared data incremented by 1." << std::endl;
        t_mtx.unlock();
    } else {
        // 未能在指定时间内锁定互斥量
        std::cout << "Could not acquire lock within specified time." << std::endl;
    }
}

int main() {
    std::thread t(try_lock_function);

    t.join();

    std::cout << "Final shared data value: " << shared_data << std::endl;

    return 0;
}

在此示例中,std::timed_mutex 类型的对象 t_mtx 用于保护共享资源 shared_datatry_lock_function() 函数尝试在100毫秒内锁定 t_mtx,如果在超时时间内成功获得了锁,则增加 shared_data 的值,否则在未获得锁时输出一条提示信息。这样可以避免线程无限期等待互斥量的锁定,提高程序的鲁棒性。

std::recursive_timed_mutex

std::recursive_timed_mutex 是 C++ 标准库提供的一个互斥量类,结合了 std::recursive_mutexstd::timed_mutex 的特性,允许同一个线程多次对其进行锁定,并提供了超时功能。

主要特点和用法:

  1. 递归和超时std::recursive_timed_mutex 允许同一个线程对互斥量多次进行锁定,并在一定时间内尝试锁定互斥量,提供了递归性和超时性。
  2. try_lock_for()try_lock_until():提供了超时版本的 try_lock(),允许线程在一段时间内尝试锁定互斥量。
  3. lock()unlock():与 std::recursive_mutex 类似,用于手动锁定和解锁互斥量。

示例:

以下是一个简单的示例,演示了 std::recursive_timed_mutex 的基本用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>

std::recursive_timed_mutex rt_mtx;
int shared_data = 0;

void recursive_lock_function(int count) {
    std::unique_lock<std::recursive_timed_mutex> lock(rt_mtx, std::defer_lock);

    if (count > 0) {
        if (lock.try_lock_for(std::chrono::milliseconds(100))) {
            // 成功锁定互斥量
            ++shared_data;
            std::cout << "Shared data incremented by 1." << std::endl;
            lock.unlock();
        } else {
            // 未能在指定时间内锁定互斥量
            std::cout << "Could not acquire lock within specified time." << std::endl;
        }
        recursive_lock_function(count - 1); // 递归调用
    }
}

int main() {
    recursive_lock_function(3);

    std::cout << "Final shared data value: " << shared_data << std::endl;

    return 0;
}

在此示例中,std::recursive_timed_mutex 类型的对象 rt_mtx 用于保护共享资源 shared_datarecursive_lock_function() 函数尝试在100毫秒内锁定 rt_mtx,如果在超时时间内成功获得了锁,则增加 shared_data 的值,并在解锁后进行递归调用。这样,线程可以多次锁定同一个 std::recursive_timed_mutex 对象,并提供了超时机制。

C++ std::call_once函数 详解

std::call_once 是 C++11 标准库 <mutex> 头文件中的一个函数,用于保证某个函数在多线程环境下只被调用一次。它通常与 std::once_flag 结合使用。

这个函数的原型如下:

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

其中:

  • flag 是一个 std::once_flag 对象,用于标识某个函数是否已经被调用过。
  • f 是一个可调用对象,可以是函数指针、函数对象或者是 lambda 表达式。
  • argsf 执行时所需的参数。

std::call_once 的工作原理是,它会检查 std::once_flag 对象是否已经被设置过。如果没有,它会调用传递给它的可调用对象 f,并将 std::once_flag 对象设置为已调用状态。这样,即使在多线程环境下,也只会有一个线程调用 f

一般来说,std::call_once 用于在多线程环境下执行初始化工作或者执行某个需要确保只执行一次的操作。例如,可以用它来确保在多线程环境下只创建一个全局对象。

以下是一个简单的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <iostream>
#include <mutex>
#include <thread>

std::once_flag flag;

void initialize_shared_resource() {
    std::cout << "Initializing shared resource\n";
    // 在这里执行初始化工作
}

void thread_function() {
    std::call_once(flag, initialize_shared_resource);
}

int main() {
    std::thread t1(thread_function);
    std::thread t2(thread_function);
    std::thread t3(thread_function);
    std::thread t4(thread_function);

    t1.join();
    t2.join();
    t3.join();
    t4.join();

    return 0;
}

在这个示例中,initialize_shared_resource 函数会被多个线程调用,但是由于 std::call_once 的作用,它只会被调用一次。

C++ std::once_flag 详解

std::once_flag 是 C++11 标准库 <mutex> 头文件中定义的一个标准库类,用于在多线程环境中确保某个函数或某段代码只被执行一次。

std::once_flag 的作用是作为 std::call_once 函数的参数,用于标记某个函数是否已经被调用过。它通常在多线程环境中与 std::call_once 函数结合使用。

这个类的定义相对简单,它通常只是一个轻量级的标志,标识某个函数是否已经被调用过。

以下是 std::once_flag 的简单示例用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>
#include <mutex>

std::once_flag flag;

void initialize_shared_resource() {
    std::cout << "Initializing shared resource\n";
    // 在这里执行初始化工作
}

int main() {
    std::call_once(flag, initialize_shared_resource); // 仅会执行一次
    std::call_once(flag, [](){ std::cout << "This won't run\n"; }); // 由于flag已被设置,这里的lambda表达式不会执行

    return 0;
}

在这个示例中,initialize_shared_resource 函数仅被执行一次,即使在多个线程中调用了 std::call_once。这是因为 std::once_flag 确保了其中的初始化代码仅被执行一次。

总的来说,std::once_flag 提供了一种在多线程环境中安全地执行某个函数或代码段的机制,确保它们只被执行一次。