logofree制作网站,自己做网站需要哪些软件,系统开发毕业设计,报社网站建设之思考文章目录 一、线程池1. 线程池模型和应用场景2. 单例模式实现线程池(懒汉模式) 二、其他常见的锁1. STL、智能指针和线程安全2. 其他常见的锁 三、读者写者问题1. 读者写者模型2. 读写锁 一、线程池
1. 线程池模型和应用场景
线程池是一种线程使用模式。线程过多会带来调度开… 文章目录 一、线程池1. 线程池模型和应用场景2. 单例模式实现线程池(懒汉模式) 二、其他常见的锁1. STL、智能指针和线程安全2. 其他常见的锁 三、读者写者问题1. 读者写者模型2. 读写锁 一、线程池
1. 线程池模型和应用场景
线程池是一种线程使用模式。线程过多会带来调度开销进而影响缓存局部性和整体性能。而线程池维护着多个线程等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用还能防止过分调度。可用线程数量应该取决于可用的并发处理器、处理器内核、内存、网络sockets等的数量。 线程池模型
线程池模型本质上也是生产者消费者模型线程池的实现原理是在线程池中预先准备好并创建一批线程然后上层将任务push到任务队列中休眠的线程如果检测到任务队列中有任务就直接被操作系统唤醒然后去消费并处理任务唤醒一个线程的代价比创建一个线程的代价小的很多。 任务线程指的是生产者任务队列指的是交易场所右边的一大批线程指的是消费者因此。线程池的本质还是生产消费模型。 线程池的应用场景
需要大量的线程来完成任务且完成任务的时间比较短。 WEB服务器完成网页请求这样的任务使用线程池技术是非常合适的。因为单个任务小而任务数量巨大你可以想象一个热门网站的点击次数。 但对于长时间的任务比如一个Telnet连接请求线程池的优点就不明显了。因为Telnet会话时间比线程的创建时间大多了。对性能要求苛刻的应用比如要求服务器迅速响应客户请求。接受突发性的大量请求但不至于使服务器因此产生大量线程的应用。突发性大量客户请求在没有线程池情况下将产生大量线程虽然理论上大部分操作系统线程数目最大值不是问题短时间内产生大量线程可能使内存到达极限出现错误。 2. 单例模式实现线程池(懒汉模式) ThreadPool.hpp
#pragma once
#include iostream
#include string
#include vector
#include queue
#include unistd.h
#include Thread.hpp
#include Task.hpp
#include lockGuard.hpp
using namespace std;const static int N 5;// 将此代码设计成单例模式————懒汉模式template class T
class ThreadPool
{
private:ThreadPool(int num N) : _num(num){pthread_mutex_init(_lock, nullptr);pthread_cond_init(_cond, nullptr);}ThreadPool(const ThreadPoolT tp) delete;void operator(const ThreadPoolT tp) delete;
public:// 设计一个静态成员函数来返回创建的对象static ThreadPoolT* getinstance(){if(_instance nullptr){LockGuard lockguard(_instance_lock);{if(_instance nullptr){_instance new ThreadPoolT();_instance-init();_instance-start();}}}return _instance;}pthread_mutex_t *getlock(){return _lock;}void threadWait(){pthread_cond_wait(_cond, _lock);}void threadWake(){pthread_cond_signal(_cond);}bool isEmpty(){return _tasks.empty();}void init(){for (int i 0; i _num; i){_threads.push_back(Thread(i 1, threadRoutine, this));}}void start(){for (auto t : _threads){t.run();}}void check(){for (auto t : _threads)cout t.threadname() running... endl;}static void threadRoutine(void *args){ThreadPoolT *tp static_castThreadPoolT *(args);while (true){T t;// 检测此时有没有任务, 如果有任务就处理任务, 否则就挂起等待{LockGuard lockguard(tp-getlock());while (tp-isEmpty()){tp-threadWait();}t tp-popTask();}t();cout thread handler done, result: t.formatRes() endl;}}T popTask(){T t _tasks.front();_tasks.pop();return t;}void pushTask(const T t){LockGuard lockguard(_lock);_tasks.push(t);threadWake();}~ThreadPool(){for (auto t : _threads){t.join();}pthread_mutex_destroy(_lock);pthread_cond_destroy(_cond);}private:vectorThread _threads;int _num;queueT _tasks; // 使用stl的自动扩容机制pthread_mutex_t _lock;pthread_cond_t _cond;static ThreadPoolT* _instance;static pthread_mutex_t _instance_lock;
};templateclass T
ThreadPoolT* ThreadPoolT::_instance nullptr;templateclass T
pthread_mutex_t ThreadPoolT::_instance_lock PTHREAD_MUTEX_INITIALIZER;Thread.hpp
#pragma once#include iostream
#include cstdlib
#include string
#include pthread.h
using namespace std;class Thread
{
public:typedef enum{NEW 0,RUNNING,EXITED} ThreadStatus;typedef void (*func_t)(void*);public:Thread(int num, func_t func, void* args) :_tid(0), _status(NEW),_func(func),_args(args){char name[128];snprintf(name, 128, thread-%d, num);_name name;}int status(){ return _status; }string threadname(){ return _name; }pthread_t get_id(){if(_status RUNNING)return _tid;elsereturn 0;}static void* thread_run(void* args){Thread* ti static_castThread*(args);(*ti)();return nullptr;}void operator()(){if(_func ! nullptr)_func(_args);}void run() // 封装线程运行{int n pthread_create(_tid, nullptr, thread_run, this);if(n ! 0)exit(-1);_status RUNNING; // 线程状态变为运行}void join() // 疯转线程等待{int n pthread_join(_tid, nullptr);if(n ! 0){cout main thread join thread: _name error endl;return;}_status EXITED;}~Thread(){}
private:pthread_t _tid;string _name;func_t _func; // 线程未来要执行的回调void* _args;ThreadStatus _status;
};Task.hpp
#pragma once
#include iostream
#include string
using namespace std;class Task
{
public:Task(){}Task(int x, int y, char op):_x(x), _y(y), _op(op), _result(0), _exitcode(0){}void operator()(){switch (_op){case :_result _x _y; break;case -:_result _x - _y;break;case *:_result _x * _y;break;case /:{if(_y 0)_exitcode -1;else _result _x / _y;}break;case %:{if(_y 0)_exitcode -1;else _result _x % _y;}break;default:break;}}string formatArge(){return to_string(_x) _op to_string(_y) ;}string formatRes(){return to_string(_result) ( to_string(_exitcode) );}~Task(){}private:int _x;int _y;char _op;int _result;int _exitcode;
};lockGuard.hpp
#pragma once#include iostream
#include pthread.husing namespace std;class Mutex // 自己不维护锁有外部传入
{
public:Mutex(pthread_mutex_t *mutex):_pmutex(mutex){}void lock(){pthread_mutex_lock(_pmutex);}void unlock(){pthread_mutex_unlock(_pmutex);}~Mutex(){}
private:pthread_mutex_t *_pmutex;
};class LockGuard // 自己不维护锁有外部传入
{
public:LockGuard(pthread_mutex_t *mutex):_mutex(mutex){_mutex.lock();}~LockGuard(){_mutex.unlock();}
private:Mutex _mutex;
};main.cc
#include ThreadPool_V4.hpp
#include Task.hpp
#include memoryconst string ops -*/%;int main()
{srand(time(nullptr) ^ getpid());while(true){sleep(1);int x rand() % 100;int y rand() % 100;char op ops[(x y) % ops.size()];Task t(x, y, op);ThreadPoolTask::getinstance()-pushTask(t);// tp-pushTask(t);cout the question is what: t.formatArge() ? endl;}return 0;
}二、其他常见的锁
1. STL、智能指针和线程安全 STL中的容器是否是线程安全的?
不是原因是, STL 的设计初衷是将性能挖掘到极致, 而一旦涉及到加锁保证线程安全, 会对性能造成巨大的影响而且对于不同的容器, 加锁方式的不同, 性能可能也不同(例如hash表的锁表和锁桶). 因此 STL 默认不是线程安全. 如果需要在多线程环境下使用, 往往需要调用者自行保证线程安全。 智能指针是线程安全的吗?
智能指针是线程安全的吗unique_ptr 是和资源强关联只是在当前代码块范围内生效因此不涉及线程安全问题。对于 shared_ptr多个对象需要共有一个引用计数变量所以会存在线程安全问题。但是标准库实现的时候也考虑到了这个问题就基于原子操作Compare And Swap(CAS) 的方式保证 shared_ptr 能够高效原子地操作引用计数。shared_ptr 是线程安全的但不意味着对其管理的资源进行操作是线程安全的所以对 shared_ptr 管理的资源进行操作时也可能需要进行加锁保护。 2. 其他常见的锁 悲观锁悲观锁做事比较悲观它认为多线程同时修改共享资源的概率比较高于是很容易出现冲突所以访问贡献资源前先要进行加锁保护。常见的悲观锁有互斥锁、自旋锁和读写锁等。乐观锁乐观锁做事比较乐观它乐观地认为共享数据不会被其他线程修改因此不上锁。它的工作方式是先修改完共享数据再判断这段时间内有没有发生冲突。如果其他线程没有修改共享数据那么则操作成功。如果发现其他线程已经修改该共享数据就放弃本次操作。乐观锁全程并没有加锁所以它也叫无锁编程。乐观锁主要采取两种方式版本号机制Gitee等和 CAS 操作。乐观锁虽然去除了加锁和解锁的操作但是一旦发生冲突重试的成本是很高的所以只有在冲突概率非常低且加锁成本非常高的场景下才考虑使用乐观锁。CAS 操作当需要更新数据时判断当前内存值和之前取得的值是否相等。如果相等则用新值更新。若不等则失败失败则重试一般是一个自旋的过程即不断重试。自旋锁使用自旋锁的时候当多线程发生竞争锁的情况时加锁失败的线程会忙等待这里的忙等待可以用 while 循环等待实现直到它拿到锁。而互斥锁加锁失败后线程会让出 CPU 资源给其他线程使用然后该线程会被阻塞挂起。如果临界区代码执行时间过长自旋的线程会长时间占用 CPU 资源所以自旋的时间和临界区代码执行的时间是成正比的关系。如果临界区代码执行的时间很短就不应该使用互斥锁而应该选用自旋锁。因为互斥锁加锁失败是需要发生上下文切换的如果临界区执行的时间比较短那可能上下文切换的时间会比临界区代码执行的时间还要长。 三、读者写者问题
1. 读者写者模型
在编写多线程的时候有一种情况是十分常见的。那就是有些公共数据修改的机会比较少。相比较改写它们读的机会反而高的多。通常而言在读的过程中往往伴随着查找的操作中间耗时很长。给这种代码段加锁会极大地降低我们程序的效率。那么有没有一种方法可以专门处理这种多读少写的情况呢
这就需要我们的读者写者模型出场了读者写者模型其实也是维护321原则三种关系读者与读者、读者与写者、写者与写者。两种对象读者和写者。一个交易场所需要写入和从中读取的缓冲区。
下面我们来看一下读者写者模型的三种关系 读者与读者没有关系读者与写者互斥与同步写者与写者互斥 那么为什么在生产者消费者模型中消费者和消费者是互斥关系而在读者写者问题中读者和读者之间没有关系呢
读者写者模型和生产者消费者模型的最大区别就是消费者会将数据拿走而读者不会拿走数据读者仅仅是对数据做读取并不会进行任何修改的操作因此共享资源也不会因为有多个读者来读取而导致数据不一致的问题。 2. 读写锁
在读者写者模型中pthread库为我们提供了 读写锁 来维护其中的同步与互斥关系。读写锁由读锁和写锁两部分构成如果只读取共享资源用读锁加锁如果要修改共享资源则用写锁加锁。所以读写锁适用于能明确区分读操作和写操作的场景。
读写锁的工作原理
当写锁没有被写线程持有时多个读线程能够并发地持有读锁这大大提高了共享资源的访问效率。因为读锁是用于读取共享资源的场景所以多个线程同时持有读锁也不会破坏共享资源的数据。但是一旦写锁被写进程持有后读线程获取读锁的操作会被阻塞而其它写线程的获取写锁的操作也会被阻塞。
伪代码
// 写者进程/线程执行的函数
void Writer()
{while(true){P(wCountMutex); // 进入临界区if(wCount 0)P(rMutex); // 当第一个写者进入如果有读者则阻塞读者wCount;// 写者计数 1V(wCountMutex); // 离开临界区P(wDataMutex); // 写者写操作之间互斥进入临界区write(); // 写数据V(wDataMutex); // 离开临界区P(wCountMutex); // 进入临界区wCount--; // 写完数据准备离开if(wCount 0){V(rMutex); // 最后一个写者离开了则唤醒读者}V(wCountMutex); //离开临界区}
}// 读者进程/线程执行的次数
void reader()
{while(TRUE){P(rMutex);P(rCountMutex); // 进入临界区if ( rCount 0 )P(wDataMutex); // 当第一个读者进入如果有写者则阻塞写者写操作rCount;V(rCountMutex); // 离开临界区V(rMutex);read( ); // 读数据P(rCountMutex); // 进入临界区rCount--;if ( rCount 0 )V(wDataMutex); // 当没有读者了则唤醒阻塞中写者的写操作V(rCountMutex); // 离开临界区}
}初始化
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);读者写者问题很明显会存在读者优先还是写者优先的问题如果是读者优先的话可能就会带来写者饥饿的问题。而写者优先可以保证写线程不会饿死但如果一直有写线程获取写锁那么读者也会被饿死。所以使用读写锁时需要考虑应用场景。读写锁通常用于数据被读取的频率非常高而被修改的频率非常低。注Linux 下的读写锁默认是读者优先的。