shared_ptr 实现
手写一个 shared_ptr 的实现, 主要需要注意以下几点: -
成员变量是两个指针, 一个指向被管理对象 ,
一个指向引用计数器 (其实是指向控制块,
但是这里简化为指向引用计数器) -
拷贝构造 和拷贝赋值 运算符都需要增加引用计数 器,
因为新对象共享同一个被管理对象 -
移动构造 和移动赋值 运算符不增加引用计数 器,
因为资源从源对象转移到目标对象, 源对象不再管理该资源 -
需要在析构函数中减少引用计数器, 当引用计数器为0时, 释放对象内存.
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 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 template <class T >class MysharedPtr { T* ptr; int * count; void cleanup () { if (count!=nullptr ){ (*count)--; if (*count==0 ){ delete ptr; delete count; } ptr = nullptr ; count = nullptr ; } } public : explicit MysharedPtr (T* node = nullptr ) : ptr(node){ if (ptr!=nullptr ){ count = new int (1 ); }else { count = nullptr ; } } ~MysharedPtr (){ cleanup (); } MysharedPtr (const MysharedPtr<T>& other){ ptr = other.ptr; count = other.count; if (count!=nullptr ) (*count)++; } MysharedPtr<T>& operator =(const MysharedPtr<T>& other){ if (&other==this ) return *this ; cleanup (); ptr = other.ptr; count = other.count; if (count!=nullptr ) (*count)++; return *this ; } MysharedPtr (MysharedPtr<T>&& other){ ptr = other.ptr; count = other.count; other.ptr = nullptr ; other.count = nullptr ; } MysharedPtr<T>& operator =(MysharedPtr<T>&& other){ if (&other==this ) return *this ; cleanup (); ptr = other.ptr; count = other.count; other.ptr = nullptr ; other.count = nullptr ; return *this ; } T* operator ->(){ return ptr; } T& operator *(){ return *ptr;} T* get () { return ptr; } int use_count () { return (count==nullptr )? 0 : *count; } };
不过, 上述实现有个比较重要的缺陷: 它不是线程安全的. 在多线程环境下,
多个线程可能同时修改引用计数器 , 导致竞态条件.
解决这个问题通常需要使用原子操作来保护引用计数器的修改.(但是依旧只保证了引用计数器的线程安全,
并不能保证被管理对象本身的线程安全,
如果需要的话还需要额外对对象本身加锁或者使用线程安全的数据结构)
下面是一个使用 C++11 原子操作实现的线程安全 shared_ptr,
同时使用了控制块来管理引用计数器和被管理对象:
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 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 template <class T >class MysharedPtr { struct ControlBlock { std::atomic<int > strong; ControlBlock (): strong (1 ){} }; T* ptr; ControlBlock* cb; void cleanup () { if (cb!=nullptr ){ if (cb->strong.fetch_sub (1 )==1 ){ delete ptr; delete cb; } ptr = nullptr ; cb = nullptr ; } } public : explicit MysharedPtr (T* node = nullptr ) : ptr(node){ if (ptr!=nullptr ){ cb = new ControlBlock (); }else { cb = nullptr ; } } ~MysharedPtr (){ cleanup (); } MysharedPtr (const MysharedPtr<T>& other){ ptr = other.ptr; cb = other.cb; if (cb!=nullptr ) cb->strong.fetch_add (1 ); } MysharedPtr<T>& operator =(const MysharedPtr<T>& other){ if (&other==this ) return *this ; cleanup (); ptr = other.ptr; cb = other.cb; if (cb!=nullptr ) cb->strong.fetch_add (1 ); return *this ; } MysharedPtr (MysharedPtr<T>&& other) noexcept { ptr = other.ptr; cb = other.cb; other.ptr = nullptr ; other.cb = nullptr ; } MysharedPtr<T>& operator =(MysharedPtr<T>&& other) noexcept { if (&other==this ) return *this ; cleanup (); ptr = other.ptr; cb = other.cb; other.ptr = nullptr ; other.cb = nullptr ; return *this ; } T* operator ->() const { return ptr; } T& operator *() const { return *ptr; } T* get () const { return ptr; } int use_count () const { return (cb==nullptr )? 0 : cb->strong.load (); } void reset () { cleanup (); } void reset (T* node) { cleanup (); *this = MysharedPtr (node); } void swap (MysharedPtr<T>& other) { std::swap (ptr, other.ptr); std::swap (cb, other.cb); } };
memcpy 实现
手写一个简单的 memcpy 函数, 用于内存拷贝. 需要注意以下几点: -
函数参数包括: 目标地址 dest , 源地址
src , 拷贝字节数 n -
函数返回值是目标地址 dest -
除了实现简单的逐字节拷贝之外, 还需要解决两个问题: -
效率问题 :
可以考虑按更大单位 (如4字节或8字节)进行拷贝, 提高效率.
但是需要考虑地址对齐 问题 - 重叠问题 :
如果源地址和目标地址有重叠, 则需要考虑使用 memmove
的策略, 避免数据被覆盖
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 #include <cstddef> #include <cassert> void * my_memcpy (void * dest, const void * src, size_t n) { assert (dest != nullptr && src != nullptr ); unsigned char * d = static_cast <unsigned char *>(dest); const unsigned char * s = static_cast <const unsigned char *>(src); for (size_t i = 0 ; i < n; ++i) { d[i] = s[i]; } return dest; }
C++标准规定:unsigned char
是唯一保证 可以无歧义地访问任意类型原始内存字节 的类型。它是“字节的同义词”,适合做内存拷贝、内存操作等底层工作。
上面的实现是一个简单版本的 memcpy, 它假设源地址和目标地址不重叠,
并且逐字节进行拷贝.
下面实现一个更高效的版本, 它按照8字节对齐 进行拷贝.
这个实现需要注意的点有: - 先处理前导字节, 直到目标地址对齐到8字节边界 -
然后按8字节块进行拷贝 - 最后处理剩余的字节
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 #include <cstddef> #include <cassert> void * my_memcpy (void * dest, const void * src, size_t n) { assert (dest != nullptr && src != nullptr ); unsigned char * d = static_cast <unsigned char *>(dest); const unsigned char * s = static_cast <const unsigned char *>(src); while (n > 0 && (reinterpret_cast <uintptr_t >(d) % 8 != 0 )) { *d++ = *s++; --n; } size_t num_blocks = n / 8 ; for (size_t i = 0 ; i < num_blocks; ++i) { *reinterpret_cast <uint64_t *>(d) = *reinterpret_cast <const uint64_t *>(s); d += 8 ; s += 8 ; } n %= 8 ; while (n > 0 ) { *d++ = *s++; --n; } return dest; }
对于重叠的情况, 需要根据源地址和目标地址的相对位置来决定拷贝的方向: -
如果目标地址在源地址之后, 则从后向前拷贝 - 例如: 假设 dest = 0x1004, src
= 0x1000, n = 8 - 则拷贝顺序应该是: 拷贝 src[7] 到 dest[7], 然后拷贝
src[6] 到 dest[6], 依此类推 - 如果目标地址在源地址之前, 则从前向后拷贝,
因为此时不会覆盖未拷贝的数据
这也就是标准库中的 memmove 函数的实现思路.
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 <cstddef> #include <cassert> void * my_memmove (void * dest, const void * src, size_t n) { assert (dest != nullptr && src != nullptr ); unsigned char * d = static_cast <unsigned char *>(dest); const unsigned char * s = static_cast <const unsigned char *>(src); if (d < s) { for (size_t i = 0 ; i < n; ++i) { d[i] = s[i]; } } else if (d > s) { for (size_t i = n; i > 0 ; --i) { d[i - 1 ] = s[i - 1 ]; } } return dest; }
多线程交替打印
实现三个线程交替打印 ABCABC ,
需要使用互斥锁 和条件变量 进行线程同步.
主要思路是: - 使用一个互斥锁保护共享变量, 该变量表示当前轮到哪个线程打印
- 使用条件变量让线程等待和通知, 当轮到某个线程打印时,
该线程被唤醒进行打印, 打印完成后修改共享变量并通知所有线程,
每个线程根据共享变量判断是否轮到自己打印
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 56 57 58 59 60 61 62 63 #include <iostream> #include <thread> #include <mutex> #include <condition_variable> std::mutex mtx; std::condition_variable cv; int turn = 1 ; void PrintA () { for (int i = 0 ; i < 10 ; ++i) { unique_lock<mutex> lock (mtx) ; cv.wait (lock, [] { return turn == 1 ; }); cout << "A" ; turn = 2 ; cv.notify_all (); } } void PrintB () { for (int i = 0 ; i < 10 ; ++i) { unique_lock<mutex> lock (mtx) ; cv.wait (lock, [] { return turn == 2 ; }); cout << "B" ; turn = 3 ; cv.notify_all (); } } void PrintC () { for (int i = 0 ; i < 10 ; ++i) { unique_lock<mutex> lock (mtx) ; cv.wait (lock, [] { return turn == 3 ; }); cout << "C" ; turn = 1 ; cv.notify_all (); } } int main () { const int iterations = 10 ; std::thread t1 (print_numbers, iterations) ; std::thread t2 (print_chars, iterations) ; std::thread t3 (print_symbols, iterations) ; t1. join (); t2. join (); t3. join (); return 0 ; }
简单HashMap 实现
手写一个简单的 HashMap 实现, 主要功能包括插入、查找和删除键值对.
需要注意以下几点: - 使用链地址法 解决哈希冲突 -
实现基本的哈希函数, 即根据键计算哈希值 -
提供插入、查找和删除操作的接口
下面是一个简单的 HashMap 实现, 使用字符串作为键, 整数作为值,
暂时不支持扩容机制
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 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 #include <iostream> #include <string> #include <vector> #include <list> #include <utility> #include <functional> class MyHashMap {private : size_t m_bucket_size; std::vector<std::list<std::pair<std::string, int >>> m_buckets; size_t _hash(const std::string& key) { size_t hash_value = std::hash<std::string>{}(key); return hash_value % m_bucket_size; } public : MyHashMap (size_t size = 100 ) : m_bucket_size (size) { m_buckets.resize (m_bucket_size); } void put (const std::string& key, int value) { size_t index = _hash(key); std::list<std::pair<std::string, int >>& bucket_list = m_buckets[index]; for (auto & pair : bucket_list) { if (pair.first == key) { std::cout << "更新 Key: " << key << " 的值为 " << value << std::endl; pair.second = value; return ; } } std::cout << "插入 Key: " << key << ", Value: " << value << std::endl; bucket_list.push_back ({key, value}); } int get (const std::string& key) { size_t index = _hash(key); const std::list<std::pair<std::string, int >>& bucket_list = m_buckets[index]; for (const auto & pair : bucket_list) { if (pair.first == key) { return pair.second; } } return -1 ; } void remove (const std::string& key) { size_t index = _hash(key); std::list<std::pair<std::string, int >>& bucket_list = m_buckets[index]; for (auto it = bucket_list.begin (); it != bucket_list.end (); ++it) { if (it->first == key) { std::cout << "删除 Key: " << key << std::endl; bucket_list.erase (it); return ; } } std::cout << "Key: " << key << " 不存在,无法删除" << std::endl; } };
生产者-消费者模型
实现一个简单的生产者-消费者模型, 使用条件变量和互斥锁进行同步.
主要功能包括: - 生产者线程不断生成数据并放入缓冲区 -
消费者线程不断从缓冲区取出数据进行处理 -
使用条件变量通知生产者和消费者线程, 注意需要两个条件变量,
一个用于通知生产者缓冲区有空位, 另一个用于通知消费者缓冲区有数据
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 56 57 58 59 #include <iostream> #include <queue> #include <mutex> #include <condition_variable> using namespace std;class SPSC_Buffer {private : queue<int > queue_; size_t capacity_; mutex mtx_; condition_variable not_full_; condition_variable not_empty_; public : explicit SPSC_Buffer (size_t capacity) : capacity_(capacity){ } void put (int item) { unique_lock<mutex> lk (mtx_) ; not_full_.wait (lk, [this ](){ return queue_.size ()<capacity_; }); queue_.push (item); not_empty_.notify_one (); } int get () { unique_lock<mutex> lk (mtx_) ; not_empty_.wait (lk, [this ](){ return !queue_.empty (); }); int ret = queue_.front (); queue_.pop (); not_full_.notify_one (); return ret; } }; int main () { SPSC_Buffer bf (5 ) ; thread producer ([&](){ for (int i=0 ;i<20 ;i++){ bf.put(i); cout << "Produced: " << i << endl; } }) ; thread Consumer ([&](){ for (int i=0 ;i<20 ;i++){ int ret = bf.get(); cout << "Consumer: " << i <<endl; } }) ; producer.join (); Consumer.join (); return 0 ; }
自旋锁实现
手写一个简单的自旋锁实现, 主要功能包括: - 提供加锁和解锁的接口 -
使用原子变量实现自旋锁的状态标志
1 2 3 4 5 6 7 8 9 10 11 12 13 class SpinLock { atomic_flag flag = ATOMIC_FLAG_INIT; public : void lock () { while (flag.test_and_set (memory_order_acquire)){ } } void unlock () { flag.clear (memory_order_release); } };
单例模式实现
手写一个线程安全的懒汉式单例模式实现: -
成员变量是一个静态指针 , 指向唯一实例 ;
还需要一个互斥锁 保护实例创建过程 -
构造函数私有化 , 防止外部实例化 - 确保类只有一个实例 -
提供一个全局访问点获取该实例
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 template <typename T>class Singleton {private : inline static T* instance = nullptr ; inline static std::mutex mtx; Singleton (){} = default ; ~Singleton (){} = default ; Singleton (const Singleton&) = delete ; Singleton& operator =(const Singleton&) = delete ; public : static T& getInstance () { if (instance==nullptr ){ std::unique_lock<std::mutex> lk (mtx) ; if (instance==nullptr ){ instance = new T (); } } return *instance; } void destroyInstance () { std::lock_guard<std::mutex> lk (mtx) ; if (instance!=nullptr ){ delete instance; instance = nullptr ; } } };
这里还实现了
双重检查锁定 (Double-Checked
Locking)来减少锁的开销, 只有在实例未创建时才加锁.双重检查锁定
(Double-Checked Locking Pattern, DCLP)
是一种用于延迟初始化单例对象的设计模式。它旨在
减少获取单例实例时的锁开销 ,同时
确保线程安全 。这种模式的核心思想是,在获取单例实例时,先进行一次非锁定的检查,如果实例已经存在,则直接返回实例;如果实例不存在,则进入锁定区域,再次检查实例是否存在,如果仍然不存在,则创建实例。
下面是一个C++11使用静态局部变量 实现的线程安全单例模式示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 template <typename T>class Singleton {private : Singleton (){} = default ; ~Singleton (){} = default ; Singleton (const Singleton&) = delete ; Singleton& operator =(const Singleton&) = delete ; public : static T& getInstance () { static T instance; return instance; } };
线程池实现
手写一个简单的线程池实现, 主要功能包括: - 初始化一定数量的工作线程 -
提供提交任务的接口 - 工作线程从任务队列中取出任务并执行 -
使用互斥锁和条件变量进行线程同步
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 class ThreadPool { vector<thread> workers; queue<function<void ()>> tasks; mutex mtx; condition_variable cv; atomic<bool > stop; public : ThreadPool (int nums): stop (false ){ for (int i=0 ;i<nums;i++){ workers.emplace_back ([this ]{ while (1 ){ function<void ()> task; { unique_lock<mutex> lock (mtx); cv.wait (lock, [this ](){ return !tasks.empty () || stop.load (); }); if (stop.load () && tasks.empty ()) return ; task = move (tasks.front ()); tasks.pop (); } task (); } }); } } void enqueue (function<void ()> task) { { unique_lock<mutex> lock (mtx) ; tasks.emplace (move (task)); } cv.notify_one (); } ~ThreadPool (){ stop.store (true ); cv.notify_all (); for (auto & worker: workers){ worker.join (); } } };
上面的线程池实现了基本的功能, 不过它只支持提交
function<void()> 类型的任务,
我们可以使用模板来实现一个更通用的
enqueue 方法,
支持
任意可调用对象和参数 :
1 2 3 4 5 6 7 8 9 template <class F, class ... Args>void enqueue (F&& f, Args&&... args) { auto task = std::bind (std::forward<F>(f), std::forward<Args>(args)...); { std::unique_lock<std::mutex> lock (mtx) ; tasks.emplace (std::move (task)); } cv.notify_one (); }
不过上面的实现还是无法获取任务的返回值, 我们可以使用
std::packaged_task 和 std::future
来实现支持返回值的任务提交: