1.概要
读写锁的理解
读的时候,只要是读的线程都不受限制,但不能写。
写的时候,线程独占,任何写和读的线程都不可以。
最初我以为,只有限制写就可以了,读完全不受现在,但是有可能读到不完整的数据,比如写一半的数据等等待,所以这就是对读的那部分控制,共享锁的价值。
2.std::shared_mutex 详细说明
std::shared_mutex
是C++17中引入的一个同步原语,用于在多线程环境下提升对共享资源的访问效率。与传统的互斥锁(如std::mutex
)不同,std::shared_mutex
允许多个线程以只读模式共享对资源的访问,但写入操作必须独占资源,以防止同时有其他线程对共享资源进行读取或写入。
关键特性
- 两种访问级别:
- 共享访问(读锁):多个线程可以同时拥有读锁,允许它们并行读取数据。
- 独占访问(写锁):只有一个线程可以拥有写锁,以此来修改数据。当一个线程拥有写锁时,其他线程无法获得读锁或写锁。
- 适用场景:这种机制非常适合于多读少写的场景,因为它能够最大化读操作的并发性,同时确保写操作的安全性。
使用方法
为了安全地使用std::shared_mutex
,你需要理解其两种锁模型:
- 独占锁(写锁):当你需要对共享数据进行写入操作时,应该使用独占锁。独占锁确保了在执行写入操作期间,没有其它线程对数据进行读或写操作。
- 共享锁(读锁):当多个线程需要读取共享数据但不进行修改时,可以使用共享锁。多个线程可以同时拥有读锁,但如果有线程正在使用独占锁(写锁),则其他线程无法获得读锁或写锁。
示例
下面是一个简单的示例,展示了如何使用std::shared_mutex
来同步对共享资源的访问:
#include <iostream>
#include <thread>
#include <vector>
#include <shared_mutex> std::shared_mutex mtx;
int shared_data = 0; void reader(int id) { std::shared_lock<std::shared_mutex> lock(mtx); // 读取共享数据 std::cout << "Reader " << id << " reads: " << shared_data << std::endl;
} void writer(int value) { std::unique_lock<std::shared_mutex> lock(mtx); // 修改共享数据 shared_data = value; std::cout << "Writer writes: " << shared_data << std::endl;
} int main() { std::vector<std::thread> threads; // 创建多个读线程 for (int i = 0; i < 5; ++i) { threads.emplace_back(reader, i); } // 创建一个写线程,稍后进行写入操作 threads.emplace_back(writer, 42); // 等待所有线程完成 for (auto& t : threads) { t.join(); } return 0;
}
请注意,上述示例中的写线程在创建后并不会立即执行,因为线程的调度顺序是由操作系统决定的。为了更精确地控制读写操作的顺序,你可能需要使用条件变量或其他同步机制。
3.共享锁和没有锁有啥区别
C++中的共享锁(如std::shared_lock
配合std::shared_mutex
使用)和没有锁(即不使用任何同步机制)之间存在显著的区别。这些区别主要体现在对多线程环境中共享资源访问的控制和安全性上。
没有锁
如果不使用任何锁或同步机制,多个线程可以自由访问和修改共享资源,这可能导致以下问题:
- 数据竞争(Data Race):当两个或更多的线程并发访问同一内存位置,且至少有一个线程是写入操作,并且线程间没有使用适当的同步来协调这些访问时,就会发生数据竞争。数据竞争会导致未定义的行为,因为写入操作的顺序是不确定的。
- 不一致的状态:由于线程之间没有协调它们的访问,一个线程可能看到由另一个线程部分修改的变量,导致程序处于不一致的状态。
- 难以调试:没有锁的多线程程序通常很难调试,因为线程的执行顺序是不确定的,并且程序的行为可能因环境而异(例如,CPU核心数量、线程调度策略等)。
共享锁
共享锁允许多个线程同时读取共享资源,但阻止任何线程在已有线程读取或写入时写入资源。使用共享锁可以解决上述问题:
- 防止数据竞争:通过确保在读取或写入共享资源时只有一个线程可以执行,共享锁防止了数据竞争。
- 保持数据一致性:由于写入操作是独占的,因此当一个线程正在写入时,其他线程无法读取或写入,从而确保了数据的一致性。
- 提高并发性:与互斥锁(如
std::mutex
)相比,共享锁允许多个线程同时读取共享资源,从而提高了并发性。这对于多读少写的场景特别有用。 - 简化编程:使用共享锁可以简化多线程编程,因为程序员不需要手动协调线程之间的访问,而是可以依靠锁机制来自动处理。
总结
没有锁的多线程程序可能导致数据竞争、不一致的状态和难以调试的问题。而共享锁通过确保在读取或写入共享资源时只有一个线程可以执行,解决了这些问题,并提高了并发性。因此,在需要多线程访问共享资源的场景中,使用适当的锁或同步机制是非常重要的。
4.读写锁和普通锁的对比试验
为了对比C++中的读写锁(std::shared_mutex
或其他第三方读写锁实现)和普通锁(如 std::mutex
)的性能,我们可以设计一个简单的试验。在这个试验中,我们将创建多个线程,其中一些线程作为读者(只读取数据),而另一些线程作为写者(修改数据)。
以下是一个简化的示例,展示了如何使用std::mutex
和std::shared_mutex
来模拟这种场景,并进行性能对比。
使用 std::mutex
#include <iostream>
#include <thread>
#include <vector>
#include <mutex>
#include <chrono> std::mutex mtx;
int data = 0; void reader(int id) { for (int i = 0; i < 10000; ++i) { std::lock_guard<std::mutex> lock(mtx); // 假设我们只是读取数据 int value = data; // ...(省略其他读取操作) }
} void writer(int id) { for (int i = 0; i < 1000; ++i) { std::lock_guard<std::mutex> lock(mtx); // 假设我们只是写入数据 data = id; // ...(省略其他写入操作) std::this_thread::sleep_for(std::chrono::milliseconds(1)); // 模拟耗时写入 }
} int main() { auto start = std::chrono::high_resolution_clock::now(); std::vector<std::thread> threads; for (int i = 0; i < 5; ++i) { threads.emplace_back(reader, i); } for (int i = 0; i < 2; ++i) { threads.emplace_back(writer, i); } for (auto& t : threads) { t.join(); } auto end = std::chrono::high_resolution_clock::now(); std::chrono::duration<double> diff = end - start; std::cout << "Time taken with std::mutex: " << diff.count() << " s\n"; return 0;
}
使用 std::shared_mutex
#include <iostream>
#include <thread>
#include <vector>
#include <shared_mutex>
#include <chrono> std::shared_mutex smtx;
int data = 0; void reader(int id) { for (int i = 0; i < 10000; ++i) { std::shared_lock<std::shared_mutex> lock(smtx); // 假设我们只是读取数据 int value = data; // ...(省略其他读取操作) }
} void writer(int id) { for (int i = 0; i < 1000; ++i) { std::unique_lock<std::shared_mutex> lock(smtx, std::defer_lock); lock.lock(); // 使用unique_lock来锁定,因为shared_lock不允许升级锁 // 假设我们只是写入数据 data = id; // ...(省略其他写入操作) std::this_thread::sleep_for(std::chrono::milliseconds(1)); // 模拟耗时写入 }
} // main函数与上述类似,只是锁的类型改变了
注意:在上面的std::shared_mutex
示例中,我们使用std::unique_lock
来锁定写操作,因为std::shared_lock
不允许锁升级(即,从共享锁升级到独占锁)。但是,为了保持示例的简单性,我们仍然使用std::shared_mutex
来展示读写锁的基本概念。
性能对比
要获得准确的性能对比,您应该多次运行这两个程序,并可能使用更复杂的基准测试工具来测量CPU时间、线程争用等。但是,从概念上讲,当读者数量远多于写者时,使用std::shared_mutex
(或类似的读写锁)应该比使用std::mutex
具有更好的性能,因为多个读者可以同时访问数据,而不会被彼此阻塞。
5.defer_lock 啥意思
在C++中,std::defer_lock
是一个标签类型,它用于与互斥体(std::mutex
、std::timed_mutex
、std::recursive_mutex
、std::recursive_timed_mutex
、std::shared_mutex
等)的std::unique_lock
或std::shared_lock
结合使用,以指示在构造时不立即锁定互斥体。
具体来说,当你使用std::unique_lock
或std::shared_lock
时,默认情况下,互斥体会在std::unique_lock
或std::shared_lock
的构造函数中被锁定。但是,如果你使用std::defer_lock
标签,则互斥体不会在构造函数中立即锁定,你可以稍后通过调用lock()
、try_lock()
或unlock()
等方法来控制锁定和解锁。
这在某些情况下可能是有用的,例如:
- 条件变量:当你使用条件变量(如
std::condition_variable
)时,你可能希望在等待条件变量之前不锁定互斥体,因为条件变量的wait()
方法会自动解锁互斥体并在条件满足时重新锁定它。在这种情况下,你可以使用std::defer_lock
来构造一个未锁定的std::unique_lock
,然后调用wait()
。 - 复杂的同步逻辑:在更复杂的同步场景中,你可能希望根据某些条件来决定是否立即锁定互斥体。使用
std::defer_lock
可以让你延迟这个决策。
下面是一个使用std::defer_lock
的示例:
#include <iostream>
#include <mutex>
#include <thread>
#include <chrono> std::mutex mtx; void print_block(int n, char c) { std::unique_lock<std::mutex> lck(mtx, std::defer_lock); // 在这里,mtx还没有被锁定 // 模拟一些工作 std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 现在我们决定锁定互斥体 lck.lock(); for (int i = 0; i < n; ++i) { std::cout << c; } std::cout << '\n'; // 解锁(但在这个例子中,当lck离开作用域时会自动解锁) lck.unlock(); // 或者简单地让lck离开作用域,它会在析构时自动解锁
} int main() { std::thread th1(print_block, 50, '*'); std::thread th2(print_block, 50, '$'); th1.join(); th2.join(); return 0;
}
注意,在这个示例中,尽管我们使用了std::defer_lock
,但实际上我们还是在lck.lock()
调用时立即锁定了互斥体。但是,这个例子展示了如何使用std::defer_lock
来延迟锁定决策。在更复杂的场景中,你可能会根据某些条件来决定是否调用lock()
。