目录
1.线程池
1.1 线程池:
1.2 线程池的应用场景:
1.3 线程池的种类:
1.4 线程池示例:
1.5 线程池编程模拟实现:
2. 线程安全的单例模式
2.1 什么是单例模式
2.2 什么是设计模式
2.3 单例模式的特点
2.3.1 饿汉实现方式和懒汉实现方式
2.3.2 饿汉方式实现单例模式
2.3.3 懒汉方式实现单例模式
2.3.4 懒汉方式实现单例模式(线程安全版本)
3.STL、智能指针、线程安全
3.1 STL中的容器是否是线程安全的?
3.2 智能指针是否是线程安全的?
4. 其他常见的各种锁
5. 读者写者问题
5.1 读写锁
5.2 读写锁接口
5.3 编程模拟实现读写锁案例:
后记:●由于作者水平有限,文章难免存在谬误之处,敬请读者斧正,俚语成篇,恳望指教!
——By 作者:新晓·故知
/* 1.1 线程池:
* 一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。可用线程数量应该取决于可用的并发处理器、处理器内核、内存、网络sockets等的数量。
* 1.2 线程池的应用场景:
* 1. 需要大量的线程来完成任务,且完成任务的时间比较短。 WEB服务器完成网页请求这样的任务,使用线程池技术是非常合适的。因为单个任务小,而任务数量巨大,你可以想象一个热门网站的点击次数。 但对于长时间的任务,比如一个Telnet连接请求,线程池的优点就不明显了。因为Telnet会话时间比线程的创建时间大多了。
* 2. 对性能要求苛刻的应用,比如要求服务器迅速响应客户请求。
* 3. 接受突发性的大量请求,但不至于使服务器因此产生大量线程的应用。突发性大量客户请求,在没有线程池情况下,将产生大量线程,虽然理论上大部分操作系统线程数目最大值不是问题,短时间内产生大量线程可能使内存到达极限,出现错误.
* 1.3 线程池的种类:
* 1.4 线程池示例:
* 1. 创建固定数量线程池,循环从任务队列中获取任务对象,
* 2. 获取到任务对象后,执行任务对象中的任务接口
*/
1.5 线程池编程模拟实现:
version1:
shell脚本:
ps -aL | grep -E 'master|follower'
ThreadPool.hpp:
#pragma once #include
#include #include #include #include #include #include #include #include "Log.hpp" #include "Lock.hpp" using namespace std; int gThreadNum = 5; template class ThreadPool { private: ThreadPool(int threadNum = gThreadNum) :threadNum_(threadNum) ,isStart_(false) { assert(threadNum_ > 0); pthread_mutex_init(&mutex_, nullptr); pthread_cond_init(&cond_, nullptr); } ThreadPool(const ThreadPool &) = delete; void operator=(const ThreadPool &) = delete; public: static ThreadPool *getInstance() { static Mutex mutex; if (nullptr == instance) //仅仅是过滤重复的判断 { LockGuard lockguard(&mutex); //进入代码块,加锁。退出代码块,自动解锁 if (nullptr == instance) { instance = new ThreadPool (); } } return instance; } //类内成员, 成员函数,都有默认参数this //static函数无法访问类内成员,只能通过接口,这里通过传入的this指针 static void *threadRoutine(void *args) { pthread_detach(pthread_self()); ThreadPool *tp = static_cast *>(args); prctl(PR_SET_NAME, "follower"); while (1) { tp->lockQueue(); while (!tp->haveTask()) { tp->waitForTask(); } //这个任务就被拿到了线程的上下文中 T t = tp->pop(); tp->unlockQueue(); // for debug int one, two; char oper; t.get(&one, &two, &oper); //规定,所有的任务都必须有一个run方法 Log() << "新线程完成计算任务: " << one << oper << two << "=" << t.run() << "\n"; } } void start() { assert(!isStart_); for (int i = 0; i < threadNum_; i++) { pthread_t temp; pthread_create(&temp, nullptr, threadRoutine, this); } isStart_ = true; } void push(const T &in) { lockQueue(); taskQueue_.push(in); choiceThreadForHandler(); unlockQueue(); } ~ThreadPool() { pthread_mutex_destroy(&mutex_); pthread_cond_destroy(&cond_); } private: void lockQueue() { pthread_mutex_lock(&mutex_); } void unlockQueue() { pthread_mutex_unlock(&mutex_); } bool haveTask() { return !taskQueue_.empty(); } void waitForTask() { pthread_cond_wait(&cond_, &mutex_); } void choiceThreadForHandler() { pthread_cond_signal(&cond_); } T pop() { T temp = taskQueue_.front(); taskQueue_.pop(); return temp; } private: bool isStart_; int threadNum_; queue taskQueue_; pthread_mutex_t mutex_; pthread_cond_t cond_; static ThreadPool *instance; // const static int a = 100; }; template ThreadPool *ThreadPool ::instance = nullptr; ThreadPoolTest.cc:
#include "ThreadPool.hpp" #include "Task.hpp" #include
#include // 如何对一个线程进行封装, 线程需要一个回调函数(支持lambda)自行实现 // class tread{ // }; int main() { prctl(PR_SET_NAME, "master"); const string operators = "+/*/%"; // unique_ptr > tp(new ThreadPool ()); unique_ptr > tp(ThreadPool ::getInstance()); tp->start(); srand((unsigned long)time(nullptr) ^ getpid() ^ pthread_self()); // 派发任务的线程 while(true) { int one = rand()%50; int two = rand()%10; char oper = operators[rand()%operators.size()]; Log() << "主线程派发计算任务: " << one << oper << two << "=?" << "\n"; Task t(one, two, oper); tp->push(t); sleep(1); } } Lock.hpp:
#pragma once #include
#include class Mutex { public: Mutex() { pthread_mutex_init(&lock_, nullptr); } void lock() { pthread_mutex_lock(&lock_); } void unlock() { pthread_mutex_unlock(&lock_); } ~Mutex() { pthread_mutex_destroy(&lock_); } private: pthread_mutex_t lock_; }; class LockGuard { public: LockGuard(Mutex *mutex) : mutex_(mutex) { mutex_->lock(); std::cout << "加锁成功..." << std::endl; } ~LockGuard() { mutex_->unlock(); std::cout << "解锁成功...." << std::endl; } private: Mutex *mutex_; }; Task.hpp:
#pragma once #include
#include class Task { public: Task() : elemOne_(0), elemTwo_(0), operator_('0') { } Task(int one, int two, char op) : elemOne_(one), elemTwo_(two), operator_(op) { } int operator() () { return run(); } int run() { int result = 0; switch (operator_) { case '+': result = elemOne_ + elemTwo_; break; case '-': result = elemOne_ - elemTwo_; break; case '*': result = elemOne_ * elemTwo_; break; case '/': { if (elemTwo_ == 0) { std::cout << "div zero, abort" << std::endl; result = -1; } else { result = elemOne_ / elemTwo_; } } break; case '%': { if (elemTwo_ == 0) { std::cout << "mod zero, abort" << std::endl; result = -1; } else { result = elemOne_ % elemTwo_; } } break; default: std::cout << "非法操作: " << operator_ << std::endl; break; } return result; } int get(int *e1, int *e2, char *op) { *e1 = elemOne_; *e2 = elemTwo_; *op = operator_; } private: int elemOne_; int elemTwo_; char operator_; }; Log.hpp:
#pragma once #include
#include #include std::ostream &Log() { std::cout << "Fot Debug |" << " timestamp: " << (uint64_t)time(nullptr) << " | " << " Thread[" << pthread_self() << "] | "; return std::cout; } makefile:
CC=g++ FLAGS=-std=c++11 LD=-lpthread bin=threadpool src=ThreadPoolTest.cc $(bin):$(src) $(CC) -o $@ $^ $(LD) $(FLAGS) .PHONY:clean clean: rm -f $(bin)
version 2:
g++ threadpool.cpp -o threadpool -pthread -lrt -std=c++0x
/*threadpool.h*/ /* 线程池: * 一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。可用线程数量应该取决于可用的并发处理器、处理器内核、内存、网络sockets等的数量。 * 线程池的应用场景: * 1. 需要大量的线程来完成任务,且完成任务的时间比较短。 WEB服务器完成网页请求这样的任务,使用线程池技术是非常合适的。因为单个任务小,而任务数量巨大,你可以想象一个热门网站的点击次数。 但对于长时间的任务,比如一个Telnet连接请求,线程池的优点就不明显了。因为Telnet会话时间比线程的创建时间大多了。 * 2. 对性能要求苛刻的应用,比如要求服务器迅速响应客户请求。 * 3. 接受突发性的大量请求,但不至于使服务器因此产生大量线程的应用。突发性大量客户请求,在没有线程池情况下,将产生大量线程,虽然理论上大部分操作系统线程数目最大值不是问题,短时间内产生大量线程可能使内存到达极限,出现错误. * 线程池的种类: * 线程池示例: * 1. 创建固定数量线程池,循环从任务队列中获取任务对象, * 2. 获取到任务对象后,执行任务对象中的任务接口 */ /*threadpool.hpp*/ #ifndef __M_TP_H__ #define __M_TP_H__ #include
#include #include #include #define MAX_THREAD 5 typedef bool (*handler_t)(int); class ThreadTask { private: int _data; handler_t _handler; public: ThreadTask() : _data(-1), _handler(NULL) {} ThreadTask(int data, handler_t handler) { _data = data; _handler = handler; } void SetTask(int data, handler_t handler) { _data = data; _handler = handler; } void Run() { _handler(_data); } }; class ThreadPool { private: int _thread_max; int _thread_cur; bool _tp_quit; std::queue _task_queue; pthread_mutex_t _lock; pthread_cond_t _cond; private: void LockQueue() { pthread_mutex_lock(&_lock); } void UnLockQueue() { pthread_mutex_unlock(&_lock); } void WakeUpOne() { pthread_cond_signal(&_cond); } void WakeUpAll() { pthread_cond_broadcast(&_cond); } void ThreadQuit() { _thread_cur--; UnLockQueue(); pthread_exit(NULL); } void ThreadWait() { if(_tp_quit) { ThreadQuit(); } pthread_cond_wait(&_cond, &_lock); } bool IsEmpty() { return _task_queue.empty(); } static void *thr_start(void *arg) { ThreadPool *tp = (ThreadPool *)arg; while (1) { tp->LockQueue(); while (tp->IsEmpty()) { tp->ThreadWait(); } ThreadTask *tt; tp->PopTask(&tt); tp->UnLockQueue(); tt->Run(); delete tt; } return NULL; } public: ThreadPool(int max = MAX_THREAD) : _thread_max(max), _thread_cur(max), _tp_quit(false) { pthread_mutex_init(&_lock, NULL); pthread_cond_init(&_cond, NULL); } ~ThreadPool() { pthread_mutex_destroy(&_lock); pthread_cond_destroy(&_cond); } bool PoolInit() { pthread_t tid; for (int i = 0; i < _thread_max; i++) { int ret = pthread_create(&tid, NULL, thr_start, this); if (ret != 0) { std::cout << "create pool thread error\n"; return false; } } return true; } bool PushTask(ThreadTask *tt) { LockQueue(); if (_tp_quit) { UnLockQueue(); return false; } _task_queue.push(tt); WakeUpOne(); UnLockQueue(); return true; } bool PopTask(ThreadTask **tt) { *tt = _task_queue.front(); _task_queue.pop(); return true; } bool PoolQuit() { LockQueue(); _tp_quit = true; UnLockQueue(); while (_thread_cur > 0) { WakeUpAll(); usleep(1000); } return true; } }; #endif /*main.cpp*/ bool handler(int data) { srand(time(NULL)); int n = rand() % 5; printf("Thread: %p Run Tast: %d--sleep %d sec\n", pthread_self(), data, n); sleep(n); return true; } int main() { int i; ThreadPool pool; pool.PoolInit(); for (i = 0; i < 10; i++) { ThreadTask *tt = new ThreadTask(i, handler); pool.PushTask(tt); } pool.PoolQuit(); return 0; }
2.1 什么是单例模式
单例模式是一种 "经典的, 常用的, 常考的" 设计模式.2.2 什么是设计模式
IT行业这么火, 涌入的人很多. 俗话说林子大了啥鸟都有. 大佬和菜鸡们两极分化的越来越严重. 为了让菜鸡们不太拖大佬的后腿, 于是大佬们针对一些经典的常见的场景, 给定了一些对应的解决方案, 这个就是 设计模式2.3 单例模式的特点
某些类, 只应该具有一个对象(实例), 就称之为单例.例如一个男人只能有一个媳妇.在很多服务器开发场景中, 经常需要让服务器加载很多的数据 (上百G) 到内存中. 此时往往要用一个单例的类来管理这些数据.2.3.1 饿汉实现方式和懒汉实现方式
[洗完的例子]吃完饭, 立刻洗碗, 这种就是饿汉方式. 因为下一顿吃的时候可以立刻拿着碗就能吃饭.吃完饭, 先把碗放下, 然后下一顿饭用到这个碗了再洗碗, 就是懒汉方式.懒汉方式最核心的思想是 "延时加载". 从而能够优化服务器的启动速度.2.3.2 饿汉方式实现单例模式
template
class Singleton { static T data; public: static T *GetInstance() { return &data; } }; 只要通过 Singleton 这个包装类来使用 T 对象, 则一个进程中只有一个 T 对象的实例.2.3.3 懒汉方式实现单例模式
template
class Singleton { static T *inst; public: static T *GetInstance() { if (inst == NULL) { inst = new T(); } return inst; } }; 存在一个严重的问题, 线程不安全.第一次调用 GetInstance 的时候, 如果两个线程同时调用, 可能会创建出两份 T 对象的实例.但是后续再次调用, 就没有问题了.2.3.4 懒汉方式实现单例模式(线程安全版本)
注意事项:1. 加锁解锁的位置2. 双重 if 判定, 避免不必要的锁竞争3. volatile关键字防止过度优化3.STL、智能指针、线程安全
3.1 STL中的容器是否是线程安全的?
不是.原因是, STL 的设计初衷是将性能挖掘到极致, 而一旦涉及到加锁保证线程安全, 会对性能造成巨大的影响.而且对于不同的容器, 加锁方式的不同, 性能可能也不同(例如hash表的锁表和锁桶).因此 STL 默认不是线程安全. 如果需要在多线程环境下使用, 往往需要调用者自行保证线程安全.3.2 智能指针是否是线程安全的?
对于 unique_ptr, 由于只是在当前代码块范围内生效, 因此不涉及线程安全问题.对于 shared_ptr, 多个对象需要共用一个引用计数变量, 所以会存在线程安全问题. 但是标准库实现的时候考虑到了这个问题, 基于原子操作(CAS)的方式保证 shared_ptr 能够高效, 原子的操作引用计数.4. 其他常见的各种锁
- 悲观锁:在每次取数据时,总是担心数据会被其他线程修改,所以会在取数据前先加锁(读锁,写锁,行锁等),当其他线程想要访问数据时,被阻塞挂起。
- 乐观锁:每次取数据时候,总是乐观的认为数据不会被其他线程修改,因此不上锁。但是在更新数据前,会判断其他数据在更新前有没有对数据进行修改。主要采用两种方式:版本号机制和CAS操作。(乐观锁是由程序员实现的)
- CAS操作:当需要更新数据时,判断当前内存值和之前取得的值是否相等。如果相等则用新值更新。若不等则失败,失败则重试,一般是一个自旋的过程,即不断重试。
- 自旋锁,公平锁,非公平锁?
5. 读者写者问题
5.1 读写锁
在编写多线程的时候,有一种情况是十分常见的。那就是,有些公共数据修改的机会比较少。相比较改写,它们读的机会反而高的多。通常而言,在读的过程中,往往伴随着查找的操作,中间耗时很长。给这种代码段加锁,会极大地降低我们程序的效率。那么有没有一种方法,可以专门处理这种多读少写的情况呢? 有,那就是读写锁。注意:写独占,读共享,读锁优先级高5.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_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);
5.3 编程模拟实现读写锁案例:
version 1:
#include
#include #include using namespace std; int board = 0; pthread_rwlock_t rw; void *reader(void* args) { const char *name = static_cast (args); cout << "run..." << endl; while(true) { pthread_rwlock_rdlock(&rw); cout << "reader read : " << board << "tid: " << pthread_self() << endl; sleep(1); pthread_rwlock_unlock(&rw); } } void *writer(void *args) { const char *name = static_cast (args); sleep(1); while(true) { pthread_rwlock_wrlock(&rw); board++; cout << "I am writer" << endl; sleep(1); pthread_rwlock_unlock(&rw); } } int main() { pthread_rwlock_init(&rw, nullptr); pthread_t r1,r2,r3,r4,r5,r6, w; pthread_create(&r1, nullptr, reader, (void*)"reader"); pthread_create(&r2, nullptr, reader, (void*)"reader"); pthread_create(&r3, nullptr, reader, (void*)"reader"); pthread_create(&r4, nullptr, reader, (void*)"reader"); pthread_create(&r5, nullptr, reader, (void*)"reader"); pthread_create(&r6, nullptr, reader, (void*)"reader"); pthread_create(&w, nullptr, writer, (void*)"writer"); pthread_join(r1, nullptr); pthread_join(r2, nullptr); pthread_join(r3, nullptr); pthread_join(r4, nullptr); pthread_join(r5, nullptr); pthread_join(r6, nullptr); pthread_join(w, nullptr); pthread_rwlock_destroy(&rw); return 0; }
version 2:
#include
#include #include #include #include #include #include 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 &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 &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 const &vec) { // 我们按创建的 逆序 来进行线程的回收 for (std::vector ::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 readers(reader_nr); std::vector writers(writer_nr); init_rwlock(); init_readers(readers); init_writers(writers); join_threads(writers); join_threads(readers); pthread_rwlock_destroy(&rwlock); } g++ -std=c++11 readwrite.cc -o readwrite -lpthread