C++线程、多线程教程详解(全网最全、示例最多、最详细)(第一篇)

目录

 A、线程/多线程基础

一、C++11 创建线程的几种方式

1.1 使用函数指针

1.2 使用 lambda 表达式

1.3 使用成员函数

1.4 使用可调用对象 (Functor)

二、定义一个线程类

三、join() 与 detach() 的详细用法及区别

3.1 join() 的用法

3.2 detach() 的用法

3.3 join() 与 detach() 的区别总结

四、std::this_thread

4.1、主要功能

std::this_thread::get_id()

std::this_thread::yield()

std::this_thread::sleep_for()

std::this_thread::sleep_until()

4.2、各方法的用法及适用场景

1. std::this_thread::get_id()

2. std::this_thread::yield()

3. std::this_thread::sleep_for()

4. std::this_thread::sleep_until()

4.3、std::this_thread::sleep_for 与 std::this_thread::sleep_until 的区别

注:sleep_for 与 sleep_until 对比示例

五、std::mutex 详解

主要功能

使用场景

代码示例

1. 基本使用 std::mutex

2. 使用 try_lock()

3. 使用 std::lock_guard 简化互斥锁管理

4. 使用 std::scoped_lock 同时锁定多个互斥量

重要注意事项

总结

六、std::unique_lock

主要功能:

构造函数与锁定策略

使用示例

1. 基本使用:自动锁定与解锁

2. 手动锁定与解锁

3. 使用 try_lock 尝试锁定

4. 使用 adopt_lock 接管已经锁定的互斥量

std::unique_lock 与 std::lock_guard 的区别

总结

七、std::condition_variable::wait

std::condition_variable::wait 的原理

wait() 的使用场景

代码示例

1. 基本使用 wait() 和 notify_one()

2. 使用 notify_all() 唤醒所有等待线程

3. 防止虚假唤醒

4. wait_for() 和 wait_until()

4.1 使用 wait_for() 超时等待

4.2 使用 wait_until() 等待到指定时间点

总结


 A、线程/多线程基础

一、C++11 创建线程的几种方式

1.1 使用函数指针

这是最基本的线程创建方式,通过将一个普通的函数指针传递给 std::thread

#include 
#include 

void threadFunction(int x) {
    std::cout << "Thread function called with value: " << x << std::endl;
}

int main() {
    std::thread t(threadFunction, 10);
    t.join();  // 等待线程执行完毕
    return 0;
}
1.2 使用 lambda 表达式

C++11 引入了 lambda 表达式,可以简化代码结构,使用匿名函数作为线程的入口。

#include 
#include 

int main() {
    std::thread t([](int x) {
        std::cout << "Lambda thread called with value: " << x << std::endl;
    }, 20);

    t.join();  // 等待线程执行完毕
    return 0;
}
1.3 使用成员函数

如果你需要在线程中调用类的成员函数,可以将成员函数和对象指针一起传递给 std::thread

#include 
#include 

class MyClass {
public:
    void memberFunction(int x) {
        std::cout << "Member function called with value: " << x << std::endl;
    }
};

int main() {
    MyClass obj;
    std::thread t(&MyClass::memberFunction, &obj, 30);
    t.join();  // 等待线程执行完毕
    return 0;
}
1.4 使用可调用对象 (Functor)

一个可调用对象是一个重载了 operator() 的类对象,它可以像函数一样被调用。

#include 
#include 

class Functor {
public:
    void operator()(int x) {
        std::cout << "Functor called with value: " << x << std::endl;
    }
};

int main() {
    Functor functor;
    std::thread t(functor, 40);
    t.join();  // 等待线程执行完毕
    return 0;
}

二、定义一个线程类

我们可以封装线程操作,定义一个线程类来简化线程管理。下面是一个简单的线程类示例:

#include 
#include 

class ThreadClass {
private:
    std::thread t;

public:
    // 构造函数,接受函数和参数来启动线程
    template 
    explicit ThreadClass(Callable&& func, Args&&... args) {
        t = std::thread(std::forward(func), std::forward(args)...);
    }

    // 加入线程(等待线程完成)
    void join() {
        if (t.joinable()) {
            t.join();
        }
    }

    // 分离线程(让线程独立运行)
    void detach() {
        if (t.joinable()) {
            t.detach();
        }
    }

    // 析构函数,确保线程对象在销毁前已被处理
    ~ThreadClass() {
        if (t.joinable()) {
            t.join();  // 通常在析构时,选择 join 或 detach 来处理未处理的线程
        }
    }
};

void threadFunction(int x) {
    std::cout << "Thread function running with value: " << x << std::endl;
}

int main() {
    // 使用类来管理线程
    ThreadClass threadObj(threadFunction, 50);
    threadObj.join();  // 等待线程完成
    return 0;
}

解释

  • ThreadClass 使用模板构造函数来创建线程,并且使用 std::forward 保证参数的完美转发。
  • 提供了 join()detach() 方法来管理线程的生命周期。
  • 在析构函数中确保未处理的线程被 join(),防止程序崩溃。

三、join()detach() 的详细用法及区别

3.1 join() 的用法

join() 的作用是阻塞当前线程(通常是主线程),等待被 join 的子线程执行完毕。如果一个线程对象在退出前没有调用 join()detach(),程序会抛出异常。

#include 
#include 

void task() {
    std::cout << "Task is running in thread." << std::endl;
}

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

    // 主线程等待子线程完成
    std::cout << "Waiting for thread to finish..." << std::endl;
    t.join();  // 阻塞,直到子线程完成
    std::cout << "Thread has finished." << std::endl;

    return 0;
}

解释

  • join() 会使主线程等待 t 所代表的子线程结束。如果没有 join(),主线程可能会提前退出,从而导致异常。
3.2 detach() 的用法

detach() 会使线程与主线程分离,子线程在后台独立运行。主线程不再等待子线程的完成。调用 detach() 后,子线程的资源将在其完成时由系统自动回收。

示例:

#include 
#include 
#include 

void task() {
    std::cout << "Detached thread is running." << std::endl;
    std::this_thread::sleep_for(std::chrono::seconds(2));  // 模拟耗时操作
    std::cout << "Detached thread finished." << std::endl;
}

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

    // 分离线程,允许其在后台运行
    t.detach();

    // 主线程不等待子线程完成,继续执行
    std::cout << "Main thread is not waiting for detached thread." << std::endl;

    // 模拟主线程的其他操作
    std::this_thread::sleep_for(std::chrono::seconds(1));
    std::cout << "Main thread finished." << std::endl;

    return 0;
}

解释

  • detach() 后,线程将独立于主线程运行。即使主线程退出,子线程也会继续执行直到完成。
  • 一旦线程被 detach(),不能再调用 join(),并且主线程不能再追踪该线程的状态。
3.3 join()detach() 的区别总结
  • join():阻塞当前线程,直到被 join 的线程完成。主线程必须等待子线程执行完毕。
  • detach():线程与主线程分离,线程会在后台独立运行,主线程不再等待子线程,也无法再追踪该子线程的状态。

四、std::this_thread

是 C++11 标准库引入的一个命名空间,包含了一组与当前线程相关的函数。它提供了控制和查询当前线程(即执行这些代码的线程)的功能,例如让线程休眠、获取当前线程的 ID 等。std::this_thread 类本身并不是真正的类,它是一个命名空间,其作用是组织与当前线程相关的函数。所有函数都是针对当前线程执行的,不需要创建线程对象即可使用。

4.1、主要功能
std::this_thread::get_id()
  • 获取当前线程的 ID。
  • 返回类型为 std::thread::id,它是线程的唯一标识符,可以用来比较两个线程是否相同。
#include 
#include 

int main() {
    // 获取当前线程的 ID
    std::thread::id this_id = std::this_thread::get_id();
    std::cout << "Current thread ID: " << this_id << std::endl;
    return 0;
}

输出示例

Current thread ID: 140735758188288

std::this_thread::yield()
  • 将当前线程的执行权暂时让出,让操作系统可以将 CPU 分配给其他线程。这并不意味着当前线程会终止,而是它可以在下一次被调度时恢复执行。
  • 这个函数通常用于避免某些线程长时间占用 CPU,给其他线程一个执行的机会。
#include 
#include 

void task() {
    for (int i = 0; i < 5; ++i) {
        std::cout << "Task executing...\n";
        std::this_thread::yield();  // 让出 CPU
    }
}

int main() {
    std::thread t(task);
    t.join();
    return 0;
}

输出示例

Task executing...
Task executing...
Task executing...
Task executing...
Task executing...

std::this_thread::sleep_for()
  • 让当前线程休眠(暂停执行)一段指定的时间。
  • 参数是 std::chrono::duration 类型,可以传递秒、毫秒、微秒等不同的时间单位。
  • 该函数是将线程挂起一段相对时间(从现在开始等多久)。
#include 
#include 
#include 

int main() {
    std::cout << "Sleeping for 3 seconds...\n";
    std::this_thread::sleep_for(std::chrono::seconds(3));  // 休眠3秒
    std::cout << "Awake now!\n";
    return 0;
}

输出示例

Sleeping for 3 seconds...
Awake now!

std::this_thread::sleep_until()
  • 让当前线程休眠直到某个指定的绝对时间点。
  • 参数是 std::chrono::time_point 类型,表示绝对时间点,常与 std::chrono::system_clock::now() 或其他时间点一起使用。
  • sleep_for 不同,sleep_until 是指定一个未来的时间点,线程休眠直到那个时间点到达。
#include 
#include 
#include 

int main() {
    using std::chrono::system_clock;
    
    // 获取当前时间点,并加上 5 秒
    auto wake_time = system_clock::now() + std::chrono::seconds(5);
    
    std::cout << "Sleeping until the next 5 seconds...\n";
    std::this_thread::sleep_until(wake_time);  // 休眠到指定的时间点
    std::cout << "Awake now!\n";
    
    return 0;
}

输出示例

Sleeping until the next 5 seconds...
Awake now!

4.2、各方法的用法及适用场景
1. std::this_thread::get_id()
  • 用途:当你需要区分不同线程时,get_id() 能够返回当前线程的唯一 ID。尤其是在调试或日志记录中,跟踪哪个线程在执行特定任务是非常有用的。
  • 示例场景:多线程下载任务,你可以在日志中记录每个线程的 ID,帮助排查问题。
2. std::this_thread::yield()
  • 用途:在某些场景下,你可能希望当前线程暂时让出 CPU 资源,让系统调度其他线程执行。这样可以避免当前线程长时间占用 CPU,尤其是在没有显式同步机制时,这有助于提升多线程应用的公平性。
  • 示例场景:在编写需要轮询等待某个条件满足的代码时,yield() 可以帮助让出 CPU,而不是长时间阻塞在一个无效的循环中。
3. std::this_thread::sleep_for()
  • 用途:当你需要让线程暂停执行一段时间后再继续执行时,可以使用 sleep_for()。常见的用途包括定时任务、轮询操作、间隔式的数据更新等。
  • 示例场景:编写一个定时器,每隔 10 秒检查一次服务器状态。
4. std::this_thread::sleep_until()
  • 用途:当你需要让线程休眠到某个特定的时间点时,sleep_until() 是非常合适的选择。例如你想要在某个时间戳开始执行某些任务。
  • 示例场景:编写一个程序,每天早上 8:00 执行任务。可以通过 sleep_until() 让程序休眠到指定的时间。
4.3、std::this_thread::sleep_forstd::this_thread::sleep_until 的区别
  • sleep_for 是相对时间休眠。它让线程休眠一个相对时间长度,从现在起计算需要休眠多久。例如休眠 5 秒。
  • sleep_until 是绝对时间休眠。它让线程休眠到一个确定的时间点。你可以传递一个绝对时间点,线程会休眠直到这个时间点。例如让线程休眠到今天的 12:00。
注:sleep_forsleep_until 对比示例
#include 
#include 
#include 

int main() {
    using namespace std::chrono;

    // 使用 sleep_for 休眠 2 秒
    std::cout << "Sleeping for 2 seconds...\n";
    std::this_thread::sleep_for(seconds(2));  // 相对休眠
    std::cout << "Awake after 2 seconds.\n";

    // 使用 sleep_until 休眠到未来的绝对时间点
    auto future_time = system_clock::now() + seconds(3);
    std::cout << "Sleeping until the next 3 seconds...\n";
    std::this_thread::sleep_until(future_time);  // 绝对时间点休眠
    std::cout << "Awake at the specified time point.\n";

    return 0;
}

输出示例

Sleeping for 2 seconds...
Awake after 2 seconds.
Sleeping until the next 3 seconds...
Awake at the specified time point.

总结

std::this_thread 提供了一个简单而强大的接口,用于控制当前线程的行为,包括:

  • 获取当前线程的 ID:有助于在调试和日志记录中识别不同线程。
  • 让出线程的执行权:通过 yield() 提高 CPU 使用效率,允许系统调度其他线程。
  • 让线程休眠:可以使用 sleep_forsleep_until 暂时暂停线程的执行,应用场景包括定时任务、延迟执行等。

这些工具使得 C++11 开始的多线程编程变得更加灵活和易于控制。

五、std::mutex 详解

std::mutex 是 C++11 引入的一种线程同步机制,用于在多线程环境下防止数据竞争(race condition)。在多线程程序中,如果多个线程同时访问并修改同一个共享数据,会导致数据竞争问题,造成不可预知的错误。std::mutex(互斥量)提供了一种机制,确保每次只有一个线程能够访问共享资源,其他线程必须等待,直到前一个线程释放互斥量。

主要功能

std::mutex 类提供了几个基本的方法来控制线程对共享资源的访问:

  1. lock():锁定互斥量。如果互斥量已经被其他线程锁定,则调用 lock() 的线程将会阻塞,直到该互斥量被解锁。
  2. unlock():解锁互斥量,允许其他线程锁定它。
  3. try_lock():尝试锁定互斥量。如果互斥量已被其他线程锁定,则返回 false,否则锁定并返回 true
使用场景

std::mutex 通常用于保护临界区(critical section),即多个线程可能会同时访问的共享资源。

代码示例
1. 基本使用 std::mutex

这是一个简单的例子,展示了如何使用 std::mutex 来保护共享数据(计数器),防止多个线程同时修改它。

#include 
#include 
#include 

std::mutex mtx;  // 全局互斥量,用于同步对共享数据的访问
int counter = 0; // 共享资源

void increaseCounter() {
    for (int i = 0; i < 10000; ++i) {
        mtx.lock();   // 锁定互斥量,进入临界区
        ++counter;    // 对共享数据进行操作
        mtx.unlock(); // 解锁互斥量,离开临界区
    }
}

int main() {
    // 创建两个线程,执行 increaseCounter 函数
    std::thread t1(increaseCounter);
    std::thread t2(increaseCounter);

    // 等待两个线程完成
    t1.join();
    t2.join();

    // 输出最终的计数器值
    std::cout << "Final counter value: " << counter << std::endl;
    return 0;
}

解释

  • 互斥量 mtx:在每个线程访问共享资源之前使用 lock() 来锁定互斥量,保证在任何时候只有一个线程可以修改 counter 变量。
  • 临界区:临界区是指 mtx.lock()mtx.unlock() 之间的代码区域,即对共享资源的访问部分。
  • 线程安全:在互斥量的保护下,两个线程不会同时修改 counter,因此保证了线程安全。

输出示例

Final counter value: 20000

如果没有使用 std::mutex,两个线程可能同时访问并修改 counter,导致最后输出的值小于预期的 20000,因为多个线程可能在同一时刻操作同一内存位置,发生了数据竞争。

2. 使用 try_lock()

try_lock()std::mutex 提供的另一个方法,它不会阻塞线程。如果互斥量已经被锁定,try_lock() 会立即返回 false,而不是阻塞等待。

#include 
#include 
#include 

std::mutex mtx;  // 全局互斥量

void printMessage(const std::string& msg) {
    if (mtx.try_lock()) {  // 尝试获取锁,如果成功则执行
        std::cout << msg << std::endl;
        mtx.unlock();      // 解锁
    } else {
        std::cout << "Lock busy, couldn't print: " << msg << std::endl;
    }
}

int main() {
    std::thread t1(printMessage, "Hello from Thread 1");
    std::thread t2(printMessage, "Hello from Thread 2");

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

    return 0;
}

解释

  • try_lock() 方法尝试锁定互斥量。如果成功获取锁,它就进入临界区执行输出操作;如果未能获取锁,说明其他线程已锁定了互斥量,则输出 "Lock busy"。

输出示例

Hello from Thread 1
Lock busy, couldn't print: Hello from Thread 2
3. 使用 std::lock_guard 简化互斥锁管理

C++11 提供了 std::lock_guard,它是一个 RAII 风格的锁管理器,在作用域结束时自动释放锁,避免了手动 lock()unlock() 可能引发的错误。

#include 
#include 
#include 

std::mutex mtx;  // 全局互斥量
int counter = 0; // 共享资源

void increaseCounter() {
    for (int i = 0; i < 10000; ++i) {
        std::lock_guard lock(mtx);  // 自动锁定互斥量,作用域结束时自动解锁
        ++counter;
    }
}

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

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

    std::cout << "Final counter value: " << counter << std::endl;
    return 0;
}

解释

  • std::lock_guard 是一个模板类,管理互斥锁的生命周期。它在构造时自动锁定互斥量,在作用域结束时(例如函数返回或异常抛出时)自动解锁,避免了忘记解锁或出现死锁的情况。

输出示例

Final counter value: 20000
4. 使用 std::scoped_lock 同时锁定多个互斥量

C++17 引入了 std::scoped_lock,它不仅可以同时锁定多个互斥量,还能防止死锁。std::scoped_lockstd::lock_guard 的改进版,特别适用于需要同时锁定多个资源的场景。

#include 
#include 
#include 

std::mutex mtx1, mtx2;  // 两个互斥量

void task1() {
    std::scoped_lock lock(mtx1, mtx2);  // 同时锁定 mtx1 和 mtx2
    std::cout << "Task 1 has locked both mutexes." << std::endl;
}

void task2() {
    std::scoped_lock lock(mtx1, mtx2);  // 同时锁定 mtx1 和 mtx2
    std::cout << "Task 2 has locked both mutexes." << std::endl;
}

int main() {
    std::thread t1(task1);
    std::thread t2(task2);

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

    return 0;
}

解释

  • std::scoped_lock 同时锁定多个互斥量,并且能避免死锁。它会确保在解锁时,以相反顺序解锁,从而避免了交叉锁定不同互斥量时可能发生的死锁问题。

输出示例

Task 1 has locked both mutexes.
Task 2 has locked both mutexes.
重要注意事项
  1. 锁的生命周期:互斥量应该在所有访问共享资源的线程中共享。一个线程加锁后,其他线程必须等待该锁被解锁才能继续执行。
  2. 防止死锁:在多个互斥量之间锁定时(如同时访问多个资源),应小心防止死锁。死锁发生在多个线程相互等待其他线程释放锁的情况下。使用 std::scoped_lock 可以简化锁定多个资源时的死锁管理。
  3. lock_guardscoped_lock 的推荐使用lock_guardscoped_lock 是 RAII 风格的锁管理工具,推荐使用这些工具来管理锁的生命周期,避免手动 lock()unlock() 可能引发的错误。
总结

std::mutex 是 C++11 提供的基本线程同步工具,用于解决多线程编程中的数据竞争问题。通过互斥量可以确保同一时刻只有一个线程能够访问共享资源,防止多个线程同时修改数据而导致不可预测的行为。结合 std::lock_guardstd::scoped_lock,可以更简便地管理锁的生命周期,确保代码更加安全和高效。

六、std::unique_lock

std::unique_lock 是 C++11 提供的一种灵活的锁管理工具。它与 std::lock_guard 类似,都用于管理互斥锁的生命周期,确保锁的正确释放。与 std::lock_guard 不同的是,std::unique_lock 提供了更多的灵活性,包括:

  • 可以延迟锁定互斥量(懒惰锁定)。
  • 可以在需要时手动解锁和重新锁定。
  • 可以使用 std::try_lockstd::defer_lockstd::adopt_lock 等高级锁定策略。
主要功能:
  1. 自动锁管理:和 std::lock_guard 一样,std::unique_lock 在构造时锁定互斥量,在作用域结束时自动解锁,防止忘记解锁的错误。
  2. 手动锁定与解锁:你可以手动调用 lock()unlock() 方法,控制锁的生命周期。
  3. 延迟锁定:你可以选择延迟锁定互斥量,也就是说,创建 std::unique_lock 对象时并不立即锁定互斥量,稍后根据需要手动锁定。
  4. 移动语义支持std::unique_lock 可以被移动,这对于实现线程间的互斥锁传递很有用。
构造函数与锁定策略
  • std::defer_lock:不自动锁定互斥量,稍后由程序员手动锁定。
  • std::try_to_lock:尝试锁定互斥量,若无法锁定,则不阻塞并返回 false
  • std::adopt_lock:假定互斥量已经被锁定,不再锁定。
使用示例
1. 基本使用:自动锁定与解锁

std::unique_lock 可以像 std::lock_guard 一样,在构造时自动锁定互斥量,并在析构时自动解锁。

#include 
#include 
#include 

std::mutex mtx;  // 全局互斥量
int counter = 0; // 共享资源

void increaseCounter() {
    for (int i = 0; i < 10000; ++i) {
        std::unique_lock lock(mtx);  // 自动锁定互斥量
        ++counter;  // 临界区
        // lock 会在作用域结束时自动解锁
    }
}

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

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

    std::cout << "Final counter value: " << counter << std::endl;
    return 0;
}

解释

  • std::unique_lock 的构造函数会立即锁定互斥量 mtx,并且当 lock 作用域结束时,它会自动解锁。这种机制避免了手动调用 unlock() 的必要。
2. 手动锁定与解锁

你可以通过 std::unique_lock 对互斥量进行手动锁定和解锁操作,控制更为灵活。

#include 
#include 
#include 

std::mutex mtx;

void task() {
    std::unique_lock lock(mtx, std::defer_lock);  // 延迟锁定互斥量
    std::cout << "Before locking" << std::endl;
    
    lock.lock();  // 手动锁定互斥量
    std::cout << "Locked and working..." << std::endl;
    
    lock.unlock();  // 手动解锁
    std::cout << "Unlocked" << std::endl;
}

int main() {
    std::thread t(task);
    t.join();
    return 0;
}

解释

  • 这里使用了 std::defer_lockstd::unique_lock 在构造时不会自动锁定互斥量。随后,我们手动调用了 lock()unlock() 来控制互斥量的锁定和解锁过程。
3. 使用 try_lock 尝试锁定

std::unique_lock 还可以用于尝试锁定互斥量,而不阻塞线程。如果锁定失败,线程可以继续执行其他任务。

#include 
#include 
#include 

std::mutex mtx;

void task() {
    std::unique_lock lock(mtx, std::try_to_lock);  // 尝试锁定互斥量
    if (lock.owns_lock()) {
        std::cout << "Lock acquired, working..." << std::endl;
    } else {
        std::cout << "Couldn't acquire lock, doing other work..." << std::endl;
    }
}

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

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

解释

  • 通过 std::try_to_lock,线程会尝试锁定互斥量。如果无法锁定,程序可以继续执行其他任务,而不是阻塞等待锁的释放。
  • owns_lock() 可以检查 std::unique_lock 是否成功锁定互斥量。
4. 使用 adopt_lock 接管已经锁定的互斥量

如果你手动锁定了互斥量,但希望将它交给 std::unique_lock 来管理,可以使用 std::adopt_lock。这会告诉 std::unique_lock,互斥量已经被锁定,不再需要再次锁定。

#include 
#include 
#include 

std::mutex mtx;

void task() {
    mtx.lock();  // 手动锁定互斥量
    std::unique_lock lock(mtx, std::adopt_lock);  // 接管锁
    std::cout << "Lock adopted, working..." << std::endl;
    // lock 会在作用域结束时自动解锁
}

int main() {
    std::thread t(task);
    t.join();
    return 0;
}

解释

  • std::adopt_lock 告诉 std::unique_lock,互斥量已经被锁定,因此不需要再次调用 lock()。但是,当 std::unique_lock 对象销毁时,它会自动调用 unlock()
std::unique_lockstd::lock_guard 的区别
  1. 灵活性

    • std::lock_guard 在构造时立即锁定互斥量,并在销毁时自动解锁。它适合用于简单的锁管理。
    • std::unique_lock 提供了更多灵活的功能,如延迟锁定、尝试锁定、手动解锁等。它适用于需要复杂锁定逻辑的场景。
  2. 锁的控制

    • std::lock_guard 不允许手动解锁,只能在作用域结束时解锁。
    • std::unique_lock 允许在代码中手动调用 unlock(),并且可以手动重新锁定。
  3. 性能

    • 由于 std::lock_guard 更简单,通常它的性能要比 std::unique_lock 稍高一些。但如果你的程序不需要 std::unique_lock 的灵活性,优先使用 std::lock_guard 会更高效。
总结

std::unique_lock 是一个灵活的锁管理工具,适合处理复杂的线程同步场景。它与 std::lock_guard 的区别在于其灵活的控制能力:支持延迟锁定、手动解锁、尝试锁定等操作。此外,std::unique_lock 可以使用 std::defer_lockstd::try_to_lockstd::adopt_lock 等锁定策略,适用于需要更精细控制的场景。在简单的锁定场景中,std::lock_guard 更适合,而 std::unique_lock 则适用于需要更多灵活性的场景。

七、std::condition_variable::wait

是 C++11 中引入的一个机制,用于在线程同步时协调多个线程之间的执行。它与 std::mutexstd::unique_lock 配合使用,用于阻塞线程直到特定条件满足。

wait() 的主要用途是在某个条件不满足时让线程进入等待状态,等到另一个线程通知条件发生变化时再继续执行。通过 wait(),可以有效避免忙等待(busy waiting),减少 CPU 资源浪费。

std::condition_variable::wait 的原理

  1. 互斥锁wait() 必须与一个互斥锁(std::unique_lock)一起使用。它在调用 wait() 时会自动释放锁,进入等待状态,并在被通知时重新获取锁。
  2. 条件变量wait() 用于阻塞线程,直到另一个线程通过条件变量调用 notify_one()notify_all() 来通知等待线程可以继续执行。
  3. 通知机制
    • notify_one():唤醒一个等待的线程。
    • notify_all():唤醒所有等待的线程。

wait() 的使用场景

典型的 wait() 使用场景是生产者-消费者问题。在这种情况下,消费者等待生产者生成数据,而生产者在生成数据后通知消费者继续处理。

代码示例

1. 基本使用 wait()notify_one()

这是一个简单的示例,其中一个线程等待某个条件满足,而另一个线程在一定时间后设置条件并通知等待的线程继续执行。

#include 
#include 
#include 
#include 

std::mutex mtx;                    // 互斥锁
std::condition_variable cv;        // 条件变量
bool ready = false;                // 条件标志

void waitForWork() {
    std::unique_lock lock(mtx);  // 获取锁
    std::cout << "Waiting for work to be ready...\n";
    
    // 当 ready 为 false 时,线程进入等待状态,锁被自动释放
    cv.wait(lock, [] { return ready; });

    // 当 ready 为 true 时,wait 结束,线程重新获取锁
    std::cout << "Work is ready, proceeding...\n";
}

void prepareWork() {
    std::this_thread::sleep_for(std::chrono::seconds(2));  // 模拟工作准备
    {
        std::lock_guard lock(mtx);  // 获取锁并设置条件
        ready = true;
        std::cout << "Work is prepared, notifying worker...\n";
    }
    cv.notify_one();  // 通知等待线程可以继续执行
}

int main() {
    std::thread worker(waitForWork);   // 创建等待线程
    std::thread preparer(prepareWork); // 创建准备线程

    worker.join();
    preparer.join();

    return 0;
}

解释

  • wait():在 waitForWork() 函数中,调用 cv.wait(lock, [] { return ready; }),它将当前线程阻塞,直到 ready 变为 truewait() 在等待期间自动释放锁,避免其他线程无法获取锁。
  • notify_one():在 prepareWork() 函数中,调用 cv.notify_one() 通知等待的线程(waitForWork)可以继续执行。
  • 锁的管理wait()notify_one() 都需要与互斥锁配合使用,以保证线程之间的同步。

输出示例

Waiting for work to be ready...
Work is prepared, notifying worker...
Work is ready, proceeding...
2. 使用 notify_all() 唤醒所有等待线程

notify_all() 可以唤醒所有因条件不满足而阻塞的线程。这对于多个线程等待同一个条件的场景非常有用。

#include 
#include 
#include 
#include 
#include 

std::mutex mtx;
std::condition_variable cv;
bool ready = false;

void worker(int id) {
    std::unique_lock lock(mtx);
    cv.wait(lock, [] { return ready; });  // 所有线程都等待 ready 为 true
    std::cout << "Worker " << id << " is proceeding.\n";
}

void prepareWork() {
    std::this_thread::sleep_for(std::chrono::seconds(2));  // 模拟工作准备
    {
        std::lock_guard lock(mtx);
        ready = true;
        std::cout << "Work is ready, notifying all workers...\n";
    }
    cv.notify_all();  // 通知所有等待的线程
}

int main() {
    std::vector workers;
    
    // 创建多个线程
    for (int i = 1; i <= 5; ++i) {
        workers.emplace_back(worker, i);
    }

    std::thread preparer(prepareWork);  // 创建准备线程

    // 等待所有线程完成
    for (auto& t : workers) {
        t.join();
    }
    preparer.join();

    return 0;
}

解释

  • notify_all()notify_all() 会唤醒所有等待的线程,这里唤醒了 5 个 worker 线程。
  • 多线程等待:所有 worker 线程都会在 cv.wait() 中等待,直到 ready 变为 true 并且收到通知。

输出示例

Work is ready, notifying all workers...
Worker 1 is proceeding.
Worker 2 is proceeding.
Worker 3 is proceeding.
Worker 4 is proceeding.
Worker 5 is proceeding.
3. 防止虚假唤醒

虚假唤醒(spurious wakeups)是指线程在没有被条件变量显式唤醒的情况下,也可能会被唤醒。这是操作系统的特性,因此我们在使用 wait() 时应该总是配合条件判断来避免这种情况。

为了避免虚假唤醒,C++ 中的 wait() 提供了一种安全的模式,即使用条件判断的 lambda 表达式来确保条件满足时才继续执行。

cv.wait(lock, [] { return ready; });

这个版本的 wait() 会在 readyfalse 时继续阻塞线程,直到条件真正满足。

4. wait_for()wait_until()

除了 wait() 之外,C++11 还提供了 wait_for()wait_until() 方法,允许我们指定等待的时间。如果在指定的时间内条件未满足,线程会超时并继续执行。

  • wait_for():阻塞线程一段时间,超时后线程自动返回。
  • wait_until():阻塞线程直到某个时间点,超时后线程自动返回。
4.1 使用 wait_for() 超时等待
#include 
#include 
#include 
#include 
#include 

std::mutex mtx;
std::condition_variable cv;
bool ready = false;

void worker() {
    std::unique_lock lock(mtx);
    if (cv.wait_for(lock, std::chrono::seconds(3), [] { return ready; })) {
        std::cout << "Work is ready, proceeding...\n";
    } else {
        std::cout << "Timeout, work is not ready.\n";
    }
}

void prepareWork() {
    std::this_thread::sleep_for(std::chrono::seconds(5));  // 模拟准备时间
    {
        std::lock_guard lock(mtx);
        ready = true;
        std::cout << "Work is prepared, notifying worker...\n";
    }
    cv.notify_one();
}

int main() {
    std::thread t1(worker);
    std::thread t2(prepareWork);

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

    return 0;
}

解释

  • wait_for():等待 3 秒,如果 ready 在 3 秒内没有变为 true,线程会超时并继续执行。
  • 超时处理:如果超过等待时间,线程会输出 "Timeout, work is not ready."。

输出示例

Timeout, work is not ready.
Work is prepared, notifying worker...
4.2 使用 wait_until() 等待到指定时间点
#include 
#include 
#include 
#include 
#include 

std::mutex mtx;
std::condition_variable cv;
bool ready = false;

void worker() {
    auto timeout = std::chrono::system_clock::now() + std::chrono::seconds(3);
    std::unique_lock lock(mtx);
    if (cv.wait_until(lock, timeout, [] { return ready; })) {
        std::cout << "Work is ready, proceeding...\n";
    } else {
        std::cout << "Timeout, work is not ready.\n";
    }
}

void prepareWork() {
    std::this_thread::sleep_for(std::chrono::seconds(5));  // 模拟准备时间
    {
        std::lock_guard lock(mtx);
        ready = true;
        std::cout << "Work is prepared, notifying worker...\n";
    }
    cv.notify_one();
}

int main() {
    std::thread t1(worker);
    std::thread t2(prepareWork);

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

    return 0;
}

解释

  • wait_until():等待到指定时间点,如果在这个时间点前 ready 变为 true,线程会继续执行;否则超时。

输出示例

Timeout, work is not ready.
Work is prepared, notifying worker...

总结

  • std::condition_variable::wait 用于协调线程之间的同步,它可以让一个线程在等待某个条件满足时进入等待状态,并通过条件变量的 notify_one()notify_all() 来唤醒线程。
  • wait() 需要与 std::mutexstd::unique_lock 配合使用,以保证线程在等待期间不会造成数据竞争。
  • wait_for()wait_until() 提供了超时等待机制,允许线程在指定的时间内等待条件满足,否则超时返回。

你可能感兴趣的:(c++,c++全套攻略,c++多线程,c++)