0%

C++ Notes

some notes about cpp # 遍历map容器 ## c++98

1
2
3
4
5
map<string, int>::iterator it;
for (it = m2.begin(); it != m2.end(); it++) {
string s = it->first;
printf("%s %d\n", s.data(), it->second);
}
## c++11
1
2
3
for(auto it : map1){
cout << it.first <<" "<< it.second <<endl;
}
## c++17
1
2
3
for (auto& [k, v] : map1) {
std::cout << k << " " << v << std::endl;
}

sort函数

1
sort(a.begin(),a.end(),std::greater<int>())

Lambda(匿名函数)

c++11新特性

语法:

image-20240709004856410

按照上图中的标号,具体解释如下:

标号1:指定捕获列表,所谓捕获,是把Lambda表达式之外定义的变量,捕获到Lambda表达式内部,这样Lambda内部可以直接引用这些变量,省去参数传递的过程。

捕获分为两种方式:

  • 按值捕获,捕获到Lambda表达式内部的变量是副本,注意,按值捕获的变量默认是不能修改的,可以使用mutable关键字突破这个限制,见下文标号3.
  • 按引用捕获,捕获到Lambda表达式内部的变量是引用,修改变量会影响外部的同名变量

捕获的举例如下:

  • [],空捕获列表,不捕获任何变量,此时引用外部变量则会提示编译错误
  • [=],默认按值捕获全部变量
  • [&],默认按引用捕获全部变量
  • [=,&x,&y],默认按值捕获全部变量,但是变量x,变量y按引用捕获
  • [&,=x,=y],默认按引用捕获全部变量,但是变量x,变量y按值捕获
  • [&,x,y],效果同上,即变量名前面没有写=或者&时,默认为按值捕获
  • [=,x,y],编译出错,变量x,变量y按值捕获,和默认按值捕获全部变量重复
  • [x,y],只按值捕获变量x和变量y
  • [&x,&y],只按引用捕获变量x和变量y
  • [x,&y],只按值捕获变量x,按引用捕获变量y
  • [=x,=y],编译出错,应为[x,y]
  • [this],捕获this指针,然后在Lambda表达式内部就可以直接引用类成员了

标号2:函数参数

用法和普通函数一样

标号3:mutable,用来突破不能修改按值捕获变量的限制

如下代码,按值捕获了变量x,在Lambda表达式内部,是不能修改x的值的

1
2
int x = 1;
auto f=\[x\](){x++;};// 编译错误,不能修改x的值f();

为了突破上面的限制,添加mutable即可编译成功

1
2
int x = 1;
auto f=\[x\]()mutable{x++;};// 编译成功f();

注意,即使Lambda表达式内部修改了x的值,但是依旧不影响Lambda表达式外部的x的值,两者是相互独立的。

标号4:throw关键字,和C++中throw用法保持一致

标号5:Lambda表达式返回值的类型

标号6:函数内容;注意函数最后面,需要添加一个;

单例模式

单例模式(Singleton Pattern)是一种设计模式,确保一个类只有一个实例,并提供全局访问点。它主要用于控制资源的访问或确保全局状态的唯一性。例如,常见的应用场景包括日志记录器、配置文件管理、数据库连接池等。

实现步骤

  1. 私有化构造函数:防止外部通过 new 操作创建实例。
  2. 私有化拷贝构造函数和赋值运算符:防止通过复制或赋值生成多个实例。
  3. 提供一个静态方法来获取类的唯一实例

基本实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
template <typename T>
class Singleton{
protected:
Singleton() = default;
Singleton(const Singleton<T>&) =delete;
Singleton& operator = (const Singleton&) = delete;
static std::shared_ptr<T> _instance;
public:
static std::shared_ptr<T> GetInstance(){
static std::once_flag s_flag;
std::call_once(s_flag,[&](){
_instance = std::shared_ptr<T>(new T);
});
return _instance;
}
void PrintAddress(){
std::cout<<_instance.get()<<std::endl;
}
~Singleton(){
std::cout<<"this singleton destruct"<<std::endl;
}
};
template<typename T>
std::shared_ptr<T> Singleton<T>::_instance = nullptr;

智能指针

在C++中,智能指针(Smart Pointer)是一种用于自动管理动态分配内存的类模板。与传统的原生指针不同,智能指针能够在对象不再需要时自动释放内存,从而避免手动管理内存带来的错误(例如内存泄漏、重复释放等问题)。C++11 标准引入了几种常用的智能指针类型,分别是 std::unique_ptrstd::shared_ptrstd::weak_ptr,它们位于 <memory> 头文件中。

std::shared_ptr

  • 特点
    • 允许多个指针共享同一个对象的所有权。
    • 使用引用计数来管理对象的生命周期。当最后一个 shared_ptr 销毁时,才会释放对象的内存。
  • 适用场景
    • 当多个部分需要共享访问同一个对象并且不确定它们何时会释放时使用。

std::unique_ptr

独占所有权

  • unique_ptr 拥有所指向对象的唯一所有权。这意味着同一时间只能有一个 unique_ptr 指向一个特定的对象。如果尝试将它复制给另一个 unique_ptr,编译器会报错。
  • 不能复制,但可以通过 std::move 转移所有权。

自动内存管理

  • unique_ptr 超出作用域或被显式重置时,所管理的对象会自动被销毁,释放资源。

轻量化

  • unique_ptr 本质上是一个指针,因此它的大小和普通指针一样。它不会引入额外的运行时开销。

与裸指针的转换

  • 可以通过 get() 函数获取裸指针,但这只是为了与那些需要裸指针的接口兼容,不会转移所有权。

std::bind

std::bind 是 C++11 引入的一个工具,用来创建函数对象(functor),即绑定某个函数或成员函数的特定参数,从而生成一个新的可调用对象。它可以将部分或全部参数绑定到一个已有函数上,以简化调用时的操作。

基本语法

1
std::bind(function, arg1, arg2, ...);
  • function:要绑定的函数或成员函数。
  • arg1, arg2, ...:绑定的参数,或者用 _1, _2 等表示的占位符(表示调用时需要提供的参数)。

示例 1:绑定普通函数

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>
#include <functional>

void print_sum(int a, int b) {
std::cout << "Sum: " << a + b << std::endl;
}

int main() {
// 使用 std::bind 绑定参数 5 和 10
auto bound_func = std::bind(print_sum, 5, 10);
bound_func(); // 输出:Sum: 15
return 0;
}

示例 2:绑定成员函数

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

class Example {
public:
void display(int x) const {
std::cout << "Value: " << x << std::endl;
}
};

int main() {
Example obj;
// 绑定成员函数,注意传递对象指针
auto bound_func = std::bind(&Example::display, &obj, 42);
bound_func(); // 输出:Value: 42
return 0;
}

示例 3:使用占位符

如果你希望绑定部分参数而在调用时提供其他参数,可以使用占位符 _1, _2, 等。

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

void print_product(int a, int b) {
std::cout << "Product: " << a * b << std::endl;
}

int main() {
using namespace std::placeholders; // 为了使用 _1, _2 等占位符
// 绑定参数 a 为 5,b 使用占位符
auto bound_func = std::bind(print_product, 5, _1);
bound_func(3); // 输出:Product: 15
return 0;
}

主要用途

  1. 延迟调用:通过绑定部分参数,简化函数调用时需要传递的参数。
  2. 与标准库的算法结合:可以将 std::bind 结合使用于 STL 算法中,比如 std::for_each
  3. 代替自定义函数对象:在某些情况下,它可以减少需要创建自定义 functor 类的必要。

注意事项

  • std::bind 创建的函数对象是以值方式复制参数的,除非你显式传递引用(使用 std::refstd::cref)。
  • C++14 引入了更灵活的 lambda 表达式,很多场景下可以使用 lambda 替代 std::bind

std::bind 提供了强大的参数绑定功能,能够提高代码的简洁性和可读性,尤其在需要部分参数预设的情况下特别有用。

右值引用

右值引用(rvalue reference)是 C++11 引入的一种引用类型,它允许你引用一个右值,从而支持移动语义完美转发。传统的引用(即左值引用)只能绑定到左值,而右值引用允许绑定到右值,从而提供了更多的优化机会。

左值 vs 右值

  • 左值(lvalue):指的是表达式在内存中占有一个明确的地址,可以被取地址符 & 获取。例如,变量、数组元素、对象等都是左值。
    1
    2
    int a = 10;  // a 是一个左值
    int& ref = a; // 左值引用
  • 右值(rvalue):指的是不占有明确内存地址的临时值,如字面常量、临时对象、表达式的返回值等。例如:
    1
    int b = 10 + 20;  // 10+20 是一个右值

右值引用的定义

通过使用 && 定义右值引用:

1
int&& r = 10;  // 右值引用,r 绑定到右值 10

右值引用的特点

  • 右值引用只能绑定到右值,即那些没有持久性或存储地址的临时对象。例如:
    1
    2
    3
    int&& r = 10;  // 合法
    int x = 20;
    int&& r2 = x; // 错误,不能将右值引用绑定到左值
  • 右值引用用于移动语义:在类的移动构造函数和移动赋值运算符中,可以利用右值引用高效地“移动”资源,而不是“复制”资源。

右值引用的用途

  1. 移动语义:右值引用支持通过移动资源来避免不必要的深拷贝。配合 std::move() 使用,可以将左值转换为右值,以支持资源的转移。

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

    class MyClass {
    public:
    std::string data;

    // 移动构造函数
    MyClass(std::string&& str) : data(std::move(str)) {
    std::cout << "Move constructor called!" << std::endl;
    }
    };

    int main() {
    std::string myStr = "Hello, World!";
    MyClass obj(std::move(myStr)); // 使用移动语义
    std::cout << "myStr after move: " << myStr << std::endl; // myStr 变为空或未定义
    }

  2. 完美转发:右值引用可以结合模板和 std::forward() 实现完美转发,允许将参数的“值类别”完全传递到函数中,保持参数的左值或右值属性。

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

void process(int& i) { std::cout << "Lvalue\n"; }
void process(int&& i) { std::cout << "Rvalue\n"; }

template <typename T>
void forward(T&& arg) {
process(std::forward<T>(arg)); // 保留左值或右值属性
}

int main() {
int a = 0;
forward(a); // 输出 "Lvalue"
forward(10); // 输出 "Rvalue"
}

总结

右值引用是 C++11 提供的一种新的引用类型,主要用于提高性能,通过移动语义避免不必要的拷贝。它可以绑定到右值,通常与 std::move()std::forward() 配合使用。

std::thread

std::thread 是 C++11 引入的标准库中的一个类,用于创建和管理线程。它允许程序以并发的方式执行多个任务,这在多核处理器或需要并行处理的场景中非常有用。以下是 std::thread 的一些关键概念和用法:

1. 基本用法

要创建一个新线程,可以将可调用对象(如函数、lambda表达式、函数对象等)传递给 std::thread 的构造函数。线程开始执行时,会调用这个对象。线程可以和主线程并发执行。

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

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

int main() {
// 创建并启动一个新线程,执行 task 函数
std::thread t(task);

// 等待线程 t 结束
t.join();

std::cout << "Main thread is done" << std::endl;
return 0;
}

在上面的例子中,std::thread t(task); 创建了一个新线程来执行 task 函数。t.join(); 确保主线程等待新线程完成。

2. join()detach()

  • join():阻塞调用线程,直到目标线程完成。这是保证线程同步的重要方式。如果不调用 join()detach(),程序结束时会崩溃。

  • detach():将线程分离,变为守护线程,允许它独立运行,不会阻塞主线程。程序结束时,分离的线程仍然可以继续执行,但无法被主线程管理。

1
2
std::thread t(task);
t.detach(); // 分离线程,不等待其完成

3. 传递参数给线程

可以通过 std::thread 向线程函数传递参数,参数会按值传递或通过 std::ref 进行引用传递。

1
2
3
4
5
6
7
8
9
void task(int n, const std::string& str) {
std::cout << "n = " << n << ", str = " << str << std::endl;
}

int main() {
std::string msg = "Hello";
std::thread t(task, 42, std::ref(msg)); // 通过 std::ref 传递引用
t.join();
}

4. 线程的移动语义

std::thread 对象不可复制,但可以通过移动语义来传递。

1
2
std::thread t1(task);
std::thread t2 = std::move(t1); // 将 t1 移动给 t2

5. 多线程中的共享数据问题

多线程中访问共享数据时需要小心,可能会引发数据竞争。可以使用互斥锁(std::mutex)来保护共享数据:

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

std::mutex mtx;

void print_task(const std::string& msg) {
std::lock_guard<std::mutex> lock(mtx);
std::cout << msg << std::endl;
}

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

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

6. 线程的生命周期

std::thread 对象一旦创建就会启动一个新线程,直到: - 该线程的函数执行完毕; - 调用 join()detach() 方法。

如果不调用这些方法,程序在销毁 std::thread 对象时会抛出异常。因此,在多线程编程中要确保所有线程都正确地管理其生命周期。

总结

std::thread 为 C++ 提供了简洁且强大的多线程支持。在使用多线程时,必须特别注意同步问题(如数据竞争),适当使用锁或条件变量来管理线程间的共享数据。

在C++中,锁(Lock)是用于管理对共享资源的并发访问的机制。锁用于确保在多线程环境中,只有一个线程在给定的时间点访问共享资源,从而避免数据竞争和不一致的问题。C++11 标准引入了一些内置的同步机制,主要集中在 std::mutex 和相关的锁对象上。

1. mutex(互斥量)

std::mutex 是 C++ 中的基本互斥量,它用于确保只有一个线程可以同时访问共享资源。通过使用 std::mutex,你可以手动锁定和解锁线程对资源的访问。

常用方法:

  • lock():锁定互斥量,如果已经被其他线程锁定,则当前线程会被阻塞,直到可以锁定。
  • unlock():解锁互斥量,允许其他线程锁定。
  • try_lock():尝试锁定互斥量,如果已经被锁定,则不会阻塞,直接返回 false

示例:

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 print_message(const std::string& message) {
mtx.lock(); // 锁定互斥量
std::cout << message << std::endl;
mtx.unlock(); // 解锁互斥量
}

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

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

return 0;
}

2. std::lock_guard

std::lock_guard 是 C++ 中用于简化锁定过程的类,它通过 RAII(资源获取即初始化)技术,自动管理锁的生命周期。锁在创建时自动加锁,在离开作用域时自动解锁。

使用场景:

  • 当你希望自动管理锁的生命周期,避免手动调用 lock()unlock() 时,std::lock_guard 非常方便。

示例:

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 print_message(const std::string& message) {
std::lock_guard<std::mutex> lock(mtx); // 自动加锁
std::cout << message << std::endl;
// 离开作用域时自动解锁
}

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

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

return 0;
}

3. std::unique_lock

std::unique_lock 是一个更加灵活的锁类型,它支持延迟锁定、手动解锁、重新锁定等操作。与 std::lock_guard 类似,它也遵循 RAII 机制,但提供了更大的控制灵活性。

常用方法:

  • lock():锁定互斥量。
  • unlock():解锁互斥量。
  • try_lock():尝试锁定互斥量,立即返回锁定是否成功。
  • release():释放对互斥量的所有权,但不解锁互斥量。

示例:

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

std::mutex mtx;

void print_message(const std::string& message) {
std::unique_lock<std::mutex> lock(mtx); // 手动控制锁的生命周期
std::cout << message << std::endl;
lock.unlock(); // 可以手动解锁
// lock.lock(); // 需要时可以重新锁定
}

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

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

return 0;
}

4. std::shared_mutex(读写锁)

std::shared_mutex(C++17 引入)是一种允许多个线程同时读取但只允许一个线程写入的锁。使用它可以实现读写分离,读操作可以并发执行,写操作则独占。

常用方法:

  • lock():独占模式锁定(用于写入),阻塞所有其他读写操作。
  • unlock():解锁。
  • lock_shared():共享模式锁定(用于读取),允许多个线程同时持有共享锁。
  • unlock_shared():解锁共享模式。

示例:

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
#include <iostream>
#include <thread>
#include <shared_mutex>

std::shared_mutex rw_mutex;
int shared_data = 0;

void read_data() {
std::shared_lock<std::shared_mutex> lock(rw_mutex); // 共享锁,允许多个线程读取
std::cout << "Read: " << shared_data << std::endl;
}

void write_data(int value) {
std::unique_lock<std::shared_mutex> lock(rw_mutex); // 独占锁,只有一个线程可以写入
shared_data = value;
std::cout << "Written: " << value << std::endl;
}

int main() {
std::thread writer1(write_data, 1);
std::thread reader1(read_data);
std::thread writer2(write_data, 2);
std::thread reader2(read_data);

writer1.join();
reader1.join();
writer2.join();
reader2.join();

return 0;
}

5. std::call_once 和 std::once_flag

std::call_oncestd::once_flag 是用于确保某段代码只执行一次的机制,通常用于实现线程安全的单例模式或初始化代码。

示例:

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
#include <iostream>
#include <thread>
#include <mutex>

std::once_flag flag;

void initialize() {
std::cout << "Initialized once!" << std::endl;
}

void thread_func() {
std::call_once(flag, initialize); // 确保 initialize 只执行一次
}

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

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

return 0;
}

6. 死锁与避免

在多线程编程中,如果多个线程相互等待对方释放锁,可能会造成死锁。为了避免死锁,可以遵循以下准则: - 锁的顺序: 确保所有线程按照相同的顺序获取多个锁。 - 使用 std::lock: C++ 提供了 std::lock 可以同时锁定多个互斥量,避免死锁。 - 避免持有锁进行阻塞操作: 在持有锁的情况下避免执行可能导致线程阻塞的操作。

避免死锁示例:

1
2
3
4
5
6
7
8
std::mutex m1, m2;

void func1() {
std::lock(m1, m2); // 同时锁定 m1 和 m2
std::lock_guard<std::mutex> lock1(m1, std::adopt_lock); // 锁已经锁定,直接接管
std::lock_guard<std::mutex> lock2(m2, std::adopt_lock); // 锁已经锁定,直接接管
// 执行操作
}

通过了解和正确使用 C++ 中的锁机制,可以有效避免多线程环境中的数据竞争问题,确保程序的正确性和稳定性。

条件变量

C++ 中的条件变量(std::condition_variable)是一种用于多线程同步的机制。它使得线程能够等待某个条件的满足,通常与互斥量(std::mutex)一起使用,以确保线程安全地访问共享资源。

1. 条件变量的作用

条件变量允许一个或多个线程等待,直到另一个线程通知条件满足(通过调用 notify_one()notify_all())。它常用于以下场景: - 生产者-消费者模式: 当消费者线程需要等待生产者生产数据时,消费者可以等待条件变量;生产者完成生产后,通知消费者条件已经满足。 - 同步等待事件: 多个线程可以等待某个特定事件发生。

2. 条件变量的成员函数

C++ 提供了两个条件变量类: - std::condition_variable:用于与 std::mutex 配合。 - std::condition_variable_any:可以与任何类型的锁配合(如 std::unique_lockstd::shared_lock 等)。

常用成员函数:

  • wait():使线程阻塞,直到被通知并且条件成立。
  • wait_for():使线程等待一段时间,超时后自动唤醒。
  • wait_until():使线程等待直到某个时间点,超时后自动唤醒。
  • notify_one():唤醒一个等待该条件的线程。
  • notify_all():唤醒所有等待该条件的线程。

3. 使用示例

示例1:基本使用(生产者-消费者模型)

以下是一个简单的生产者-消费者模型,演示如何使用条件变量:

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>

std::mutex mtx;
std::condition_variable cv;
std::queue<int> data_queue;
bool done = false; // 标记生产者是否完成

// 生产者线程
void producer() {
for (int i = 0; i < 5; ++i) {
std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 模拟生产时间
std::lock_guard<std::mutex> lock(mtx);
data_queue.push(i);
std::cout << "Produced: " << i << std::endl;
cv.notify_one(); // 通知消费者有新数据
}

// 生产完成,通知消费者
{
std::lock_guard<std::mutex> lock(mtx);
done = true;
cv.notify_all(); // 通知所有等待线程生产完成
}
}

// 消费者线程
void consumer() {
while (true) {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, [] { return !data_queue.empty() || done; }); // 等待有数据或生产完成

if (!data_queue.empty()) {
int data = data_queue.front();
data_queue.pop();
lock.unlock(); // 解锁后再处理数据
std::cout << "Consumed: " << data << std::endl;
} else if (done) {
break; // 生产结束,跳出循环
}
}
}

int main() {
std::thread prod(producer);
std::thread cons(consumer);

prod.join();
cons.join();

return 0;
}

代码解析:

  1. 生产者线程:每隔一段时间向队列中插入数据,并通过 cv.notify_one() 通知等待的消费者线程。
  2. 消费者线程:调用 cv.wait() 等待条件变量,直到队列不为空(即有数据可消费)或生产者完成。wait() 函数中使用了 lambda 表达式来判断条件是否成立。
  3. 通知机制:消费者被生产者通过 notify_one() 唤醒。生产完成后,通过 notify_all() 唤醒所有等待线程。

示例2:超时等待

有时,线程需要在等待某个条件时设置超时时间。可以使用 wait_for()wait_until() 实现超时机制。

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
33
34
35
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <chrono>

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

void wait_for_ready() {
std::unique_lock<std::mutex> lock(mtx);
if (cv.wait_for(lock, std::chrono::seconds(2), []{ return ready; })) {
std::cout << "Thread was notified and ready is true." << std::endl;
} else {
std::cout << "Timed out waiting for the condition." << std::endl;
}
}

void set_ready() {
std::this_thread::sleep_for(std::chrono::seconds(1)); // 模拟延迟操作
std::lock_guard<std::mutex> lock(mtx);
ready = true;
cv.notify_one(); // 通知等待的线程
}

int main() {
std::thread t1(wait_for_ready);
std::thread t2(set_ready);

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

return 0;
}

代码解析:

  1. wait_for_ready() 使用 cv.wait_for(),等待 ready 状态变为 true,最多等待2秒。如果 ready 变为 true,线程会立即继续执行,否则在超时后继续。
  2. set_ready() 线程在1秒后设置 readytrue,并通过 cv.notify_one() 通知等待中的线程。

4. 常见用法模式

1. 等待条件并防止虚假唤醒

条件变量可能会发生虚假唤醒(即线程被唤醒时,条件并未满足)。为了防止虚假唤醒,通常会使用一个条件谓词(predicate)来检查条件是否真正满足。

1
2
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, []{ return condition_is_met(); }); // condition_is_met() 是判断条件的谓词

cv.wait(lock, predicate) 形式会一直等待,直到谓词返回 true,并且会自动处理锁的释放和重新加锁。

2. 生产者-消费者模型中的等待与通知

生产者-消费者模式是条件变量的典型应用,生产者通过 notify_one()notify_all() 通知消费者有新数据可用,消费者则通过 wait() 等待新数据的到来。

  • 单个消费者: 使用 notify_one() 唤醒一个等待的消费者。
  • 多个消费者: 如果有多个消费者等待,可以使用 notify_all() 唤醒所有等待的线程。

5. 总结

  • 条件变量允许线程等待某一条件的满足,同时防止忙等待(即线程反复检查条件是否成立)。
  • 锁与条件变量通常配合使用,确保对共享资源的访问是线程安全的。
  • wait()notify_one() / notify_all() 是条件变量最常用的操作,wait_for()wait_until() 提供了超时等待的功能。

原子操作

C++ 中的 atomic(原子操作)是用于实现线程安全的操作,确保在多线程环境下访问和修改变量时不会出现竞争条件(race condition)。C++11 引入了原子操作,主要通过 std::atomic 模板类来提供。atomic 类型可以在没有锁的情况下确保对共享数据的安全访问和修改,它们的操作是不可分割的,即对原子变量的操作在不同线程之间不会产生中断。

1. std::atomic 的特性

  • 无锁机制std::atomic 提供的操作是无锁的,直接依赖硬件支持的原子指令,因此性能通常比使用锁的同步机制更好。
  • 原子性:对 std::atomic 类型的读、写、交换等操作是不可分割的,这意味着这些操作会在内部自动保证线程安全,不需要额外的锁。
  • 线程安全:多个线程可以同时访问 std::atomic 类型的变量,而不会引发数据竞争。

2. std::atomic 的基本用法

std::atomic 是一个模板类,支持各种内置数据类型(如 intboolpointer 等)。可以通过 std::atomic<T> 来创建原子类型的变量,其中 T 是具体的数据类型。

示例:

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

std::atomic<int> counter(0); // 定义一个原子整型变量,初始值为0

void increment() {
for (int i = 0; i < 1000; ++i) {
++counter; // 原子递增操作
}
}

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

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

std::cout << "Counter: " << counter << std::endl; // 输出2000,因为两个线程总共递增2000次
return 0;
}

在这个例子中,counter 是一个原子变量,多个线程可以安全地对它进行操作。由于 ++counter 是原子操作,不需要加锁,也不会产生数据竞争。

3. 常用的 std::atomic 操作

1. 加载和存储

  • load():读取原子变量的值。
  • store():将值存储到原子变量中。

示例:

1
2
3
std::atomic<int> x(0);
x.store(5); // 存储值5
int val = x.load(); // 加载x的值

2. 原子加减

  • fetch_add(value):对原子变量执行加法操作,并返回原值。
  • fetch_sub(value):对原子变量执行减法操作,并返回原值。

示例:

1
2
3
std::atomic<int> counter(0);
counter.fetch_add(1); // counter += 1
counter.fetch_sub(1); // counter -= 1

3. 交换操作

  • exchange(new_value):将原子变量的值设置为 new_value,并返回旧值。

示例:

1
2
std::atomic<int> x(5);
int old_value = x.exchange(10); // 将x的值设置为10,返回旧值5

4. 比较并交换(CAS)

  • compare_exchange_weak(expected, desired)compare_exchange_strong(expected, desired):这两个函数用于实现比较并交换(Compare-And-Swap,CAS)操作。CAS 是一种常见的无锁同步原语,它将原子变量的当前值与 expected 进行比较,如果相等,则将其设置为 desired,否则将 expected 更新为当前值。

示例:

1
2
3
4
5
6
7
std::atomic<int> x(10);
int expected = 10;
if (x.compare_exchange_strong(expected, 20)) {
std::cout << "Value was 10, now set to 20" << std::endl;
} else {
std::cout << "Value was not 10, it is " << x.load() << std::endl;
}

4. 内存顺序(Memory Ordering)

C++ 的 atomic 支持不同的内存顺序(memory ordering),用于控制操作在多核 CPU 上的可见性和同步。内存顺序决定了线程间的操作顺序如何被观察。

常见的内存顺序选项: - std::memory_order_relaxed: 最弱的内存序,允许最大限度的优化,只保证原子操作本身的原子性,但不保证内存顺序。 - std::memory_order_acquire: 保证在读取操作之前的所有读写操作不会被重排序。 - std::memory_order_release: 保证写入操作不会被重排序到写入操作之后。 - std::memory_order_acq_rel: 结合了 acquirerelease 的语义。 - std::memory_order_seq_cst: 默认的内存顺序,提供最强的顺序保证,即所有操作都按照顺序执行,防止 CPU 或编译器对操作进行重排序。

示例:

1
2
3
4
5
6
7
std::atomic<int> x(0);

// 使用 relaxed 内存顺序
x.store(5, std::memory_order_relaxed);

// 使用 acquire 内存顺序
int value = x.load(std::memory_order_acquire);

5. std::atomic_flag

std::atomic_flag 是一种专门用于实现自旋锁(spinlock)的原子类型,它比 std::atomic<bool> 更加底层和高效,只支持两个操作: - test_and_set():将标志位设置为 true,并返回原来的值。 - clear():将标志位清除(设置为 false)。

示例:使用 atomic_flag 实现自旋锁

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 <atomic>
#include <iostream>
#include <thread>

std::atomic_flag lock_flag = ATOMIC_FLAG_INIT;

void spinlock_lock() {
while (lock_flag.test_and_set(std::memory_order_acquire)) {
// 自旋等待,直到锁被释放
}
}

void spinlock_unlock() {
lock_flag.clear(std::memory_order_release); // 释放锁
}

void critical_section(int thread_id) {
spinlock_lock(); // 获取锁
std::cout << "Thread " << thread_id << " is in the critical section." << std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 模拟工作
spinlock_unlock(); // 释放锁
}

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

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

return 0;
}

在这个示例中,atomic_flag 用作自旋锁,线程会持续检查锁的状态,直到它能够获得锁。

6. 总结

  • std::atomic 提供了一种无锁的、线程安全的机制来访问和修改共享数据。
  • 基本操作 包括 load()store()fetch_add()exchange() 和 CAS 操作。
  • 内存顺序 可以用来控制操作的可见性和重排序,默认使用最严格的顺序保证(std::memory_order_seq_cst)。
  • atomic_flag 是最简单的原子类型,常用于实现自旋锁。

通过使用 atomic,可以在不使用锁的情况下实现线程安全的操作,从而提高并发程序的性能,尤其在多核系统中。