18 多线程编程之线程安全的数据结构

在上一篇教程中,我们讨论了多线程编程中的互斥量条件变量,了解了如何在多线程程序中保护共享数据的访问。接下来,我们将深入探讨如何构建线程安全的数据结构,确保在多线程环境下数据的一致性与安全性。这些线程安全的数据结构常用于需要并发访问的场景,比如高性能的服务器和复杂的应用程序。

线程安全的数据结构

在多线程编程中,数据结构本身的设计也需要考虑到线程的安全。我们一般可以通过两种方式确保线程安全:

  1. 通过锁(Mutex)保护数据结构的访问
  2. 使用无锁编程(Lock-free Programming)

使用锁保护数据结构

最直观的解决方案是使用锁来同步对数据结构的访问。下面我们以一个简单的线程安全的栈(Stack)为例,通过互斥量来保护数据的正确性。

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

class ThreadSafeStack {
private:
std::stack<int> _stack;
mutable std::mutex _mutex;

public:
void push(int value) {
std::lock_guard<std::mutex> guard(_mutex);
_stack.push(value);
}

void pop() {
std::lock_guard<std::mutex> guard(_mutex);
if (!_stack.empty()) {
_stack.pop();
}
}

bool empty() const {
std::lock_guard<std::mutex> guard(_mutex);
return _stack.empty();
}
};

void threadFunction(ThreadSafeStack& tsStack) {
for (int i = 0; i < 10; ++i) {
tsStack.push(i);
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
}

int main() {
ThreadSafeStack tsStack;
std::vector<std::thread> threads;

for (int i = 0; i < 5; ++i) {
threads.emplace_back(threadFunction, std::ref(tsStack));
}

for (auto& t : threads) {
t.join();
}

std::cout << "Stack empty: " << tsStack.empty() << std::endl;
return 0;
}

在上面的例子中,我们实现了一个线程安全的栈类ThreadSafeStack。通过std::mutexstd::lock_guard,我们确保了在不同线程之间对栈的并发访问是安全的。lock_guard负责管理互斥量的加锁与解锁,避免了手动加锁和解锁的错误。

无锁编程

无锁编程是另一种确保线程安全的方式,通常采用原子操作和特殊的算法结构来实现。C++11引入了原子类型(std::atomic),可以在多个线程中安全地操作这些类型而无需锁。

下面,我们看看如何使用std::atomic构建一个简单的线程安全计数器:

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

class ThreadSafeCounter {
private:
std::atomic<int> _count;

public:
ThreadSafeCounter() : _count(0) {}

void increment() {
_count.fetch_add(1, std::memory_order_relaxed);
}

int get() const {
return _count.load(std::memory_order_relaxed);
}
};

void incrementCounter(ThreadSafeCounter& counter) {
for (int i = 0; i < 1000; ++i) {
counter.increment();
}
}

int main() {
ThreadSafeCounter counter;
std::vector<std::thread> threads;

for (int i = 0; i < 10; ++i) {
threads.emplace_back(incrementCounter, std::ref(counter));
}

for (auto& t : threads) {
t.join();
}

std::cout << "Final count: " << counter.get() << std::endl;
return 0;
}

在这个例子中,我们实现了一个简单的线程安全计数器。使用std::atomic<int>,我们可以直接对计数进行原子递增,从而免去了使用互斥量的开销。这样的实现非常适合于高性能的应用场景。

设计线程安全的数据结构的注意事项

在设计线程安全的数据结构时,需要考虑以下几点:

  1. 锁的粒度:尽量使用细粒度锁,避免造成不必要的性能瓶颈。
  2. 死锁风险:确保在获取多个锁时,遵守一定的顺序,避免死锁情况的出现。
  3. 内存模型:理解C++的内存模型,合理运用内存序列(memory order),避免数据竞争。

总结

本篇教程探讨了如何设计线程安全的数据结构,包含使用锁和无锁方法。通过案例展示如何实现一个线程安全的栈和计数器,使您在多线程编程中能更好地管理数据的一致性和安全性。

在下一篇教程中,我们将看看C++20的新特性,特别是概念与约束,这些特性将进一步提升我们编写泛型代码的能力及其安全性。敬请期待!

18 多线程编程之线程安全的数据结构

https://zglg.work/c-plusplus-one/18/

作者

IT教程网(郭震)

发布于

2024-08-10

更新于

2024-08-22

许可协议

分享转发

交流

更多教程加公众号

更多教程加公众号

加入星球获取PDF

加入星球获取PDF

打卡评论