目录
1.什么是单例模式
2.什么是设计模式
3.特点
4.饿汉和懒汉
5.峨汉实现单例
6.懒汉实现单例
7.懒汉实现单例(线程安全)
8.STL容器是否线程安全
9.智能指针是否线程安全
10.其他常见的锁
11.读者写者问题
1. 什么是单例模式
单例模式是一种经典的,常用的,常考的设计模式
2. 什么是设计模式
针对一些常用场景,给定了相应的解决方案,这个就是设计模式
3. 特点
一个类只能具有一个对象就是单例。在很多服务器开发中,经常要让服务器数据加载到上百G内存中,往往用一个单例的类管理这些数据
4. 饿汉和懒汉
吃完饭,立刻洗碗,就是峨汉方式,下一顿吃的时候就可以立刻拿着碗吃
吃完饭,先放下,下一顿吃的时候再洗碗,就是懒汉
懒汉的核心思想是“延时加载”,从而优化服务器的启动速度,因为加载时需要加载的东西少了,到实际使用时再加载,只是调整了花费时间的比例
5. 峨汉实现单例
template
class Singleton {
static T data;
public:
static T* GetInstance() {
return &data;
}
};
只要通过 Singleton 这个包装类来使用 T 对象, 则一个进程中只有一个 T 对象的实例
6. 懒汉实现单例
template
class Singleton {
static T* inst;
public:
static T* GetInstance() {
if (inst == NULL) {
inst = new T();
}
return inst;
}
};
存在一个严重的问题,线程不安全。第一次调用GetInstance时,如果两个线程同时调用,可能会创建两份T对象实例,但是后续再调用,就没有问题了
7. 懒汉实现单例(线程安全)
将前面的线程池修改为单例模式。将构造函数都私有,定义一个类指针,只需要一个变量所以用static,用一个static函数获取这个指针,如果为空就new一个。如果多线程并发访问有可能会生成多个对象,所以用一个静态的锁对判断加锁,不是每次都需要加锁,再套一层判断,只有为空时才加锁
// 懒汉模式, 线程安全
template
class Singleton {
volatile static T* inst; // 需要设置 volatile 关键字, 否则可能被编译器优化.
static std::mutex lock;
public:
static T* GetInstance() {
if (inst == NULL) { // 双重判定空指针, 降低锁冲突的概率, 提高性能.
lock.lock(); // 使用互斥锁, 保证多线程情况下也只调用一次 new.
if (inst == NULL) {
inst = new T();
}
lock.unlock();
}
return inst;
}
};
#pragma once
#include <vector>
#include <queue>
#include <pthread.h>
#include <string>
#include <unistd.h>
//换为封装的线程
struct ThreadInfo
{
pthread_t _tid;
std::string _name;
};
template <class T>
class pool
{
static const int defaultnum = 5;
public:
std::string getname(pthread_t tid)
{
for (auto ch : _thread)
{
if (ch._tid == tid)
{
return ch._name;
}
}
return "None";
}
static void* HandlerTask(void* args)
{
pool<T> *tp = static_cast<pool<T> *>(args);
std::string name = tp->getname(pthread_self());
while (true)
{
pthread_mutex_lock(&(tp->_mutex));
while (tp->_que.empty())
{
pthread_cond_wait(&(tp->_cond), &(tp->_mutex));
}
T t = tp->_que.front();
tp->_que.pop();
pthread_mutex_unlock(&tp->_mutex);
t.run();
printf("%s finsih task:%s\n", name.c_str(), t.getresult().c_str());
sleep(1);
}
}
void start()
{
for (int i = 0; i < _thread.size(); i++)
{
_thread[i]._name = "thread" + std::to_string(i);
pthread_create(&_thread[i]._tid, nullptr, HandlerTask, this);
}
}
void push(const T& x)
{
pthread_mutex_lock(&_mutex);
_que.push(x);
pthread_cond_signal(&_cond);
pthread_mutex_unlock(&_mutex);
}
static pool<T>* GetInstance()
{
//套一层判断,只有第一次需要上锁
if (_pl == nullptr)
{
pthread_mutex_lock(&_lock);
if (_pl == nullptr)
{
printf("first create\n");
_pl = new pool<T>;
}
pthread_mutex_unlock(&_lock);
}
return _pl;
}
private:
//构造私有化
pool(int num = defaultnum)
: _thread(num)
{
pthread_mutex_init(&_mutex, nullptr);
pthread_cond_init(&_cond, nullptr);
}
pool(const pool<T> &) = delete;
const pool<T> &operator=(const pool<T>&) = delete;
~pool()
{
pthread_mutex_destroy(&_mutex);
pthread_cond_destroy(&_cond);
}
std::vector<ThreadInfo> _thread;
std::queue<T> _que;
pthread_mutex_t _mutex;
pthread_cond_t _cond;
static pthread_mutex_t _lock;
static pool<T> *_pl;
};
//类外初始化
template <class T>
pool<T>* pool<T>::_pl = nullptr;
template <class T>
pthread_mutex_t pool<T>::_lock = PTHREAD_MUTEX_INITIALIZER;
使用
pool::GetInstance()->start();
注意事项:
1.加解锁的位置
2.双重if判断,避免不必要的锁竞争
3.volatile关键字防止过度优化
8. STL容器是否线程安全
不是
STL的设计初衷是将性能挖掘到极致,一旦涉及到加锁保证线程安全,会对性能产生巨大影响,而且对于不同的容器,枷锁方式的不同,性能可能也不同(例如hash表的锁表和锁桶)
因此STL默认不是线程安全的,如果需要再多线程的环境下使用,需要调用者自行保证线程安全
9. 智能指针是否线程安全
对于unique_ptr,由于只是当前代码范围内生效,所以不涉及线程安全的问题
对于shared_ptr,多个对象需要共用一个引用计数变量,所以会存在线程安全问题,但是标准库实现的时候考虑了这个问题,基于原子操作CAS)的方式保证shared_ptr能狗高效,原子的操作引用计数
10. 其他常见的锁
悲观锁:每次取数据时,总是担心数据会被其他线程修改,所以会在取数据前先加锁(读锁,解锁等),当其他线程想要访问数据时,被阻塞挂起
乐观锁:每次取数据时,总是乐观的认为数据不会被其他线程修改,所以不上锁,但是在更新数据前,会判断其他数据在更新前有没有对数据修改。主要采取两种方式:版本号机制和CAS操作
CAS操作:当需要更新数据时,判断当前内存值和之前取得的值是否相等。如果相等则用新值更新,若不相等则失败,失败则重试,一般是一个自旋的过程,即不断重试
自旋锁,公平锁,非公平锁
当申请一种资源失败后,线程就会挂起,如果是非阻塞加锁就会返回。如果临界区的访问特别快,就没有必要挂起线程,可以不断尝试申请资源,这种就是自旋锁。用自旋锁还是挂起锁取决于临界区执行时长
自旋锁相关函数,和其他锁类似
11. 读者写者问题
在编写多线程的时候,有一种情况十分常见。有些公共数据修改的机会比较少,相较于写,读的机会反而高的多。通常而言,读的过程伴随着查找的操作,消耗的时间很长,给这段代码枷锁,会极大的降低效率,有没有处理这种多读少写的情况?就是读写锁(长时间等人和短时间等人的例子)
比如公告这种,写的人很少,读的人很多。可以多个读者同时访问公共资源,原因就是不会取走数据
321原则:
3种关系:读读(共享),读写(互斥,同步),写写(互斥竞争)
2种角色:读者,写者
1个交易场所:数据交换的地点
两种策略:
读写情况,读者访问的几率大的情况是正常现象
读者优先:当读者和写者要同时访问共享资源,所有读者访问完写者再访问
写者优先:当同时访问时,等待内部的写者写完,写者先进去,然后读者再进来
读写锁行为
当前锁状态 | 读锁请求 | 写锁请求 |
---|---|---|
无锁 | 可以 | 可以 |
读锁 | 可以 | 阻塞 |
写锁 | 阻塞 | 阻塞 |
写独占,读共享,写锁优先级高
相关函数
设置优先
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_READER_NP 一致
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 <vector>
#include <sstream>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <unistd.h>
#include <pthread.h>
volatile int ticket = 1000;
pthread_rwlock_t rwlock;
void * reader(void * arg)
{
char *id = (char *)arg;
while (1) {
pthread_rwlock_rdlock(&rwlock);
if (ticket <= 0) {
pthread_rwlock_unlock(&rwlock);
break;
}
printf("%s: %d\n", id, ticket);
pthread_rwlock_unlock(&rwlock);
usleep(1);
}
return nullptr;
}
void * writer(void * arg)
{
char *id = (char *)arg;
while (1) {
pthread_rwlock_wrlock(&rwlock);
if (ticket <= 0) {
pthread_rwlock_unlock(&rwlock);
break;
}
printf("%s: %d\n", id, --ticket);
pthread_rwlock_unlock(&rwlock);
usleep(1);
}
return nullptr;
}
struct ThreadAttr
{
pthread_t tid;
std::string id;
};
std::string create_reader_id(std::size_t i)
{
// 利用 ostringstream 进行 string 拼接
std::ostringstream oss("thread reader ", std::ios_base::ate);
oss << i;
return oss.str();
}
std::string create_writer_id(std::size_t i)
{
// 利用 ostringstream 进行 string 拼接
std::ostringstream oss("thread writer ", std::ios_base::ate);
oss << i;
return oss.str();
}
void init_readers(std::vector<ThreadAttr>& vec)
{
for (std::size_t i = 0; i < vec.size(); ++i) {
vec[i].id = create_reader_id(i);
pthread_create(&vec[i].tid, nullptr, reader, (void *)vec[i].id.c_str());
}
}
void init_writers(std::vector<ThreadAttr>& vec)
{
for (std::size_t i = 0; i < vec.size(); ++i) {
vec[i].id = create_writer_id(i);
pthread_create(&vec[i].tid, nullptr, writer, (void *)vec[i].id.c_str());
}
}
void join_threads(std::vector<ThreadAttr> const& vec)
{
// 我们按创建的 逆序 来进行线程的回收
for (std::vector<ThreadAttr>::const_reverse_iterator it = vec.rbegin(); it !=
vec.rend(); ++it) {
pthread_t const& tid = it->tid;
pthread_join(tid, nullptr);
}
}
void init_rwlock()
{
#if 0 // 写优先
pthread_rwlockattr_t attr;
pthread_rwlockattr_init(&attr);
pthread_rwlockattr_setkind_np(&attr, PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP);
pthread_rwlock_init(&rwlock, &attr);
pthread_rwlockattr_destroy(&attr);
#else // 读优先,会造成写饥饿
pthread_rwlock_init(&rwlock, nullptr);
#endif
}
int main()
{
// 测试效果不明显的情况下,可以加大 reader_nr
// 但也不能太大,超过一定阈值后系统就调度不了主线程了
const std::size_t reader_nr = 1000;
const std::size_t writer_nr = 2;
std::vector<ThreadAttr> readers(reader_nr);
std::vector<ThreadAttr> writers(writer_nr);
init_rwlock();
init_readers(readers);
init_writers(writers);
join_threads(writers);
join_threads(readers);
pthread_rwlock_destroy(&rwlock);
}
只能看到写饥饿