四川住建局和城乡建设厅,下沙网站优化,单位网站中文域名到期续费,龙岩论坛目录
信号量
信号量是什么
信号量的操作
基于环形队列的生产者消费者模型
我们想怎么写这个模型
线程池
线程池的实现
线程的属性及方法
线程池的属性及方法
主线程 信号量
信号量也是通信的一种#xff0c;前面在进程间通信的时候#xff0c;我们说信号量到线程的…目录
信号量
信号量是什么
信号量的操作
基于环形队列的生产者消费者模型
我们想怎么写这个模型
线程池
线程池的实现
线程的属性及方法
线程池的属性及方法
主线程 信号量
信号量也是通信的一种前面在进程间通信的时候我们说信号量到线程的时候会说现在我们正式的谈一下信号量。
信号量是什么
信号量是一种通信机制在进程间通信可以使用在线程中也可以使用但是信号量并不像管道那样用于数据的传输而是信号量是用于访问控制的没在多线程或者多进程中如果没有访问控制的话那么通信有时候就会有问题。
也就是当我向发给你 123456789 这个电话号码时我刚发了1234 你就拿到了但是这个数据你用不了因为我想给你发都是一个整体所以就需要访问控制。
那么现在我们先知道信号量是一个数据通信时候用于访问控制的一个机制那么下面我们继续说一下信号量。
信号量的本质类似一把计数器
下面我们来距离说一下这个信号量在前面的多线程中不论是基于阻塞队列的生产者消费者模型中还是前面的抢票。我们发现我们都将临界资源作为一个整体使用。但是实际上有一些临界资源是可以被划分的。
下面我们用电影院举例子
现在我们要去看一场电影我们现在以及买好一张票了那么这意味着我们到时候区看这场调用的时候一定有一个座位是我的那么即使我们开始的时候没有去但是我们电影即使是快结束了那么我去了的话这场电影还是有我的一个座位。
那么这个就是预定而信号量也是这样因为临界资源有时候可以被划分为多分资源每个人都访问的是不同的位置如果所有的位置都有人在访问那么其他人才访问不了如果临界资源里面的资源没有被使用完那么就可以访问。
而我们每个人都像拿线程一样而电影院中的座位就是临界资源被划分后的小资源而电影票就是信号量所以这里说信号量类似于一把计数器。
所以这里只需要记住一个结论信号量类似一把计数器。
信号量的操作
既然我们知道信号量是什么也知道信号量是干什么的下面看一下信号量的操作
信号量和前面的锁mutex、条件变量cond其实基本操作都是一类似的。
首先是信号量的初始化
NAMEsem_init - initialize an unnamed semaphore
SYNOPSIS#include semaphore.h
int sem_init(sem_t *sem, int pshared, unsigned int value);
Link with -pthread. 这里的信号量的第一个参数就是一个 sem_t 的一个指针类似于锁的 pthread_mutex_t。 第二个参数表示共享如果是0的话那么表示只在该进程内有效其他的进程看不到如果是非0的话那么就表示共享一般都设置为0即可。 第三个参数如果还记得的话那么前面我们是说过一个信号量的结论的就是信号量的本质类似一把计数器那么这把计数器当然要有值所以这个参数就是用于初始化计数器的。
信号量的销毁
NAMEsem_destroy - destroy an unnamed semaphore
SYNOPSIS#include semaphore.h
int sem_destroy(sem_t *sem);
Link with -pthread. 这个函数不需要介绍这就是一个信号量初始化后需要销毁的函数。
信号量的P操作
NAMEsem_wait, sem_timedwait, sem_trywait - lock a semaphore
SYNOPSIS#include semaphore.h
int sem_wait(sem_t *sem);
int sem_trywait(sem_t *sem);
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
Link with -pthread. 这个就是信号量的申请前面不是说了信号量的本质就是一把计数器所以信号量的申请就可以理解为对这把计数器进行减减操作。 而对信号量的申请又叫做P操作 那么上面这三个函数分别有什么意思 第一个函数就是表示对信号量进行申请如果此时计数器为0那么还要申请信号量此时就会阻塞类似于竞争锁一样而信号量可以是任意数而锁只有0~1也就是类似于锁是计数器只有1的信号量如果还有信号量那么此时就信号量申请成功就可以去访问临界资源了。 第二个函数也是申请信号量其实这个我们也在锁的时候看见过类似的pthread_mutex_trylock也是类似的效果如果没有信号量此时直接返回不进行阻塞所以这里也就可以缺点锁其实就是只有一个计数器的信号量没问题。 第三个函数这个函数多了一个 time 的参数其实我们知道了上面两个函数这个函数都能猜到是什么作用一定是在传入的时间范围内阻塞超出范围就返回。
信号量的V操作
NAMEsem_post - unlock a semaphore
SYNOPSIS#include semaphore.h
int sem_post(sem_t *sem);
Link with -pthread. 这个函数就类似于锁里面的 unlock 函数这个而前面也说了锁也可以理解为只有1的计数器那么对锁的unlock也就可以为对这把锁在加加而lock表示对这把锁减减那么这个函数就是对信号量计时器进行加加。 而这个操作就是对信号量的V操作。 看了前面的这些函数我们看到下面都有一个 link with_pthread 的一句话说明这些函数是需要 pthread 库的所以使用的时候也需要链接 pthread 的库。 基于环形队列的生产者消费者模型
我们想怎么写这个模型
我们想基于环形队列来写这个那么我们还需要什么呢我们还需要就是使用信号量因为环形队列里面大多数时候一定有很多资源不论是空位置生产者可以生产还是数据消费者可以消费这些都是资源。
那么也就意味着如果将整个环形队列看作一个整体那么一次只有一个线程可以进入访问资源那么就会导致效率低下那么可以将整个环形队列看作多个资源可以让多个线程进入那么也就可以提高效率。
那么对于生产者而言我们需要关注哪些资源呢
对于生产者而言我们需要关注的是空的位置也就是没有数据的位置。
刚开始的时候环形队列的起始位置和终止位置一定是在一起的我们认为end位置指向最后一个元素的下一个位置所以当环形队列为空的时候和环形队列为满的时候他的 start 和 end 一定是在一起的而当环形队列为空或者为满那么此时生产者和消费者分别要做什么
这里先说生产者当环形队列为空的时候那么生产者刚好需要的是空位置用于存放生产好的资源所以这时候生产者是可以生产的那么当队列为满的时候生产者应该怎么做呢当队列为满的时候此时是没有空位置的那么此时生产者是不能进行生产的而是应该让消费者来消费。
而当既有空位置又有数据的时候那么此时生产者和消费是访问的是不同的位置那么此时生产者和消费者此时就是并发的。
对于消费者而言消费者需要关注的是数据资源。
那么当环形队列刚开始的时候是没有数据的那么当消费者想要消费的时候一旦申请了信号量那么就会阻塞住所以此时一定是生产者来生成当生产者生产资源后那么就说明现在以有了数据资源也就是可以让数据的信号量进行加加操作也就是V操作而当消费者在消费后那么此时一定是有空位产生的没那么此时消费者也就可以让空位置的信号量加加也就是空位置的V操作。
那么当消费者申请一个资源后那么数据资源的信号量怎么办呢当消费者申请一个信号量的时候说明此时的环形队列里面一定会有一个数据是可以被这个消费者所访问的那么如果我们不访问这个数据呢不访问也关系我们可以后面去访问即使后面去访问那么一定还是有一个数据是属于我们的那么当我们申请资源后那么此时的数据的信号量一定是需要减减操作的。 现在我们在结合锁说一下信号量
之前我们在使用锁的时候如果是多线程的条件下锁只能保证临界资源的安全并不能保证合理性前面我们说过需要访问临界资源就需要先对临界资源检测看是否存在而之前我们检测到不存在的时候我们就需要条件变量来控制如果不满足就阻塞在条件变量里面如果当条件满足了就唤醒。
但是我们这里可没有主动的去检测临界资源是否满足因为这里我们其实以及检测了那就是使用信号量而信号量的计数器就表示资源的数量如果资源的数据剩余0那么此时继续申请信号量此时就会判断到条件不满足此时就会进行阻塞也就类似于我们判断到条件不满足后我们调用 pthread_cond_wait 操作如果此时以及条件满足的了那么此时我们调用 pthread_cond_signal/pthread_cond_broadcast 函数就类似于调用 sem_post 函数。
所以其实我们也以及检测过条件是否满足了而不满足后也是阻塞。 下面我们在说一下关于这个代码里面的需要的成员变量
首先就是我们所有线程都访问的是一个环形队列那么为什么能划分为小资源呢为什么可以让生产者和消费者可以同时访问不同的资源呢任意一方没有资源的话那么又是如何做到让生产者和消费者的互斥的呢因为我们生产者想要访问的是数据资源而消费者想要访问的是空位置资源如果没有资源那么也就是 start end 说明一定是资源要么为空要么为满所以此时不论生产者还是消费者一定是有一方会阻塞的所以信号量就保证了生产者者和消费者之间一定会访问不同的资源也会在特定的时候会互斥也会同步。那么既然是访问不同的资源那么我们一定需要两个位置用来标记生产者和消费者想要访问的不同资源。
我们在前面谈到了生产者关注的是空位置资源消费者关注的是数据资源所以我们一定至少需要两个条件变量用来维护这两个资源的个数而刚开始的时候数据资源是为0的也就是当刚开始消费者就想消费的时候是不成功的而空位置资源的个数就取决于环形队列的大小了也就是刚开始是可以生产的。
那么我们的生产者消费者模型一定是多线程的我们目前已经处理好生产者和消费者之间的关系但是当多个生产者或者多个消费者同时进入临界资源想要生产或者消费数据呢所以此时我们一定是需要锁的需要用锁来维护生产者和生产者之间的互斥也就是每次只能有一个生产者进入临界资源访问还有就是消费者和消费者之间的互斥一次也只能有一个消费者进入消费那么此时我们需要几把锁呢如果我们只有一把锁那么就是生产者消费者共用这把锁也就是一次只能由一个线程进入临界资源那么就相当于是阻塞队列所以不能只有一把锁那么也就是我们也是需要至少两把锁一把是用与消费者与消费者一把是用于生产者和生产者的。 基于环形队列的生产者消费者模型代码
成员变量
templateclass T
class ringQueue
{
public:ringQueue(int size):_ring_queue(size)// 初始化环形队列的大小使用数组模拟环形队列,_consum_sem(0)// 初始化消费者信号量,_product_sem(size)// 初始化生产者信号量,_consum_step(0)// 初始化消费者起始位置,_product_step(0)// 初始化生产者起始位置{}
private:std::vectorT _ring_queue;// 环形队列sem _consum_sem;// 将信号量封装sem _product_sem;mutex _consum_lock;// 将锁封装mutex _product_lock;int _consum_step;// 标记消费者位置int _product_step;// 标记生产者位置
};
// 封装信号量
class sem
{
public:sem(int n){sem_init(_semid, 0, n);}
~sem(){sem_destroy(_semid);}
void p(){sem_wait(_semid);}
void v(){sem_post(_semid);}
private:sem_t _semid;
};
//封装锁
class mutex
{
public:mutex(){pthread_mutex_init(_mtx, nullptr);}
~mutex(){pthread_mutex_destroy(_mtx);}
void lock(){pthread_mutex_lock(_mtx);}
void unlock(){pthread_mutex_unlock(_mtx);}
private:pthread_mutex_t _mtx;
};
下面我们环形队列还需要对数据进行添加的操作
所以我们需要一个函数用于对数据向环形队列中添加 void push(const T date){// 如果是多线程条件下那么需要先加锁// 那么加锁怎么加呢加到哪里呢应该先申请信号量还是先加锁呢// 放数据的时候首先应该判断是否有空余位置_product_sem.p();// 申请信号量如果申请到了那么就可以继续向下走如果没有信号量那么阻塞_product_lock.lock();// 这里说明一定是有属于这个线程的一个资源的那么由于是多线程需要加锁老保护生产者的这部分资源因为不光有一个生产者。_ring_queue[_product_step] date;_product_lock.unlock();// 访问后就解锁_consum_sem.v();// 如果已经生产好了那么一定是有一个数据资源的此时就需要对数据资源进程加加操作}
那么还需要一个用于对消费者拿数据的一个函数 void pop(T* date){// 拿数据的时候需要先查看是否有数据_consum_sem.p();// 先申请数据资源_consum_lock.lock();// 访问数据资源前先加锁因为不光有一个线程访问该资源*date _ring_queue[_consum_step];_consum_lock.unlock();_product_sem.v();// 访问后那么一定是一个数据被消费了那么一定就有一个空的资源产生了}
所以我们的循环队列只有这么点代码下面我们看一下主函数的逻辑
主函数里面我们只需要创建多线程出让一部分线程执行生产者的代码一部分执行消费者的代码即可然后也可以 join 其他的线程也可以直接 detach 其他线程然后不要 join但是一定要保证主线程不要退出如果退出的话那么其他的线程也就跟着退出了。
const int CONSUM_NUM 4;
const int PRODUCT_NUM 4;
void *consumer(void *args)
{ringQueueint *rqueue (ringQueueint*)args;pthread_detach(pthread_self());while(true){int date 0;usleep(1000);rqueue-pop(date);usleep(1000);cout 消费者: [ pthread_self() ]消费了一个数据: date endl;sleep(1);}
}
void *producter(void *args)
{ringQueueint *rqueue (ringQueueint*)args;pthread_detach(pthread_self());while(true){int date rand() % 100 1;cout 生产者: [ pthread_self() ]生产了一个数据: date endl;// usleep(1000);rqueue-push(date);// usleep(1000);// sleep(1);}
}
void crtConsum(void *args)
{pthread_t tid;for (int i 0; i CONSUM_NUM; i)pthread_create(tid, nullptr, consumer, args);
}
void crtProduct(void *args)
{pthread_t tid;for (int i 0; i PRODUCT_NUM; i)pthread_create(tid, nullptr, producter, args);
}
int main()
{srand(time(0));ringQueueint *rqueue new ringQueueint(5);crtConsum(rqueue);crtProduct(rqueue);while(true) sleep(100);delete rqueue;return 0;
}
线程池
在这之前先介绍一下池的概念一般的池是为了什么呢
池就是为了提高效率那么如何提高效率呢
假设我们现在有很多任务所以需要很多线程来执行但是此时是没有线程的所以需要先创建线程那么创建线程需要消费系统资源吗需要虽然说创建线程的消耗没有创建进程的消耗高但是如果一次性创建大量的线程也是很消耗系统资源的所以为了应对这个情况我们可以提前创建一批线程这样即使突然有大量的任务到达也可以不需要创建大量的线程这样就可以提高效率因为创建线程也是需要消耗时间的所以这样就可以节省每次创建线程的时间当没有任务的时候就可以让这些线程等待。
下面说一下线程池准备如何实现
我们的线程池想让主线程获取任务然后将任务放到线程池的队列中然后让创建好的线程去处理这些任务而我们发现这是什么这不就正是我们前面写的生产者消费者模型吗 下面线程的实现中使用了一下其他的头文件但是这些头文件里面的内容就不展示出来了想要看的可以去git 里面去看使用的其他头文件有 log.hpp日志 task.hpp让线程处理的任务
[线程池代码] https://gitee.com/naxxkuku/linux/tree/master/threadPool 线程池的实现
线程的属性及方法
既然我们需要线池那么一定有很多线程我们就需要对这些线程管理起来所以我们需要一个容器用来管理这些线程但是此时我们要管理线程我们是不是还需要先对这些线程进行先描述在组织
属性
那么这些线程里面有一些什么属性呢
线程的名线程的 id线程的回调函数以及线程传入的参数所以我们就可以先对线程进行描述一下也就是对线程进行封装一下由于我们可能传入的并不是一个参数所以我们也可以对传入的参数也进行封装。
下面是对线程参数的封装和线程的封装:
下面为了简单创建线程传入的参数就只有一个原本想要传入的参数还有一个就是线程的名字。
typedef void*(*func_t)(void*); // 函数指针 类型 参数为 void* 返回值为 void*
Log log;
struct threadDate
{string _name;void* _args;
};
class Thread
{
public:Thread(int thread_name, func_t routine, void* args):_name(string(thread ) to_string(thread_name)),_args(args),_routine(routine){}private:std::string _name; // 线程名pthread_t _tid; // 线程 idvoid* _args;func_t _routine; // 线程要执行的函数
};
方法
那么对线程的封装还需要什么呢线程还需要什么方法呢
仅仅有这个线程的参数和回调时没什么用的所以需要让线程启动起来也就是创建线程所以i还需要一个创建线程的函数start那么只有启动线程的函数就够了吗当然还需要一个可以 join 的函数对该线程进行等待。
// 创建线程
void start()
{threadDate* date new threadDate{_name, _args};pthread_create(_tid, nullptr, _routine, (void*)date);log(INFO, %s 线程创建成功, 线程id: %ld, _name.c_str(), _tid);
}
// 线程等待
void join()
{pthread_join(_tid, nullptr);log(INFO, %s 线程 join 成功, _name.c_str());
}
这里我们就让创建线程的时候将一个 threaddate 的参数传入里面包含了原本想要传入的参数还有就是线程的名字其实也可以不带但是这里为了演示传入其他参数的方法。
那么上面就是线程的封装线程这些封装也就足够使用了。
线程池的属性及方法
属性
那么线程池我们也是需要封装的那么线程池我们需要一些什么属性呢
线程池里面不光有一个线程而线程我们以及描述好了我们只需要在线程池中进行组织即可那么我们使用什么来组织呢我们可以使用一个 vector 来组织。
那么对于线程池来说还需要什么属性呢既然是一个线程池那么没有线程的个数怎么可以所以还需要线程的个数用于初始化线程池中线程的创建个数。
那么光这就够了吗当然不够既然我们前面说了线程池实际上也就是让一批线程拿任务还有线程向线程池里面放任务那么线程池也就是生产者消费者模型所以我们当然还需要一个缓冲区一个交易出场所所以还需要一个 queue来充当交易场所。
那么如果是多线程的情况下这个交易场所一定是临界资源那么我们也就是一定需要加锁所以我们需要一个锁光有锁只能保证临界资源的安全但是无法保证合理性所以我们还需要一个条件变量。
template class T
class threadPool
{
public:
private:int _tnum; // 线程的个数std::queueT _task_q; // 任务队列std::vectorThread * _threads; // 管理线程的数据结构pthread_mutex_t _mutex; // 互斥锁pthread_cond_t _cond; // 条件变量
};
方法
线程池的构造函数怎么写呢既然是构造那么首先就是要有一批线程的对象所以线程池的构造函数我们可以先new 一批线程的对象然后当线程池启动的时候我们就让这些线程开始执行任务的处理方法我们可以在线程池的创建中就需要将该线程需要处理的方法告诉线程还有就是该线程的参数。
当有了这些参数后就可以 New 线程的对象然后因为这些都是线程所需要的参数所以需要将这些参数都传入然后将创建好的线程对象的 push 到用于管理线程的结构体中。
threadPool(int tnum THREAD_NUM): _tnum(tnum)
{// 初始化锁和条件变量pthread_mutex_init(_mutex, nullptr);pthread_cond_init(_cond, nullptr);// 创建线程池for (int i 0; i _tnum; i){// 这里需要传入线程执行的回调所以传入 routine (静态成员方法)为什么时静态的下面解释_threads.push_back(new Thread(i 1, routine, (void *)this));// 这里传入 这里传入 this指针为什么一会解释}log(INFO, thread pool创建成功~);
}
这里再创建 new 线程对象的时候需要传入线程的名这里使用的数字再线程的构造函数中他自己会根据这些数字构建一个线程的名字thread数字来表示线程的名字。
还有就是一把锁和一个条件变量的初始化这个就不多解释了不明白的可以看一下线程的互斥与同步这篇博客。
还有就是这里需要传入线程的回调函数那么上面说明了这里传入的是一个静态的成员函数为什么传入静态的成员函数呢
解释线程的回调函数的类型是什么void*(*)(void*) 也就是这个是一个返回值为 void* 参数为 void* 的一个类型但是再C中C的 struct 升级了也和 class 一样而类里面不仅可以有属性还可以有方法而类的普通的成员函数的方法是有一个隐藏的 this 指针的也就是再函数的第一个参数主要是为了表示是那个对象调用的该函数所以这里如果传入的是成员函数那么里面就多了一个隐藏的 this 的指针所以如果将成员函数传入的话虽然类型看起来是 void*(*)void* 但是实际上的类型是void*(*)void(type*, void*) 实际类型是还多了一个 type 类型的指针所以传入的话与线程所需要的参数是不匹配的所以这里就会编译不通过。
还有一个就是参数传入该线程池对象的 this 指针这个后面解释。
既然有构造函数而且构造函数里面我们还进行了 new 操作所以一定需要再析构函数里面进行释放那么一定还需要有析构函数
~threadPool()
{for (int i 0; i _tnum; i){_threads[i]-join();delete _threads[i];}pthread_mutex_destroy(_mutex);pthread_cond_destroy(_cond);log(INFO, 线程池销毁成功~);
}
析构函数就比较简单了因为是线程所以我们需要对这个线程进行 join 其实不进行 join 也可以但是需要对线程进行 pthread_detach 否则线程可能会类似僵尸进程一样的结构导致系统资源泄露而这就是为什么线程的类中需要一个 join 的函数。
还有就是既然锁和条件变量以及初始化过了所以我们也一定不要忘记对这两个变量的销毁。
而线程池的析构函数会再该对象生命周期结束的时候自动调用该函数所以再结束的时候会自动 join 所以也就不用害怕内存泄露等问题了。
那么构造好后说明现在已经有一个线程池的对象了但是这歌线程池还没有启动那么我们可以写一个函数可以让里面的线程都启动也就是创建该线程所以我们还有一个 run函数就是为了让线程池启动而别忘了每个线程对象中还有一个start 函数这歌函数就是为了让线程创建 void run(){for (int i 0; i _tnum; i){_threads[i]-start();}log(INFO, 线程池启动成功~);}
所以那么如何启动因为再构造的时候已将将每一个线程的对象需要执行的方法以及方法的参数都传进入了所以此时只需要create 即可start函数就是直接 create 可以看一下前面的代码。
那么线程池里面的线程是需要拿 _task_q成员变量——队列中的数据消费的如果队列中没有数据的话那么也就是没法消费的所以我们还需要一个函数用来对该该队列进行插入数据
void push_task(const T task)
{lockGuard lock(_mutex);// 锁守卫这里构造会自定加锁_task_q.push(task);weakup_cond();log(DEBUG, 任务 push 成功~);
}// 当出作用域的时候 lock 对象会调用析构函数
将数据放入队列中的话需要放入到 _task_q 中而线程池中的线程去数据也是从这个结构中取数据所以该资源是临界资源所以是需要加锁的当加锁后就可以访问该资源。
这里的加锁我们可以看到使用的是锁守卫其实这个前面使用过一次还是再看一下什么是锁守卫其实锁守卫采用了RAII的思想这里可以写一个类该类的构造方法需要一把锁的地址然后再构造的时候会自动加锁当该类析构的时候会自动解锁所以就不会忘记解锁了。
下面看一下锁守卫的代码其实很简单
// 锁守卫
struct lockGuard
{lockGuard(pthread_mutex_t *mutex): _mutex(mutex){pthread_mutex_lock(_mutex);}
~lockGuard(){pthread_mutex_unlock(_mutex);}
private:pthread_mutex_t *_mutex;
};
这里将数据插入之后那么还需要做什么我们不要忘记了如果没有数据的话那么线程池的线程是再等待的也就是需要再空数据的条件变量下等待那么如果有了数据之后该怎么办是不是应该唤醒一个线程所以这里还应该调用唤醒的函数 signal/broadcast 所以这里唤后此时就会有一个/一批线程就取消费数据。
最后一个函数也就是回调函数那么回调函数就是这一批线程需要执行的方法了那么这批线程需要干什么呢既然是生产者消费者模型那么这批线程是充当消费者的角色的而主线程是充当生产者而消费者就需要做的是将队列中的数据取出来然后取处理这一批数据那么取数据是不是访问临界资源是的所以就需要加锁但是再取数据的第一步是什么?是不是查看是否还有资源也就是检测临界资源那么检测临界资源其实也是访问临界资源既然是访问临界资源那么就需要加锁所以第一步就是加锁然后检查是否还有临界资源如果没有的话那么就需要再条件变量下等待被唤醒这就是为什么再主线程将数据添加到队列后就需要唤醒一个线程。
static void *routine(void *args){threadDate *date reinterpret_castthreadDate *(args);threadPool *self reinterpret_castthreadPool *(date-_args);while (true){T taskdate;{lockGuard lock((self-_mutex));while (self-is_empty())self-wait_cond();log(DEBUG, 竞争锁成功可以消费数据~);
taskdate (self-_task_q).front();(self-_task_q).pop();log(DEBUG, 获取到任务~);}// 处理任务log(INFO, %s 处理任务完成 --- %d%c%d%d, date-_name.c_str(), taskdate.get_x(), taskdate.get_op(), taskdate.get_y(), taskdate());usleep(1000);}
// 删除 args 这里的 args 是 new 出来的delete date;}
前面以及将这个函数的大概执行逻辑说明白了前面我们不是说这个函数是一个 static 方法吗既然是 static 返回发那么可不可以访问像临界资源还有锁这种成员变量呢不可以那么我们刚才说线程需要执行这个返回发而且还需要加锁加锁后又需要检验临界资源如果不满足就需要条件变量满足的话就需要取数据然后处理那么前面说的这些都是成员变量而 static 的函数只能访问 static 的变量还有函数因为 static 的函数里面没有隐藏的 this 指针所以无法访问到对象里面的数据那么要怎么办呢这就是为什么再线程的参数哪里传入 this 指针如果有了这个指针那么是不是可以访问到该类中的成员变量还有其他的数据。
主线程
线程池就这些代码而线程池也就写完了那么既然有了线程池我们就可以使用他我们可以再主函数里面让主函数扮演生产者将数据放入到这队列中然后这些线程会自动处理这些数据但是这里的任务需要提供一个仿函数然后任务的对象调用仿函数就会自动处理任务。
const char* oper -*/%;
int main()
{threadPooltask tpool;tpool.run();
srand(time(nullptr) ^ getpid());// 获取任务while(true){int x rand() % 100 1;int y rand() % 100 1;char op oper[rand() % 5];task taskdate(x, y, op);tpool.push_task(taskdate);sleep(1);}
return 0;
}
这里的任务使用的是之前使用过的一个任务就是加减乘除模。