文章目录
- 1. Linux线程同步
- 1.1 条件变量
- 1.2 同步概念与竞态条件
- 1.3 条件变量函数
- 示例代码1:
- 示例代码2
- 1.4 为什么 pthread_ cond_ wait 需要互斥量
- 1.5 条件变量使用规范
- 2. 生产者消费者模型
- 3. 读者 写者 问题
- 3.1 读写锁
- 3.2 读写锁的相关接口
- 4. 扩展:无锁化模型
- 4.1 基本概念
- 4.2 常见的无锁数据结构和算法
- 4.3 实现无锁编程的关键技术
- ① 示例:无锁栈的简单实现
- ② 原子操作 atomic的使用
- 4.4 总结
1. Linux线程同步
1.1 条件变量
- 当一个线程互斥地访问某个变量时,可能在其它线程改变状态之前,无法做任何操作。
- 例如一个线程访问队列时,发现队列为空,它只能等待,只到其它线程将一个节点添加到队列中。这种情况就需要用到条件变量
1.2 同步概念与竞态条件
- 同步:在保证数据安全的前提下,使线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题
- 竞态条件:因为时序问题,而导致程序异常。
1.3 条件变量函数
① 初始化:
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);
- 参数:
cond
:要初始化的条件变量attr
:NULL
② 销毁:
int pthread_cond_destroy(pthread_cond_t *cond)
③ 等待条件满足:
int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
- 参数:
cond
:要在这个条件变量上等待mutex
:互斥量,后面详细解释
④ 唤醒等待:
int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);
示例代码1:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
pthread_cond_t cond;
pthread_mutex_t mutex;
void *r1( void *arg )
{
while (true){
pthread_cond_wait(&cond, &mutex);
printf("活动\n");
}
}
void *r2(void *arg )
{
while (true) {
pthread_cond_signal(&cond);
sleep(1);
}
}
int main( void )
{
pthread_t t1, t2;
pthread_cond_init(&cond, NULL);
pthread_mutex_init(&mutex, NULL);
pthread_create(&t1, NULL, r1, NULL);
pthread_create(&t2, NULL, r2, NULL);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&cond);
}
执行函数,有以下输出:
[root@localhost linux]# ./test.out
活动
活动
活动
示例代码2
直接利用condition_variable:
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable> // 条件变量
std::mutex mtx;
std::condition_variable cv;
bool ready = false;
void print_id(char id, bool flag)
{
for(int i = 0; i < 10; ++i) {
std::unique_lock<std::mutex> lck(mtx);
cv.wait(lck, [&] { return ready == flag; });
std::cout << "ThreadId: "<< id << std::endl;
ready = !ready;
cv.notify_all();
}
}
int main() {
std::thread t1(print_id, '1', true);
std::thread t2(print_id, '2', false);
t1.join();
t2.join();
return 0;
}
执行结果:
1.4 为什么 pthread_ cond_ wait 需要互斥量
- 条件等待是线程间同步的一种手段,如果只有一个线程,条件不满足,则会一直等待下去
- 所以必须有一个线程通过某些操作,改变共享变量,使原先不满足的条件变得满足,并且正常通知等待在条件变量上的线程
- 条件不会无缘无故的突然满足,一定会关联到共享数据的变化。所以必须要用互斥锁保护。
- 没有互斥锁就无法安全的获取和修改共享数据
1.5 条件变量使用规范
一般使用条件变量可以通过以下的方式:
等待条件代码:
pthread_mutex_lock(&mutex);
while (条件为假)
pthread_cond_wait(cond, mutex);
修改条件
pthread_mutex_unlock(&mutex);
给条件发信号代码:
pthread_mutex_lock(&mutex);
设置条件为真
pthread_cond_signal(cond);
pthread_mutex_unlock(&mutex);
2. 生产者消费者模型
关于生产者消费者模型的概念 与 代码实现,可以看下面的两篇文章:
- 基于互斥锁的生产者消费者模型
- 基于 BlockQueue(阻塞队列) 的 生产者消费者模型
3. 读者 写者 问题
3.1 读写锁
- 在编写多线程时,有一种十分常见的情况:有些公共数据修改的机会较少。
- 相比于改写,更多是执行读操作。 通常读的过程往往伴随着查找的操作,中间耗时长。给这种代码段加锁,会极大地降低程序效率。
有没有一种方法,可以更好的处理这种多读少写的情况呢?
有,即读写锁。
对上图进行解释,即:
- 读锁(Shared Lock):允许多个线程同时获得读锁,以进行读取操作,但在读锁被持有时,写锁无法获得。
- 写锁(Exclusive Lock):在写锁被持有时,其他线程既不能获得读锁也不能获得写锁。写锁保证独占访问以进行写操作。
3.2 读写锁的相关接口
设置读写优先:
int pthread_rwlockattr_setkind_np(pthread_rwlockattr_t *attr, int pref);
/*
pref 共有 3 种
Ⅰ PTHREAD_RWLOCK_PREFER_READER_NP (默认设置) 读者优先,可能会导致写者饥饿情况
Ⅱ PTHREAD_RWLOCK_PREFER_WRITER_NP 写者优先,目前有 BUG,导致表现行为和 Ⅰ 一致
Ⅲ PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP 写者优先,但写者不能递归加锁
*/
初始化:
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,const pthread_rwlockattr_t *restrict attr)
销毁
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
加锁 / 解锁
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
下面写一个读写锁的简单示例:
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
// 共享资源
int shared_data = 0;
// 读写锁
pthread_rwlock_t rwlock;
// 读操作
void* read_operation(void* thread_id) {
pthread_rwlock_rdlock(&rwlock); // 获取读锁
printf("Thread %ld reading data: %d\n", (long)thread_id, shared_data);
// 模拟读取操作的延迟
usleep(100000);
pthread_rwlock_unlock(&rwlock); // 释放锁
return NULL;
}
// 写操作
void* write_operation(void* thread_id) {
pthread_rwlock_wrlock(&rwlock); // 获取写锁
shared_data = (int)(long)thread_id * 10;
printf("Thread %ld writing data: %d\n", (long)thread_id, shared_data);
// 模拟写入操作的延迟
usleep(100000);
pthread_rwlock_unlock(&rwlock); // 释放锁
return NULL;
}
int main() {
pthread_t threads[7];
pthread_rwlockattr_t attr;
// 初始化读写锁属性
pthread_rwlockattr_init(&attr);
// 设置读写锁属性(例如,锁的类型)
pthread_rwlockattr_setkind_np(&attr, PTHREAD_RWLOCK_PREFER_WRITER_NP);
// 初始化读写锁
pthread_rwlock_init(&rwlock, &attr);
// 启动几个读线程
for (long i = 1; i <= 5; ++i) {
pthread_create(&threads[i-1], NULL, read_operation, (void*)i);
}
// 启动几个写线程
for (long i = 1; i <= 2; ++i) {
pthread_create(&threads[i+4], NULL, write_operation, (void*)i);
}
// 等待所有线程完成
for (int i = 0; i < 7; ++i) {
pthread_join(threads[i], NULL);
}
// 销毁读写锁
pthread_rwlock_destroy(&rwlock);
pthread_rwlockattr_destroy(&attr);
return 0;
}
执行上面的代码,会有以下的执行结果:
Thread 1 reading data: 0
Thread 2 reading data: 0
Thread 3 reading data: 0
Thread 4 reading data: 0
Thread 5 reading data: 0
Thread 6 writing data: 10
Thread 7 writing data: 20
Thread 1 reading data: 20
Thread 2 reading data: 20
Thread 3 reading data: 20
Thread 4 reading data: 20
Thread 5 reading data: 20
4. 扩展:无锁化模型
无锁化编程(Lock-Free Programming)是一种并发编程技术,旨在避免使用传统的锁机制(如互斥锁、信号量等),从而减少锁带来的性能开销和复杂性。
- 无锁编程适用于高并发环境,因为它可以显著提高程序的吞吐量和响应性。
4.1 基本概念
-
无锁的定义:
- 在无锁编程中,算法或数据结构保证 至少有一个线程在有限时间内完成操作,而其他线程的操作不会导致全局进程的阻塞 。无锁化编程中的操作不使用传统的互斥锁机制,而是使用原子操作和算法保证线程安全。
-
无锁的优势:
- 减少线程等待:由于不使用锁,线程不会因等待锁而被阻塞,从而减少了上下文切换的开销。
- 提高并发性:无锁编程可以提高系统的吞吐量,因为多个线程可以并发地执行操作,不必等待其他线程完成。
- 降低死锁风险:由于不使用锁,死锁和其他锁相关的问题(如优先级反转)不再是问题。
-
无锁编程的问题:
- 复杂性:编写无锁算法通常比传统的锁算法要复杂得多,需要对并发和原子操作有深入的理解。
- 可见性问题:在多核系统中,需要确保线程对共享数据的修改对其他线程是可见的,这通常通过原子操作来保证。
- 性能开销:无锁算法可能在某些情况下引入额外的性能开销,如自旋等待的开销。
4.2 常见的无锁数据结构和算法
-
无锁队列(Lock-Free Queue):
- 无锁队列允许多个线程同时执行入队和出队操作,而不会导致阻塞。常用的实现方式包括基于环形缓冲区的队列和基于链表的队列。
-
无锁栈(Lock-Free Stack):
- 无锁栈是一种支持并发推入(push)和弹出(pop)操作的数据结构,通常实现为链表结构,每个操作都通过原子操作来实现。
-
无锁哈希表(Lock-Free Hash Table):
- 无锁哈希表允许并发地进行插入、删除和查找操作,常用的实现方式包括分段锁无锁哈希表和基于链表的无锁哈希表。
-
无锁计数器(Lock-Free Counter):
- 无锁计数器用于支持高并发的增量操作,通常使用原子操作来保证每次对计数器的更新都是安全的。
4.3 实现无锁编程的关键技术
-
原子操作:
- 原子操作是无锁编程的基础,它保证了对共享数据的操作在执行时是不可分割的。在 C++ 中,
std::atomic
提供了对原子操作的支持。
- 原子操作是无锁编程的基础,它保证了对共享数据的操作在执行时是不可分割的。在 C++ 中,
-
自旋锁(Spinlock):
- 自旋锁是一种轻量级的锁机制,线程在尝试获取锁时,会在一个小的循环中反复检查锁的状态,而不是被阻塞。虽然自旋锁本身不是无锁的,但在无锁编程中,自旋锁可以用于实现某些无锁算法的关键部分。
-
CAS(Compare-And-Swap)操作:
- CAS 是一种常用的原子操作,它用于比较一个值与预期值是否相等,如果相等则更新为新值。CAS 是许多无锁算法的核心操作。
① 示例:无锁栈的简单实现
#include <atomic>
template<typename T>
class LockFreeStack {
public:
LockFreeStack() : head(nullptr) {}
~LockFreeStack() {
while (Node* old_head = head.load()) {
head.store(old_head->next);
delete old_head;
}
}
void push(const T& value) {
Node* new_node = new Node(value);
new_node->next = head.load();
while (!head.compare_exchange_weak(new_node->next, new_node)) {
// 如果 compare_exchange_weak 失败,说明 head 已经被其他线程修改了
// 需要将 new_node->next 更新为当前 head 并重试
}
}
bool pop(T& value) {
Node* old_head = head.load();
while (old_head && !head.compare_exchange_weak(old_head, old_head->next)) {
// 如果 compare_exchange_weak 失败,说明 head 已经被其他线程修改了
// 需要将 old_head 更新为当前 head 并重试
}
if (old_head) {
value = old_head->data;
delete old_head;
return true;
}
return false;
}
private:
struct Node {
T data;
Node* next;
Node(const T& value) : data(value), next(nullptr) {}
};
std::atomic<Node*> head;
};
在这个例子中,我们实现了一个无锁栈。std::atomic
用于保证对栈顶指针 head
的原子操作。compare_exchange_weak
是核心的无锁操作,用于在更新栈顶指针时避免数据竞争。
② 原子操作 atomic的使用
#include <iostream>
#include <thread>
#include <atomic>
#include <vector>
// 原子计数器
std::atomic<int> counter(0);
// 线程函数:增加计数器
void incrementCounter(int iterations) {
for (int i = 0; i < iterations; ++i) {
counter.fetch_add(1, std::memory_order_relaxed);
}
}
int main() {
const int numThreads = 4;
const int iterationsPerThread = 1000;
// 启动多个线程,每个线程增加计数器的值
std::vector<std::thread> threads;
for (int i = 0; i < numThreads; ++i) {
threads.emplace_back(incrementCounter, iterationsPerThread);
}
// 等待所有线程完成
for (auto& thread : threads) {
thread.join();
}
// 输出最终计数器的值
std::cout << "Final counter value: " << counter.load() << std::endl;
return 0;
}
4.4 总结
无锁化编程通过避免传统锁机制,能够提高程序的并发性和性能。它的核心在于使用原子操作和无锁算法来确保线程安全,而不是依赖于锁来保护共享资源。虽然无锁编程可以提供显著的性能优势,但它也带来了更高的复杂性;