大家好,今天来为大家分享多语言视频网站源码分享的一些知识点,和多语言网站主站有哪些的问题解析,大家要是都明白,那么可以忽略,如果不太清楚的话可以看看本篇文章,相信很大概率可以解决您的问题,接下来我们就一起来看看吧!
开源项目Workflow中有个重要的基础模块:
代码仅300行的C语言线程池。
本文会伴随源码分析,而逻辑完备、对称无差别的特点于第3部分开始
欢迎跳阅,或直接到Github主页上围观代码
https://github.com/sogou/workflow/blob/master/src/kernel/thrdpool.c
0-Workflow的thrdpool
Workflow的大招:计算通信融为一体的异步调度模式,而计算的核心:Executor调度器,就是基于这个线程池实现的。可以说,一个通用而高效的线程池,是我们写C/C++代码时离不开的基础模块。
thrdpool代码位置在src/kernel/,不仅可以直接拿来使用,同时也适合阅读学习。
而更重要的,秉承Workflow项目本身一贯的严谨极简的作风,这个thrdpool代码极致简洁,实现逻辑上亦非常完备,结构精巧,处处严谨,复杂的并发处理依然可以对称无差别,不得不让我惊叹:
妙啊!!!
你可能会很好奇,线程池还能写出什么别致的新思路吗?先列出一些,你们细品:
特点1:创建完线程池后,无需记录任何线程id或对象,线程池可以通过一个等一个的方式优雅地去结束所有线程;→也就是说,每一个线程都是对等的特点2:线程任务可以由另一个线程任务调起;甚至线程池正在被销毁时也可以提交下一个任务;(这很重要,因为线程本身很可能是不知道线程池的状态的;→即,每一个任务也是对等的特点3:同理,线程任务也可以销毁这个线程池;(非常完整~→每一种行为也是对等的,包括destroy
我真的迫不及待为大家深层解读一下,这个我愿称之为“逻辑完备”的线程池。
1-前置知识
第一部分我先从最基本的内容梳理一些个人理解,有基础的小伙伴可以直接跳过。如果有不准确的地方,欢迎大家指正交流~
为什么需要线程池?(其实思路不仅对线程池,对任何有限资源的调度管理都是类似的)
我们知道,通过pthread或者std::thread创建线程,就可以实现多线程并发执行我们的代码。
但是CPU的核数是固定的,所以真正并行执行的最大值也是固定的,过多的线程除了频繁创建产生overhead以外,还会导致对系统资源进行争抢,这些都是不必要的浪费。
因此我们可以管理有限个线程,循环且合理地利用它们。
那么线程池一般包含哪些内容呢?
首先是管理若干个工具人线程;其次是管理交给线程去执行的任务,这个一般会有一个队列;再然后线程之间需要一些同步机制,比如mutex、condition等;最后就是各线程池实现上自身需要的其他内容了;
接下来我们看看Workflow的thrdpool是怎么做的。
2-代码概览
以下共7步常用思路,足以让我们把代码飞快过一遍。
第1步:先看头文件,有什么接口。
我们打开thrdpool.h,只需关注这三个:
//创建线程池\nthrdpool_t*thrdpool_create(size_tnthreads,size_tstacksize);\n//把任务交给线程池的入口\nintthrdpool_schedule(conststructthrdpool_task*task,thrdpool_t*pool);\n//销毁线程池\nvoidthrdpool_destroy(void(*pending)(conststructthrdpool_task*),thrdpool_t*pool);
第2步:接口上有什么数据结构。
即,我们如何描述一个交给线程池的任务。
structthrdpool_task\n{\nvoid(*routine)(void*);//函数指针\nvoid*context;//上下文\n};
第3步:再看实现.c,内部数据结构。
struct__thrdpool\n{\nstructlist_headtask_queue;//任务队列\nsize_tnthreads;//线程个数\nsize_tstacksize;//构造线程时的参数\npthread_ttid;//运行期间记录的是个zero值\npthread_mutex_tmutex;\npthread_cond_tcond;\npthread_key_tkey;\npthread_cond_t*terminate;\n};
没有一个多余,每一个成员都很到位:
tid:线程id,整个线程池只有一个,它不会奇怪地去记录任何一个线程的id,这样就不完美了,它平时运行的时候是空值,退出的时候,它是用来实现链式等待的关键。mutex和cond是常见的线程间同步的工具,其中这个cond是用来给生产者和消费者去操作任务队列用的。key:是线程池的key,然后会赋予给每个由线程池创建的线程作为他们的threadlocal,用于区分这个线程是否是线程池创建的。一个pthread_cond_t*terminate,这有两个用途:不仅是退出时的标记位,而且还是调用退出的那个人要等待的condition。
以上各个成员的用途,好像说了,又好像没说,是因为几乎每一个成员都值得深挖一下,所以我们记住它们,后面看代码的时候就会豁然开朗!
第4步:接口都调用了什么核心函数。
thrdpool_t*thrdpool_create(size_tnthreads,size_tstacksize)\n{\nthrdpool_t*pool;\nret=pthread_key_create(&pool->key,NULL);\nif(ret==0)\n{\n//去掉了其他代码,但是注意到刚才的tid和terminate的赋值\nmemset(&pool->tid,0,sizeof(pthread_t));\npool->terminate=NULL;\nif(__thrdpool_create_threads(nthreads,pool)>=0)\nreturnpool;\n…
这里可以看到__thrdpool_create_threads()里边最关键的,就是循环创建nthreads个线程。
while(pool->nthreads<nthreads)\n{\nret=pthread_create(&tid,&attr,__thrdpool_routine,pool);\n…
第5步:略读核心函数的功能。
所以我们在上一步知道了,每个线程执行的是__thrdpool_routine()不难想象,它会不停从队列拿任务出来执行:
staticvoid*__thrdpool_routine(void*arg)\n{\n…\nwhile(1)\n{\n//1.从队列里拿一个任务出来,没有就等待\npthread_mutex_lock(&pool->mutex);\nwhile(!pool->terminate&&list_empty(&pool->task_queue))\npthread_cond_wait(&pool->cond,&pool->mutex);\n//2.线程池结束的标志位,记住它,先跳过\nif(pool->terminate)\nbreak;\n\n//3.如果能走到这里,恭喜你,拿到了任务~\nentry=list_entry(*pos,struct__thrdpool_task_entry,list);\nlist_del(*pos);\n//4.先解锁\npthread_mutex_unlock(&pool->mutex);\n\ntask_routine=entry->task.routine;\ntask_context=entry->task.context;\nfree(entry);\n//5.再执行\ntask_routine(task_context);\n\n//6.这里也先记住它,意思是线程池里的线程可以销毁线程池\nif(pool->nthreads==0)\n{\n/*Threadpoolwasdestroyedbythetask.*/\nfree(pool);\nreturnNULL;\n}\n}\n…//后面还有魔法,留下一章解读~~~\n\n
第6步:把函数之间的关系联系起来。
刚才看到的__thrdpool_routine()就是线程的核心函数了,它可以和谁关联起来呢?可以和接口thrdpool_schedule()关联上
我们说过,线程池上有个队列管理任务:
每个执行routine的线程,都是消费者每个发起schedule的线程,都是生产者
我们已经看过消费者了,来看看生产者的代码:
inlinevoid__thrdpool_schedule(conststructthrdpool_task*task,void*buf,\nthrdpool_t*pool)\n{\nstruct__thrdpool_task_entry*entry=(struct__thrdpool_task_entry*)buf;\n\nentry->task=*task;\npthread_mutex_lock(&pool->mutex);\n//添加到队列里\nlist_add_tail(&entry->list,&pool->task_queue);\n//叫醒在等待的线程\npthread_cond_signal(&pool->cond);\npthread_mutex_unlock(&pool->mutex);\n}\n\n
说到这里,特点2就非常清晰了:开篇说的特点2是说,”线程任务可以由另一个线程任务调起”。
只要对队列的管理做得好,显然消费者所执行的函数里也可以生产
第7步:看其他情况的处理
对于线程池来说就是比如销毁的情况。
接口thrdpool_destroy()实现非常简单:
voidthrdpool_destroy(void(*pending)(conststructthrdpool_task*),\nthrdpool_t*pool)\n{\n…\n//1.内部会设置pool->terminate,并叫醒所有等在队列拿任务的线程\n__thrdpool_terminate(in_pool,pool);\n\n//2.把队列里还没有执行的任务都拿出来,通过pending返回给用户\nlist_for_each_safe(pos,tmp,&pool->task_queue)\n{\nentry=list_entry(pos,struct__thrdpool_task_entry,list);\nlist_del(pos);\nif(pending)\npending(&entry->task);\n…//后面就是销毁各种内存,同样有魔法~\n\n
在退出的时候,我们那些已经提交但是还没有被执行的任务是绝对不能就这么扔掉了的,于是我们可以传入一个pending()函数,上层可以做自己的回收、回调、或任何保证上层逻辑完备的事情。
设计的完整性,无处不在。
接下来我们就可以跟着我们的核心问题,针对性地看看每个特点都是怎么实现的。
相关视频推荐
150行代码,带你手写线程池,自行准备linux环境
BAT面试必备:多线程、多进程、协程如何选择及线程池如何最高效
学习地址:C/C++Linux服务器开发/后台架构师【零声教育】-学习视频教程-腾讯课堂
需要C/C++Linux服务器架构师学习资料加qun812855908获取(资料包括C/C++,Linux,golang技术,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,ffmpeg等),免费分享
3-特点1:一个等待一个的优雅退出
这里提出一个问题:线程池要退出,如何结束所有线程?
一般线程池的实现都是需要记录下所有的线程id,或者thread对象,以便于我们去用join方法等待它们结束。
不严格地用join收拾干净会有什么问题?最直观的,模块退出时很可能会报内存泄漏
但是我们刚才看,pool里并没有记录所有的tid呀?正如开篇说的,pool上只有一个tid,而且还是个空的值。
而特点1正是Workflowthrdpool的答案:
无需记录所有线程,我可以让线程挨个自动退出、且一个等待下一个,最终达到调用完thrdpool_destroy()后内存回收干净的目的。
这里先给一个简单的图,假设发起destroy的人是main线程,我们如何做到一个等一个退出:
外部线程,比如main,发起destroy
步骤如下:
线程的退出,由thrdpool_destroy()设置pool->terminate开始。我们每个线程,在while(1)里会第一时间发现terminate,线程池要退出了,然后会break出这个while循环。注意这个时候,还持有着mutex锁,我们拿出pool上唯一的那个tid,放到我的临时变量,我会根据拿出来的值做不同的处理。且我会把我自己的tid放上去,然后再解mutex锁。那么很显然,第一个从pool上拿tid的人,会发现这是个0值,就可以直接结束了,不用负责等待任何其他人,但我在完全结束之前需要有人负责等待我的结束,所以我会把我的id放上去。而如果发现自己从pool里拿到的tid不是0值,说明我要负责join上一个人,并且把我的tid放上去,让下一个人负责我。最后的那个人,是那个发现pool->nthreads为0的人,那么我就可以通过这个terminate(它本身是个condition)去通知发起destroy的人。最后发起者就可以退了。
是不是非常有意思!!!非常优雅的做法!!!
所以我们会发现,其实大家不太需要知道太多信息,只需要知道我要负责的上一个人。
当然每一步都是非常严谨的,结合刚才跳过的第一段魔法感受一下:
staticvoid*__thrdpool_routine(void*arg)\n{\nwhile(1)\n{//1.注意这里还持有锁\npthread_mutex_lock(&pool->mutex);\n…//等着队列拿任务出来\n//2.这既是标识位,也是发起销毁的那个人所等待的condition\nif(pool->terminate)\nbreak;\n…//执行拿到的任务\n}\n\n/*Onethreadjoinsanother.Don&34;task-%llustart.\\n&34;pendingtask-%llu.\\n”,reinterpret_cast<unsignedlonglong>(task->context););\n}\n\nintmain()\n{\nthrdpool_t*thrd_pool=thrdpool_create(3,1024);//创建\nstructthrdpool_tasktask;\nunsignedlonglongi;\n\nfor(i=0;i<5;i++)\n{\ntask.routine=&my_routine;\ntask.context=reinterpret_cast<void*>(i);\nthrdpool_schedule(&task,thrd_pool);//调用\n}\ngetchar();//卡住主线程,按回车继续\nthrdpool_destroy(&my_pending,thrd_pool);//结束\nreturn0;\n}\n\n
再打印几行log,直接编译就可以跑起来:
?妈妈再也不用担心我的C语言作业
简单程度堪比大一上学期C语言作业。
7-并发与结构之美
最后谈谈感受。
看完之后我很后悔为什么没有早点看为什么不早点就可以获得知识的感觉,并且在浅层看懂之际,我知道自己肯定没有完全理解到里边的精髓,毕竟我不能深刻地理解到设计者当时对并发的构思和模型上的选择。
我只能说,没有十多年顶级的系统调用和并发编程的功底写不出这样的代码,没有极致的审美与对品控的偏执也写不出这样的代码。
并发编程有很多说道,就正如退出这个这么简单的事情,想要做到退出时回收干净却很难。如果说你写业务逻辑自己管线程,退出什么的sleep(1)都无所谓,但如果说做框架的人不能把自己的框架做得完美无暇逻辑自洽就难免让人感觉差点意思。
而这个thrdpool,它作为一个线程池,是如此的逻辑完备,用最对称简洁的方式去面对复杂的并发。
再次让我深深地感到震撼:我们身边那些原始的、底层的、基础的代码,还有很多新思路,还可以写得如此美。
Workflow项目源码地址:https://github.com/sogou/workflow
关于多语言视频网站源码分享的内容到此结束,希望对大家有所帮助。