在并发环境下锁的使用是家常便饭, 如何减少锁的使用是优化程序性能的一个方面. c++11里面新增了智能指针std::shared_ptr, 这个东西也许能给我们带来些启发. shared_ptr的一个特性是当引用计数为0时,它所拥有的堆内存会被自动释放. 利用这个特性我们可以做点实用的功能, 如下程序:
#include <assert.h> #include <chrono> #include <iostream> #include <mutex> #include <thread> std::shared_ptr<int> kNumPtr(new int(0)); std::mutex kmtx; std::shared_ptr<int> getSharedPtr() { kmtx.lock(); std::shared_ptr<int> ptr = kNumPtr; kmtx.unlock(); return ptr; } void dosomething(std::shared_ptr<int> ptr) { std::cout << "value: " << *ptr << std::endl; } int main() { auto threadProc = [&](){ for(size_t i = 0; i < 100; ++i) { kmtx.lock(); if(!kNumPtr.unique()){ kNumPtr.reset(new int(*kNumPtr)); } assert(kNumPtr.unique()); *kNumPtr = *kNumPtr + 1; kmtx.unlock(); std::this_thread::sleep_for(std::chrono::milliseconds(1)); } }; std::thread t1(threadProc); std::thread t2(threadProc); std::thread t3( [&](){ for(size_t i = 0; i < 100000; ++i) { std::shared_ptr<int> ptr = getSharedPtr(); dosomething(ptr); } } ); t1.join(); t2.join(); t3.join(); std::cout << "kNumPtr's value: " << *kNumPtr << std::endl; assert(*kNumPtr = 200); assert(kNumPtr.unique()); }
我们一共启动了三个线程, 这个程序模拟的场景是读的频率远远大于写的频率. 两个写线程模拟对共享数据的修改, 一个读线程用来模拟高频读的行为. dosomething模拟对数据的操作. 通常我们的程序需要在读操作的时候加上锁, 然而这里却只是加了一个锁用来做拷贝智能指针的操作,临界区的长度基本可以忽略, 如果dosomething的耗时很长, 比如服务器网络编程中通常的IO读写, 那么这个锁的开销其实是很大的, 对其它线程共享资源的访问性能延迟很大. 而如果换成现在的代码, 其代价几乎可以忽略.
再看写线程, 其实写线程用了copy_on_write技术, 将旧有的共享资源拷贝到新的地址空间上, 比较重要的一点是这个旧有的资源其实在reset之后 其引用计数(use_count)的值其实为0, 如果在读线程中处理完毕之后, 操作系统会自动将这份旧有内存进行释放, 这点才是真正有意思的地方.
程序地址:https://github.com/xiaopeifeng/CodeTricks/blob/master/shared_ptr_copyonwrite.cc