当前位置: 首页 > news >正文

苏宁易购电商网站建设需求分析wordpress怎么弄登录

苏宁易购电商网站建设需求分析,wordpress怎么弄登录,html网站尺寸,高端网站设计哪个好文章目录 1. 线程互斥1.1 问题引入1.2 线程互斥的相关概念1.3 互斥量mutex1.4 互斥量实现原理1.5 死锁 2. 线程安全和可重入函数3. 线程同步3.1 同步概念3.2 条件变量 4. 生产消费模型4.1 基于阻塞队列的cp模型4.2 基于环形队列的cp模型POSIX信号量 5. 线程池5.1 互斥量RAII版本… 文章目录 1. 线程互斥1.1 问题引入1.2 线程互斥的相关概念1.3 互斥量mutex1.4 互斥量实现原理1.5 死锁 2. 线程安全和可重入函数3. 线程同步3.1 同步概念3.2 条件变量 4. 生产消费模型4.1 基于阻塞队列的cp模型4.2 基于环形队列的cp模型POSIX信号量 5. 线程池5.1 互斥量RAII版本5.2 线程类5.3 线程池的概念 6. 线程安全的单例模式线程池_单例模式版本 7. 其它问题7.1 STL, 智能指针与线程安全7.2 其它常见的锁 1. 线程互斥 上文主要介绍了多线程之间的独立资源本文将详细介绍多线程之间的共享资源存在的问题和解决方法。 1.1 问题引入 intro 多线程共享进程地址空间包括创建的全局变量、堆、动态库等。下面是基于全局变量实现的一个多线程抢票的demo。 #include iostream #include pthread.h static const int t_num 5;//抢票线程数int tickets 1000;//票数void *threadRoutine(void *args) {char *name static_castchar *(args);while (true){//票数大于0, 抢票if (tickets 0){//模拟抢票花费的时间usleep(1000);std::cout name get ticket number: tickets-- std::endl;}//票数小于等于0, 退出else{break;}}return nullptr; }int main() {// 创建抢票的线程pthread_t tids[t_num];for (int i 0; i t_num; i){// 给每个线程一个名字char *name new char[64];snprintf(name, 64, thread-%d, i 1);pthread_create(tids i, nullptr, threadRoutine, name);}// 等待所有线程for (int i 0; i t_num; i){pthread_join(tids[i], nullptr);}return 0; }发现错误线程抢到负数编号的票为什么呢 为了解决上面的问题先要了解线程互斥的概念。 1.2 线程互斥的相关概念 临界资源多线程共享的资源临界区多线程访问临界资源的代码片段互斥任何时刻保证多个线程执行流中只有一个进入临界区访问临界资源称为互斥通常对临界资源起到保护作用原子性访问临界资源不会被任何调度机制打断只有完成访问和尚未访问两种状态没有其它中间状态称为原子性。 上述demo中tickets就是临界资源抢票的代码就是临界区但访问tickets的动作并不是原子性的。在C/C中tickets--在汇编层面被分为三条语句如下 三条汇编语句执行动作是1.将tickets变量拷贝到eax寄存器中2.对eax中的值减一3.将eax中的新值拷贝到tickets的内存空间中。 由于CPU对线程的调度机制线程在任意时刻都有可能都调度。因此非原子性的操作对共享变量来说是不安全的因为线程可能会在操作途中被调度而由其它线程访问共享变量造成变量数据被覆盖、判断条件误入等一系列问题。 对于上述demo当tickets1时可能存在以下情况线程1判断票数大于0进入抢票代码区却在usleep时被调度了切换为线程2此时tickets的值依然是1因为线程1并未修改它线程2判断票数依然大于0进入抢票代码区修改tickets为0然后调度回线程1线程1修改tickets为-1产生错误。这就是为什么会出现线程抢到负数编号的票的情况。当然在tickets--的时候调度也会导致一些错误如下 要想解决上面的问题就必须保证多线程之间是互斥的也就是保证访问临界资源的原子性即一个线程在临界区中不允许其它线程进入临界区。要想完成这些先要认识一个概念——互斥量又称互斥锁以及互斥锁操作的一些接口。 1.3 互斥量mutex 要想实现互斥我们需要一把锁进入临界区时申请锁锁只有一个申请不到就不得进入临界区退出临界区时归还锁任意时刻只能让单个线程持有锁Linux中提供的这把锁称为互斥量mutex。 理解对于临界资源的保护实际上是对临界区的进入作限制。 phtread_mutex_t phtread库中定义的互斥锁类型本质是一个结构体内含一个整型变量lock。可以简单理解为当没有线程持有锁时lock为1即可以获得。有线程持有锁时lock为0即无法获取。 pthread_mutex_init 初始化互斥锁对象 pthread_mutex_destroy 销毁一个已经初始化的互斥锁对象并释放相关的资源。在不再需要互斥锁时应该调用这个函数来避免资源泄漏。 销毁互斥量需要注意 使用 PTHREAD_ MUTEX_ INITIALIZER 初始化的互斥量不需要销毁 不要销毁一个已经加锁的互斥量 已经销毁的互斥量要确保后面不会有线程再尝试加锁 //mutex为phtread_mutex_t类型对象的地址, attr为锁的属性一般传nullptr即可 int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr); int pthread_mutex_destroy(pthread_mutex_t *mutex);//全局的互斥锁可以用宏的方式初始化程序退出会自动销毁 pthread_mutex_t mutex PTHREAD_MUTEX_INITIALIZER;RETURN VALUEIf successful, the pthread_mutex_destroy() and pthread_mutex_init() functions shall return zero; otherwise, an error number shall be returned to indicate the error. pthread_mutex_lock 申请互斥锁申请失败时意味着其它线程正在持有锁当前线程则会阻塞等待执行流被挂起等待互斥量解锁。 pthread_mutex_unlock 释放互斥锁 int pthread_mutex_lock(pthread_mutex_t *mutex); int pthread_mutex_unlock(pthread_mutex_t *mutex);RETURN VALUEIf successful, the pthread_mutex_lock() and pthread_mutex_unlock() functions shall return zero; otherwise, an error number shall be returned to indicate the error. ⭕对抢票问题加入互斥机制 #include iostream #include unistd.h #include pthread.h static const int t_num 5; // 抢票线程数int tickets 1000; // 票数pthread_mutex_t mutex PTHREAD_MUTEX_INITIALIZER; // 定义互斥锁void *threadRoutine(void *args) {char *name static_castchar *(args);while (true){pthread_mutex_lock(mutex);// 票数大于0, 抢票if (tickets 0){// 模拟抢票花费的时间usleep(1000);std::cout name get ticket number: tickets-- std::endl;pthread_mutex_unlock(mutex);}// 票数小于等于0, 退出else{pthread_mutex_unlock(mutex);break;}}return nullptr; }int main() {// 创建抢票的线程pthread_t tids[t_num];for (int i 0; i t_num; i){// 给每个线程一个名字char *name new char[64];snprintf(name, 64, thread-%d, i 1);pthread_create(tids i, nullptr, threadRoutine, name);}// 等待所有线程for (int i 0; i t_num; i){pthread_join(tids[i], nullptr);}return 0; }注意 lock和unlock之间的代码称为临界区要保证临界区是最小的临界区中尽可能不包含对非临界资源的访问否则会导致性能降低因为非临界资源是可以多线程并发访问的。 多个线程访问同一份临界资源要想实现互斥它们必须使用同一把锁。 现在可以正常抢票了。 1.4 互斥量实现原理 我们说对临界资源共享资源进行互斥保护需要用到互斥量。但是多个线程申请同一个互斥量互斥量不也是共享资源吗互斥量需要被互斥保护吗答案是不需要 为了实现互斥锁操作大多数体系结构都提供了swap或exchange指令该指令的作用是把寄存器和内存单元的数据相交换由于只有一条指令保证了原子性即使是多处理器平台访问内存的总线周期也有先后一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。 因此对于互斥量的操作本身就是原子的无需再被保护。下面看一下lock和unlock的伪代码帮助理解。 1.5 死锁 死锁Deadlock指的是多个线程在竞争有限的资源时由于彼此之间的互斥和等待条件而陷入无限循环的状态无法继续执行下去导致系统停滞不前。 死锁通常涉及以下四个必要条件只有它们同时成立时死锁才会发生。 互斥条件多个线程之间是互斥的即只能有一个执行流访问共享资源。占有与等待条件线程至少持有一份资源并且在等待获取其它资源时不释放自身占有的资源。不剥夺条件线程占有的资源不会被强行剥夺如被系统回收、被其它线程抢夺只得由线程释放后才可被其它执行主体获取。循环等待条件若干个线程形成了头尾相接的循环等待资源的关系每个线程都在等待另一个线程的资源。 如下面伪代码就发生了死锁问题。 避免死锁的方法 破坏四个死锁的必要条件确保多个线程按照相同的顺序和规则请求和释放锁保证锁会被释放资源一次性分配 2. 线程安全和可重入函数 概念 线程安全 多个线程访问同一段代码时不会产生不同的或意料之外的结果。如多线程对同一个全局变量或静态变量进行操作不加以锁的保护就是线程不安全的。 可重入函数 一个函数被多个线程同时调用即一个线程还没执行完该函数另一个线程就进入该函数了的情况下运行结果不会产生任何问题这样的函数称之为可重入函数反之称为不可重入函数。 线程不安全的常见场景 不保护共享资源的函数返回指向静态/全局变量指针或引用的函数调用线程不安全函数的函数 线程安全的常见场景 每个线程对全局变量或者静态变量只有读取的权限而没有写入的权限一般来说这些线程是安全的类或者接口对于线程来说都是原子操作多个线程之间的切换不会导致该接口的执行结果存在二义性 常见的不可重入函数 调用了malloc/free函数因为malloc底层是用全局链表管理堆的调用了标准IO库函数因为标准IO库的很多实现都是以不可重入的方式使用全局数据结构的可重入函数体使用了静态的数据结构 常见的可重入函数 不使用全局变量或静态变量包括修改和返回所有数据由调用者传参提供不调用malloc/free函数不调用不可重入函数使用私有数据或者通过制作全局数据的本地拷贝来保护全局数据 线程安全 vs 可重入函数 线程安全不一定是可重入但可重入函数一定是线程安全的。线程安全是线程之间的概念可重入只是函数的一个性质。线程安全的两个线程不一定调用同一个函数更别谈函数可不可重入了。可重入函数只是线程安全函数的一种线程安全函数就是保证多线程同时调用时不会出现错误的函数。不可重入函数一定是线程不安全的。 3. 线程同步 3.1 同步概念 场景如果一个线程是循环式地访问临界资源那么需要不断申请锁和释放锁。如果临界资源长时间未就绪线程进入临界区后无法访问临界资源即申请到锁就要释放了。该线程就这么一直申请锁释放锁持有锁期间啥也不干会造成其他线程的饥饿问题即其他线程长时间无法申请到锁一直处于等待状态。为解决这种问题当一个线程发现临界资源未就绪时应先让其它线程对临界资源进行修改使其就绪再访问。这说明线程对临界资源的访问需按照一定的顺序。 在保证数据安全的前提下让多线程按照某种顺序访问临界资源防止出现线程饥饿问题称为同步。 3.2 条件变量 使用条件变量可以解决上述问题。条件变量维护了一个队列。当线程发现临界资源未准备就绪时先到条件变量的队列中等待此时会释放锁让其他线程申请到锁并访问临界区。后面当临界资源就绪时由其它线程改变临界资源可以唤醒条件变量队列中的线程此时该线程会重新申请锁然后从休眠处继续向后运行。 pthread_cond_t是条件变量的类型操作函数和pthread_mutex_t的类似简单介绍。 int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr); // 初始化条件变量 int pthread_cond_destroy(pthread_cond_t *cond); // 销毁条件变量 pthread_cond_t cond PTHREAD_COND_INITIALIZER; // 静态条件变量的初始化pthread_cond_wait让调用线程在cond条件变量上等待配合mutex互斥锁使用内部会先释放。代码层面表现为线程阻塞在调用该函数处。 int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);下面一对函数可从cond条件变量上唤醒线程并让线程去申请刚刚释放的锁。若某线程唤醒后成功申请到锁则从pthread_cond_wait处继续往下运行。 int pthread_cond_broadcast(pthread_cond_t *cond); // 唤醒全部 int pthread_cond_signal(pthread_cond_t *cond); // 唤醒队头一个线程⭕注意 条件等待是线程间同步的一种手段如果只有一个线程条件不满足一直等下去都不会满足所以必须要有另一个线程通过某些操作改变共享变量使原先不满足的条件变得满足资源就绪并且友好的通知等待在条件变量上的线程。 条件不会无缘无故的突然变得满足了必然会牵扯到共享数据的变化。所以一定要用互斥锁来保护。没有互斥锁就无法安全的获取和修改共享数据 4. 生产消费模型 生产消费模型consumer and productor简称cp模型是多线程编程的一种应用场景。由一组线程扮演生产者负责生产数据由另外一组线程扮演消费者负责处理数据。因为生产消费二者的强耦合关系所有需要一个中间场所用以解耦提高模型效率合理协调生产者与消费者的工作步调。通俗地说就是生产者和消费者不直接通讯生产者将生产完的数据直接输入中间场所消费者需要数据时直接从中间场所拿二者都不关心对方的工作状态只关心中间场所是否满足自己的操作条件。中间场所充当一个缓冲区的作用。 生产消费模型涉及三个角色生产者、消费者、中间场所。 可以类比超市的场景生产者是给超市供货的工厂消费者就是到超市消费的居民而中间场所当然就是超市本身。工厂只关心超市还有没有空的货架可以进货而居民只关心超市有没有自己需要的商品他们的状态互不影响。 ⭕构建生产消费模型需要关注以下内容 三对关系生产者与生产者、消费者与消费者、生产者与消费者两个角色生产者和消费者一个缓冲区可以是不同的容器 4.1 基于阻塞队列的cp模型 阻塞队列(Blocking Queue)是一种常用于实现生产者和消费者模型的数据结构。阻塞队列与普通队列的区别在于生产者向阻塞队列写入数据时若阻塞队列为满则不会再写阻塞等待消费者读取数据使得队列中有空位可写。同理消费者从阻塞队列读取数据时若阻塞队列为空则不会再读阻塞等待生产者生产数据使得队列中有数据可读。与进程间通信中管道自带的同步机制类似 该模型中临界资源自然是blockQueue。生产者向队尾传入数据时若有多个生产者同时传入可能会引发线程安全问题因此生产者之间应该是互斥关系以保护临界资源。同理消费者之间也应该是互斥关系。对于生产者和消费者之间的关系该模型中将其定义为既有互斥关系又有同步关系生产者和消费者都视阻塞队列为一个整体即生产的时候不能消费消费的时候不能生产这是互斥关系而同步关系保证了阻塞队列的与普通队列的区别。也就是说任意时刻无论生产者消费者都只有一个线程在访问阻塞队列。 代码实现 //blockQueue.hpp 阻塞队列的实现 #pragma once #include iostream #include queue #include pthread.hconst int max_cap 5;template class T class blockQueue { public:blockQueue(int cap max_cap): _cap(cap){// 初始化互斥量和条件变量pthread_mutex_init(_mutex, nullptr);pthread_cond_init(_consumer_cond, nullptr);pthread_cond_init(_productor_cond, nullptr);}~blockQueue(){// 销毁互斥量和条件变量pthread_mutex_destroy(_mutex);pthread_cond_destroy(_consumer_cond);pthread_cond_destroy(_productor_cond);}bool isFull() { return _q.size() _cap; }bool isEmpty() { return _q.empty(); }// productor callvoid push(const T input){pthread_mutex_lock(_mutex);// 判断资源是否就绪while (isFull()) // 阻塞队列为满, 不push, 等待消费者消费{pthread_cond_wait(_productor_cond, _mutex);}// 生产数据_q.push(input);// 此时阻塞队列中至少有一个数据唤醒消费者pthread_cond_signal(_consumer_cond);pthread_mutex_unlock(_mutex);}// consumer callvoid pop(T *output){pthread_mutex_lock(_mutex);// 判断资源是否就绪while (isEmpty()) // 阻塞队列为空不pop等待生产者生产{pthread_cond_wait(_consumer_cond, _mutex);}// 消费数据*output _q.front();_q.pop();// 此时阻塞队列中至少有一个空位, 唤醒生产者pthread_cond_signal(_productor_cond);pthread_mutex_unlock(_mutex);}private:std::queueT _q; // 底层封装STL队列int _cap; // 阻塞队列的最大容量// 因为生产-生产, 消费-消费, 生产-消费的都是访问同一个缓冲区, 所以用同一把锁pthread_mutex_t _mutex;// 生产者和消费者的等待条件不同, 因此条件变量需各有一个pthread_cond_t _consumer_cond;pthread_cond_t _productor_cond; };为什么判断资源是否就绪用while循环判断而不是if判断?以消费者调用pop为例, 生产者调用push同理 在条件变量上wait的线程被唤醒后并不是直接向下条语句运行而是还要在wait内部申请锁。假设有多个消费者开始时阻塞队列为空消费者们等待。当生产者开始生产时消费者被唤醒并申请锁。用if判断可能发生的情况是多个消费者被唤醒因为生产者生产了多个数据开始竞争锁有一个消费者竞争锁成功了继续往下运行把阻塞队列中的数据都pop了那么下一个竞争到锁的消费者面对的是空队列它却并不知情因为if判断只会判断一次继续向下pop空队列引发错误。因此要用while循环判断生产者/消费者操作的缓冲区是否满足条件。 //单生产-单消费模型 #include blockQueue.hpp #include unistd.h #include ctime #include cstdlibstatic const int p_num 5; static const int c_num 3;void *productor(void *args) {blockQueueint *bq static_castblockQueueint *(args);srand(time(nullptr) ^ pthread_self());while (true){sleep(1);// 生产数据int n rand() % 100 1;// 输入数据bq-push(n);std::cout productor: n std::endl;}return nullptr; }void *consumer(void *args) {blockQueueint *bq static_castblockQueueint *(args);while (true){// 输出数据int n 0;bq-pop(n);// 处理消费数据n * 10;std::cout consumer: n std::endl;}return nullptr; }int main() {blockQueueint bq;pthread_t p, c;pthread_create(p, nullptr, productor, bq);pthread_create(c, nullptr, consumer, bq);pthread_join(p, nullptr);pthread_join(c, nullptr);return 0; }做一个小测试以便观察阻塞队列cp模型的互斥与同步现象生产者每次生产前先休眠一秒休眠时阻塞队列为空消费者等待生产者生产数据生产者的步调慢于消费者生产者生产一个消费者就拿走并处理一个。 事实上cp模型更有实际应用价值的是多生产-多消费模型而不是上面简单的一对一的关系。另外生产者生产数据消费者消费数据这里的“数据”也并非只是一个整数那么简单更多的情况是某种任务由生产者派发消费者处理。 以上两点凸显了cp模型的一大优点忙闲不均。多对多的体系中对于缓冲区的访问是互斥的因此不免有一些生产者或消费者在阻塞等待访问缓冲区而生产数据和处理数据的动作对于生产者和消费者来说却是并发的也就是说在这个体系中有的在生产数据有的在等待缓冲区资源有的在消费数据各司其职忙闲不均提高整体效率。 处理计算任务的多生产-多消费模型 //main.cc #include blockQueue.hpp #include Task.hpp #include unistd.h #include ctime #include cstdlibstatic const int p_num 5; static const int c_num 3;void *productor(void *args) {blockQueueTask *bq static_castblockQueueTask *(args);srand(time(nullptr) ^ pthread_self());while (true){sleep(1);// 生产计算任务int x rand() % 100 1;int y rand() % 100 1;char opt Task::getOperator(xy);Task t(x,y,opt);// 输入计算任务bq-push(t);std::cout productor: t.equation() ? std::endl;}return nullptr; }void *consumer(void *args) {blockQueueTask *bq static_castblockQueueTask *(args);while (true){// 输出任务Task t;bq-pop(t);// 处理计算t();}return nullptr; }int main() {blockQueueTask bq;// n-n cppthread_t p[p_num]; // 生产者pthread_t c[c_num]; // 消费者for (int i 0; i p_num; i){pthread_create(p i, nullptr, productor, bq);}for (int i 0; i c_num; i){pthread_create(c i, nullptr, consumer, bq);}for (int i 0; i p_num; i){pthread_join(p[i], nullptr);}for (int i 0; i c_num; i){pthread_join(c[i], nullptr);}return 0; }//Task.hpp #pragma once #include iostream #include string #include cstringclass Task { public:Task() default;Task(int x, int y, char opt) : _x(x), _y(y), _opt(opt), _ret(0), _exit_code(0){}std::string equation(){return std::to_string(_x) _opt std::to_string(_y) ;}int getResult() const{return _ret;}static char getOperator(int i){return opts[i % strlen(opts)];}void operator()(){switch (_opt){case :_ret _x _y;break;case -:_ret _x - _y;break;case *:_ret _x * _y;break;case /:{if (_y ! 0)_ret _x / _y;else_exit_code -1;}break;case %:{if (_y ! 0)_ret _x % _y;else_exit_code -1;}break;default:break;}}private:int _x;int _y;char _opt;int _ret;int _exit_code;static const char *opts; };const char *Task::opts -*/%;4.2 基于环形队列的cp模型 环形队列采用数组模拟以模运算的方式模拟环状特性 在设计基于阻塞队列bq的cp模型时我们将bq看作一个整体即在生产者和消费者中任意时刻都只能有一个线程去访问bq它们是互斥的这在可能会影响效率。某些场景下我们希望生产者和消费者能够在满足某种条件的情况下并发地访问缓冲区。 ⭕在环形队列中当head和tail指向不同空间时它们同时访问环形队列是互不影响的一个读一个写。只有指向同一块空间时由于不能同时读写数据所以才不能同时访问。什么时候head和tail会指向同个空间呢当环形队列为空或为满时。 将环形队列运用于cp模型中自然地head代表消费者tail代表生产者。我们可以将整个环形队列分成两份资源空间资源和数据资源。 生产者只关心空间资源即队列中是否还有空位消费者只关心数据资源即队列中是否有数据可读取。当两份资源都不为0时生产者和消费者可以并发地访问环形队列。当空间资源为0时对应环形队列为满生产者无法写入直到消费者读取数据空间资源增多。同理当数据资源为0时对应环形队列为空消费者无法读取直到生产者写入数据数据资源增多。这种同步机制我们可以通过POSIX信号量来实现。 POSIX信号量 POSIX信号量是一种用于实现多个进程或多之间的同步和互斥操作。POSIX信号量提供了一种机制允许进程在共享资源上进行协调以确保它们不会同时访问关键资源从而避免竞态条件和数据损坏。POSIX信号量和SystemV信号量作用相同都是用于同步操作达到无冲突的访问共享资源目的。 但POSIX可以用于线程间同步。 头文件 #include semaphore.h初始化信号量 int sem_init(sem_t *sem, int pshared, unsigned int value); // pshared0表示线程间共享非0表示进程间共享 // value: 信号量初始值 // 返回值成功返回0失败返回-1销毁信号量 int sem_destroy(sem_t *sem) // 返回值成功返回0失败返回-1等待信号量P操作 int sem_wait(sem_t *sem); // 将sem的值减1, 若sem的值为0则阻塞等待发布信号量V操作 int sem_post(sem_t *sem); // 将sem的值加1并唤醒等待该信号量的线程代码实现 实现不同cp模型关键在于维护生产-生产消费-消费生产-消费三对关系。在该模型中生产-生产是互斥关系消费-消费是互斥关系生产-消费是互斥同步的但是在某些情况下可以并发访问临界资源。 #pragma once #include iostream #include vector #include semaphore.h #include pthread.hstatic const int g_size 5;template class T class cirQueue { public:// size: 环形队列的最大容量cirQueue(int size g_size) : _cq(size), _comsumer(0), _productor(0){// 初始化信号量// 开始时环形队列为空空间资源设为size数据资源为0sem_init(_sem_room, 0, size);sem_init(_sem_data, 0, 0);// 初始化互斥量pthread_mutex_init(_c_mutex, nullptr);pthread_mutex_init(_p_mutex, nullptr);}~cirQueue(){// 销毁信号量sem_destroy(_sem_room);sem_destroy(_sem_data);// 销毁互斥量pthread_mutex_destroy(_c_mutex);pthread_mutex_destroy(_p_mutex);}// productor callvoid push(const T in){// 申请空间信号量sem_wait(_sem_room);// 输入数据占用空间// 保证生产者——生产者的互斥pthread_mutex_lock(_p_mutex);_cq[_productor] in;_productor (_productor 1) % _cq.size();pthread_mutex_unlock(_p_mutex);// 增加数据信号量sem_post(_sem_data);}// consumer callvoid pop(T *out){// 申请数据信号量sem_wait(_sem_data);// 输出数据释放空间// 保证消费者——消费者的互斥pthread_mutex_lock(_c_mutex);*out _cq[_comsumer];_comsumer (_comsumer 1) % _cq.size();pthread_mutex_unlock(_c_mutex);// 增加空间信号量sem_post(_sem_room);}private:std::vectorT _cq;int _comsumer;int _productor;pthread_mutex_t _c_mutex; // 消费者互斥量pthread_mutex_t _p_mutex; // 生产者互斥量sem_t _sem_room; // 空间资源信号量sem_t _sem_data; // 数据资源信号量 };一些细节 若等待信号量成功也就意味着资源准备就绪允许访问无需再判断资源是否就绪。等待信号量成功后可能会因为线程调度而在访问临界资源前被切走。这没有任何问题其它线程也需要申请信号量后再访问临界资源。信号量好比一张入场券拿到之后即使你没有第一时间入场你的座位始终为你留着。 5. 线程池 线程池(Thread Pool)是多线程的一大应用场景可以将上面学到的东西融会贯通起来。实现线程池之前先简单封装几个小组件。 5.1 互斥量RAII版本 有时候加锁之后可能忘了解锁我们需要一个与智能指针作用相同的东西用于自动申请锁和释放锁。 //lockGuard.hpp #pragma once #include pthread.hclass lockGuard { public://lockGuard类构造时lock析构时unlock互斥量在类对象的作用域中有效lockGuard(pthread_mutex_t *pmutex):_pmutex(pmutex){pthread_mutex_lock(_pmutex);}~lockGuard(){pthread_mutex_unlock(_pmutex);}private:pthread_mutex_t *_pmutex; };5.2 线程类 线程池中有很多个工作线程要寻求一种更简单的对线程的管理方法可以将线程封装成一个类 //Thread.hpp #pragma once #include iostream #include cstdio #include pthread.h #include functional #include unistd.h #include cstdlibusing namespace std;class Thread { public:typedef void *(*prun)(void *);typedef enum{NEW,RUNNING,EXITED} status; // 线程运行的状态public:Thread() default;// 创建线程时传入线程编号、线程函数以及线程函数的参数// 注意构造函数并没有在内核创建真正的线程只是在用户层面创建一个线程Thread(int index, prun func, void *arg) : _tid(0), _index(index), _func(func), _arg(arg), _stat(NEW){char name[64] {0};snprintf(name, sizeof(name), thread-%d, index);_name name;}~Thread(){}pthread_t getTid(){return _tid;}string getName(){return _name;}void operator()() // 执行线程函数{_func(_arg);}static void *runHelper(void *args){Thread *tp static_castThread *(args);(*tp)();return nullptr;}// 让线程开始运行真正在内核创建线程void run(){int ret pthread_create(_tid, nullptr, runHelper, this);if (ret ! 0)exit(1);_stat RUNNING;}void join(){int ret pthread_join(_tid, nullptr);if (ret ! 0)exit(1);_stat EXITED;}private:pthread_t _tid;string _name; // 线程名称int _index; // 线程编号status _stat; // 线程状态prun _func; // 线程执行函数的函数指针void *_arg; // 线程执行函数的参数 };5.3 线程池的概念 线程池是多线程的一种使用模式。线程池维护着多个工作线程等待监督管理者分配可并发执行的任务避免了频繁创建和销毁线程的开销同时也限制了并发工作线程的数量防止资源耗尽和系统过载。 线程池通常包含以下几个核心组件和概念 线程池管理器Thread Pool Manager线程池管理器负责创建、初始化和维护线程池。它决定了线程池的大小、任务队列的类型以及其他配置参数。 任务队列Task Queue任务队列是线程池用于存储待执行任务的数据结构。当一个任务被提交到线程池时它会被放入任务队列中等待执行。线程池中的空闲线程会从任务队列中取出任务并执行它们。 工作线程Worker Threads工作线程是线程池中实际执行任务的线程。线程池会预先创建一组工作线程并在需要时将任务分配给它们执行。 线程池的工作流程如下 应用程序将任务提交给线程池管理器。线程池管理器将任务放入任务队列中。线程池中的工作线程从任务队列中取出任务并执行它们。执行完任务的线程继续在线程池中等待下一个任务。 线程池的应用场景 需要大量的线程来完成任务且完成任务的时间比较短。 比如WEB服务器完成网页请求这样的任务使用线程池技术是非常合适的。因为单个任务小而任务数量巨大可以想象一个热门网站的点击次数。 但对于长时间的任务比如一个Telnet连接请求线程池的优点就不明显了。因为Telnet会话时间比线程的创建时间大多了。对性能要求苛刻的应用比如要求服务器迅速响应客户请求。接受突发性的大量请求但不至于使服务器因此产生大量线程的应用。突发性大量客户请求在没有线程池情况下短时间内产生大量线程可能使内存到达极限出现错误。 代码实现 #pragma once #include iostream #include queue #include vector #include pthread.h #include Thread.hpp #include Task.hpp #include lockGuard.hppstatic const int max_cap 10;template class T class threadPool { public:threadPool(const int cap max_cap) : _threads(cap), _cap(cap){// 创建线程, 所有线程处于等待任务的状态for (int i 0; i _cap; i){// 因为threadRoutine是静态成员函数所以这里要传入this指针才能使工作线程找到当前的线程池_threads[i] Thread(i 1, threadRoutine, this);}pthread_mutex_init(_mutex, nullptr);pthread_cond_init(_cond, nullptr);}~threadPool(){// 回收线程for (auto thr : _threads){thr.join();}pthread_mutex_destroy(_mutex);pthread_cond_destroy(_cond);}// 启动线程池, 所有线程开始待命void start(){for (auto thr : _threads){thr.run();}}void pushTask(const T in){lockGuard lg(_mutex);_tasks.push(in);pthread_cond_signal(_cond);}private:// 工作线程的执行函数线程池内部私有访问// 线程的执行函数默认接口指针类型为void*(*f)(void*)因此要设为static除去this指针static void *threadRoutine(void *args){threadPool *tp static_castthreadPool *(args);while (true){// 获取任务, 任务队列如果为空需要等待T t tp-popTask();// 处理任务t();}return nullptr;}// 从任务队列获取任务线程池内部私有访问T popTask(){T out;// 临界区{lockGuard lg(_mutex);// 任务队列为空时等待while (_tasks.empty()){pthread_cond_wait(_cond, _mutex);}out _tasks.front();_tasks.pop();}return out;}private:std::queueT _tasks; // 任务队列(大小无限制采用stl中的自动扩容)std::vectorThread _threads; // 工作线程// 工作线程访问任务队列的互斥量和条件变量pthread_mutex_t _mutex;pthread_cond_t _cond;int _cap; // 工作线程数量的最大值 };测试用例 #include threadPool.hpp #include ctime #include cstdlib #include unistd.hint main() {// 创建线程池threadPoolTask tp;// 启动线程池tp.start();srand(time(nullptr) ^ getpid());while (true){// 生产计算任务int x rand() % 100 1;int y rand() % 100 1;char opt Task::getOperator(x y);Task t(x, y, opt);// 输入计算任务tp.pushTask(t);}return 0; }6. 线程安全的单例模式 单例模式(Singleton Pattern)是一种设计模式确保一个类只有一个实例对象并提供一个静态的访问接口来获取该实例。 这意味着无论在应用程序中的任何地方调用获取单例实例的方法都会返回同一个对象实例。单例模式的目标是限制一个类的实例化并确保该实例可以在整个应用程序中共享和访问。 单例模式可以通过饿汉方式和懒汉方式实现 饿汉方式程序启动时单例模式的类实例对象就存在了这个对象一般是全局属性的且该进程中只有一个。懒汉方式第一次获取实例时才会创建该实例对象。 通常使用懒汉方式实现单例模式这样做可以节省资源避免提前的空间浪费只要实例对象要被使用了才会创建。 线程池_单例模式版本 线程池是一个较为复杂的数据结构管理难度大占用资源多因此可以使用单例模式实现线程池保证同一进程中只有一个线程池节省资源方便管理。同时使用懒汉方式的单例模式来改良线程池首次使用线程池时再创建其实例对象避免了空间浪费。 设计单例模式的核心步骤 禁止用户使用类的构造函数、拷贝构造、赋值重载。以类的静态成员变量存储唯一的实例对象或指针提供静态的访问接口get_instance以获取单例对象 关于get_instance函数的两个小细节 该函数可能会被多个线程调用多个线程都想获取同一个单例对象那么这个单例对象是临界资源需要被保护保证线程安全。否则可能会出现创建个单例对象的情况。见下面代码中get_instance函数中的注释。 #pragma once #include iostream #include queue #include vector #include pthread.h #include Thread.hpp #include Task.hpp #include lockGuard.hppstatic const int max_cap 10;template class T class threadPool {typedef threadPoolT Self;public:static Self *get_instance(){if (_tp nullptr){// 只有第一次访问单例时会进入此处// 那么绝大多数情况都是不进入的// 每次都要竞争锁再判断是否进入区块的话, 效率低// 利用双判断, 解决问题lockGuard lg(_tp_mutex);if (_tp nullptr){_tp new Self;_tp-start();}}return _tp;}~threadPool(){// 回收线程for (auto thr : _threads){thr.join();}pthread_mutex_destroy(_mutex);pthread_cond_destroy(_cond);}// 启动线程池, 所有线程开始待命void start(){for (auto thr : _threads){thr.run();}}void pushTask(const T in){lockGuard lg(_mutex);_tasks.push(in);pthread_cond_signal(_cond);}private:// 构造函数不能删除设为私有但仅会被get_instance调用一次threadPool(const int cap max_cap) : _threads(cap), _cap(cap){// 创建线程, 所有线程处于等待任务的状态for (int i 0; i _cap; i){// 因为threadRoutine是静态成员函数所以这里要传入this指针才能使工作线程找到当前的线程池_threads[i] Thread(i 1, threadRoutine, this);}pthread_mutex_init(_mutex, nullptr);pthread_cond_init(_cond, nullptr);}// 默认拷贝构造和赋值重载声明为delete删除threadPool(const Self tp) delete;void operator(const Self tp) delete;// 工作线程的执行函数线程池内部私有访问// 线程的执行函数默认接口指针类型为void*(*f)(void*)因此要设为static除去this指针static void *threadRoutine(void *args){threadPool *tp static_castthreadPool *(args);while (true){// 获取任务, 任务队列如果为空需要等待T t tp-popTask();// 处理任务t();std::cout result: t.equation() t.getResult() std::endl;}return nullptr;}// 从任务队列获取任务线程池内部私有访问T popTask(){T out;// 临界区{lockGuard lg(_mutex);// 任务队列为空时等待while (_tasks.empty()){pthread_cond_wait(_cond, _mutex);}out _tasks.front();_tasks.pop();}return out;}private:std::queueT _tasks; // 任务队列(大小无限制采用stl中的自动扩容)std::vectorThread _threads; // 工作线程// 工作线程访问任务队列的互斥量和条件变量pthread_mutex_t _mutex;pthread_cond_t _cond;int _cap; // 工作线程数量的最大值static Self *_tp; // 指向唯一的实例对象static pthread_mutex_t _tp_mutex; // 保护_tp的互斥量 };template class T threadPoolT *threadPoolT::_tp nullptr;template class T pthread_mutex_t threadPoolT::_tp_mutex PTHREAD_MUTEX_INITIALIZER;双判断可以避免get_instance中多线程不必要的竞争锁思路如下 7. 其它问题 7.1 STL, 智能指针与线程安全 STL中的容器并不是线程安全的。因为STL的设计初衷就是挖掘效率之极限而一旦涉及到加锁保证线程安全会对性能造成巨大的影响。而且对于不同的容器加锁方式的不同,性能可能也不同(例如hash表的锁表和锁桶).因此STL默认不是线程安全。如果需要在多线程环境下使用, 往往需要调用者自行保证线程安全。 智能指针对于 unique_ptr由于只是在当前代码块范围内生效因此不涉及线程安全问题。对于 shared_ptr多个对象需要共用一个引用计数变量所以会存在线程安全问题。但是标准库实现的时候考虑到了这个问题基于原子操作(CAS)的方式保证shared_ptr能够高效且原子地操作引用计数。 7.2 其它常见的锁 悲观锁 在每次取数据时总是担心数据会被其他线程修改所以会在取数据前先加锁读锁写锁行锁等当其他线程想要访问数据时被阻塞挂起乐观锁 每次取数据时候总是乐观的认为数据不会被其他线程修改因此不上锁。但是在更新数据前会判断其他数据在更新前有没有对数据进行修改。主要采用两种方式版本号机制和CAS操作。CAS操作当需要更新数据时判断当前内存值和之前取得的值是否相等。如果相等则用新值更新。若不等则失败失败则重试一般是一个自旋的过程即不断重试。自旋锁 当一个线程尝试获取自旋锁时如果锁已经被其他线程占用该线程不会被阻塞而是会在一个循环中不断检查锁是否可用。这种自旋等待的行为可以减少线程上下文切换的开销因为线程不会被挂起和唤醒但也可能导致CPU资源的浪费。 ENDING…
http://www.yutouwan.com/news/315062/

相关文章:

  • 网站设计集团购买主机可以做网站吗
  • 网站变成灰色外包公司做网站图片哪里整的
  • 新乡网站优化杭州十大跨境电商排名
  • 美空间网站大气学校网站模板
  • 网站建设要实现的目标wordpress 重写分页
  • 西安做网站的公司有哪些开网页多对什么要求高
  • 网站代运营费用软件开发税率是13%还是6
  • 交易网站开发文档网站推广码怎么做
  • 自助建站平台哪家好泰安建设网站公司
  • 电子产品网站模板淘宝上网站建设为啥这么便宜
  • 中国门户网站南宁网站建设超薄网络
  • 本溪网站开发公司怎么获取网站ftp地址
  • 重庆綦江网站建设宁波网站建设模板下载
  • 网站修改 iis6应用程序池做动漫姓氏头像的网站
  • 如何做电商网站视频广州网络维护
  • 装修企业网站建设开个做网站的公司
  • 2018年深圳建设网站公司做暖暖视频网站大全
  • wordpress模板导航类股票发行ipo和seo是什么意思
  • 无为建设局网站python教程
  • 麒贺丝网做的网站优化优化的定义
  • 做网站公司 上海大公司网站建设
  • 怎么在手机上做企业网站手机赚钱平台正规
  • 网站开发合同缺陷网站建设大神级公司
  • 做网站开发需要什么证书海南做网站公司哪家好
  • 做个网站的费用企业邮箱app下载
  • 网站建设公司汉狮网络免费wap自助建站火星建站
  • 深圳专门做网站的公司wordpress 专题页
  • 黄浦专业做网站音乐网站建设教程视频
  • 济宁公司做网站中国石油大学网页设计与网站建设
  • 运城市盐湖区姚孟精诚网站开发中心wordpress 个性主题