百科问答小站 logo
百科问答小站 font logo



如何评价「线程的本质就是一个正在运行的函数」? 第1页

     

user avatar   s.invalid 网友的相关建议: 
      

基本不沾边——如果说满分100的话,这个说法看在字数挺多份上能拿0.5分。再多就有舞弊嫌疑。


想明白线程是什么,必须先明白进程是什么——课本上那句“进程是程序的一次运行”可是不够的。


用“标准比喻”说,程序就是能放进图灵机执行的一条“纸带”——存硬盘上就是个若干K或者若干M的字符串——然后图灵机有一个读写头,可以按顺序读入纸带内容、或者在纸带上按照程序指示前后移动。


比如,“纸带”内容可以是“#!bash echo this is a program if ( cond ) then xxx else yyy end for () ...”,然后读写头从#!开始读入、执行,遇到if就跳到纸带指定位置,遇到for就在纸带上反复循环……


我们把“一条纸带以及正在纸带上来来回回忙活的读写头”叫做“一个进程”——很好理解,进行中的程序,对吧。


明白了什么是进程,那么线程就好理解了:我们可以在一台图灵机上装两个以上的读写头;当多个读写头同时分头读多个纸带、但每条纸带只有一个读写头忙碌时,这就是多进程。

类似的,当允许一条纸带上面有多个读写头同时读写时,这就是多线程。


当然,我们知道,“图灵完备”的图灵机的本质,就是“可以模拟其他所有图灵机的图灵机”——所以,哪怕某台“图灵完备”的机器只有一个读写头,它也可以模拟多个读写头的图灵机。

当然,这个就偏题了,暂不讨论。


总之,一旦明白了“多线程的本质是一条程序纸带上面多个读写头同时读写”,那么我们立即就会知道:多个读写头同时读写一块区域是可能出乱子的。


比如,有些数据是前后相关的。比如“张三 男 家庭住址XXX”,如果读写头1正在更改张三住址时,读写头2却把张三改成了李四然后读写头3说张三性别应该是女之前登记错了……哎呀你们这么挤我先等等,你们忙完我就把性别改成女……

那么,当这仨读写头折腾完,这数据自然就乱了——术语叫脏读/脏写。


为了防止脏读/脏写,我们就得玩锁、继而是信号量、旗语……然后又是死锁、忙等以及调度公平性会不会饿死等等——每个侧面的问题,那都是几本书写不完……

而且,现实中的“读写头”并没有那么简单。比如,它有cache,所以有cache时效问题;每个读写头都有自己的数据寄存器但又需要同时管理同一块内存,所以有数据同步问题……


所以,你看,说“线程的本质就是一个正在运行的函数”完全不沾边,没有丝毫夸张吧?

简单说,这样理解线程的人,他就没资格写任何多线程代码——不然他随时会给其他同事埋颗地雷。


评论区那个……


唉,还是那句话:你不懂要紧,甚至你哪怕不想学都可以——不懂线程并不耽误你增删改查。

但是,绝对不要往脑子里装错误的东西。

一旦装了,连增删改查都不放心你去做。


为什么?

知之为知之,不知为不知,是知也。

当你只有增删改查的能耐时,其实你的能力的最重要组成部分恰恰是——你知道自己什么都不知道。

知道自己不知道,那么你就不会乱来,就不会在项目中引入神奇的bug。


相反,一旦你不懂装懂、甚至欺骗了自己;那么你就放弃了你的基本能力的80%甚至90%——从“会增删改查”退步到“连增删改查都能搞错”了。


来,告诉我这都是什么:

       void give_a_fun(void (*p)(void *));  class inherit_me_show_u_sth_cool {    public virtual void run()=0; //other ... }      


估计有一些大佬会很生气:你又装逼!搞的花里胡哨一堆鬼画符,有意思吗?

而另一些大佬会很得意:这不就是c/c++风格的“begin_thread”和Java风格的“自thread类继承然后改写run方法”吗?你想吓唬谁?


嗯……没错,的确,beginthread的确是这样声明的:

_beginthread、_beginthreadex | Microsoft Docs

       uintptr_t _beginthread( // NATIVE CODE    void( __cdecl *start_address )( void * ),    unsigned stack_size,    void *arglist ); uintptr_t _beginthread( // MANAGED CODE    void( __clrcall *start_address )( void * ),    unsigned stack_size,    void *arglist ); uintptr_t _beginthreadex( // NATIVE CODE    void *security,    unsigned stack_size,    unsigned ( __stdcall *start_address )( void * ),    void *arglist,    unsigned initflag,    unsigned *thrdaddr ); uintptr_t _beginthreadex( // MANAGED CODE    void *security,    unsigned stack_size,    unsigned ( __clrcall *start_address )( void * ),    void *arglist,    unsigned initflag,    unsigned *thrdaddr );      

Java或者某些c++库里面的Thread类也的确是类似第二种方法声明。


但是,同样声明格式的,难道不能是qsort那样简单的传一个比较函数指针(甚至重载了括号运算符的所谓的“仿函数”)吗?

或者,注册一个回调函数,当什么事情发生时自动调用它?

或者,这个类的目的是,你继承了它,然后传回框架,人家帮你管理你的代码——就好像很多unittest框架搞的测试用例/测试套支持一样……


你看,目的千变万化;但有一样:虽然看起来都和beginthread调用或者Thread基类一样,然而它们都和线程没什么关系。


那好,请问各位望文生义的大佬:把一个函数指针传给beginthread,究竟和传给qsort有什么区别?

为什么都是“函数的一次运行”,前者是线程而后者不是


更进一步的,我们知道,Linux有个神奇的系统调用叫fork——你一旦调用它,你的进程就此分裂成了两个进程!

另一种说法是,fork是一个“调用一次返回两次”的神奇函数,一次返回在主进程,另一次在子进程……

那么,你可能不知道,现在的fork其实和能够创建线程的clone系统调用一样,最终都调用了do_fork——换句话说,如果你愿意,那么完全可以实现一个fork一样的、在某个函数中间的某个位置神奇的一分为二的特殊线程!

那么,请问,这样搞出来的线程,它又运行了哪个函数?



回答不了这些问题、却又确信“线程是一个正在运行的函数”——恕我直言,如果只会增删改查的你价值4000块钱一个月,那么现在的你一个月至多值800。


为什么现在你不值钱了?


我在十几年前和人合作过一个项目。

那时候线程刚刚兴起;为了方便使用,项目使用的一种脚本语言做了个很漂亮的封装:它把任务分成两类,一类UI相关,另一类是功能实现。

UI相关的线程默认不给程序员用。他们只管写功能,人家自动决定怎么在UI上更新——你看,连线程存在都不需要你知道,这还能用错?


当时一位经验丰富的前辈负责这块。他需要把用户每天采集来的差不多两万个数据点显示在界面上——很简单,view.add_point()就好了。


然而还是出问题了。

什么问题呢?

数据量太大了。当时的机器没那么好,两万个点,全部更新到界面,默认是添加一个点更新一次……

哪怕能跑到每秒100帧,这也得100秒!


然后用户一看,界面上一群点点在乱跑。不是,我要提交数据,我要看图表,你搞这个干嘛?快快快,着急要呢……我点,我点,我点点点……

Windows尽职尽责——来,消息队列走起!

这么一搞,一次卡死半小时都是少的。


于是这位前辈急了,各种寻求各种脑洞……


总之吧,大约两三个月后,这份任务到了我手里。

一看,到处莫名其妙的代码。我梳理了大半天,把基本功能代码挑出来,也就一百来行;但人家为了让程序不卡,添加的乱七八糟的东西倒有几百行——而且改来改去改的基本逻辑都不对了。


前辈倒是很诚实:“线程这玩意儿我也不懂,就是觉得这里得用,但我怎么都玩不对。你看看,不行就重写。”


有他这句话,加上我知道原始需求,所以一开始就没受他干扰;不过他的代码改的实在太乱了,我干脆重写。

写完,展示:“更新前把界面刷新关了,数据全部添加之后再打开,大约两秒搞定。”

——“不太好吧,原来那个一个个点加进去的效果看起来很酷……现在快是快了,不好看了……”

“简单。现在我按指数增量添加——刚开始一个点一个点添加,然后关刷新,添加十个点再打开;如此添加500个点,改成每次添加100个点……你看,照样有一个个点右侧飞进的效果,刚开始点大,飞的慢;后面点点连成一条线的往里飞,越缩越小,越飞越快……”

“还有Windows消息累积问题,我在用户点了这个按钮后就灰掉它,等于告知Windows我不再接受消息;处理完再使能……”

——“好,好!这效果太好了!我看看……你写在哪?”

“就在按钮click事件里面。”

——“没有啊……你还把我的都删了……”

“就在里面。我只留了基本功能代码,另外有十来行处理显示效果和按钮可用性的……”

——“十来行?这效果十来行就行?”

“是啊。不到十行。”

——“不对啊……没有多线程啊?”

“不需要。框架搞的很好了,我们配合好框架的UI线程就足够了,没必要搞什么多线程。反而容易越搞越乱……”


没错。自始至终,我没碰线程——这种语言本来也没打算让自己的用户碰线程。

但你不真正理解线程是什么,你就不可能简单轻松十来行程序配合界面UI线程在屏幕上玩出花来。


——现在,你再想想,beginthread和qsort都接受一个函数指针,两者一样吗?本质究竟区别在哪里

——这个区别,重要不重要?

——连这个区别都不知道不关注,傻乎乎的一看beginthread接受一个函数,哦,线程就是运行的函数……然后一通神奇操作、把项目彻底搞砸——这个责任,除了你,还能给谁背?


不懂不要紧,知道自己不懂就没有危害;不懂,还要跳,那我只能强迫你一边歇着去。

鲁迅说的好,无端的浪费别人的时间无异于谋财害命。

把在你身上浪费的一秒钟拿过来,撸一把猫,踢一脚狗,不都更有意义吗。


user avatar   programus 网友的相关建议: 
      

这属于话糙理不糙系列。

从说话人的口吻来看,貌似是在给某人传授关于编程的知识和经验。

诚然,这段话本身是不严谨的,但对初学者来说,有益于入门多线程编程。

毕竟,你要把本质讲清楚了,一半的人吓跑了,剩下一半听睡着了,结果一个学会的都没有。

这就跟我们小学一开始只学自然数,老师会说1 - 3减不了、2 ÷ 3除不开一样,具有更多知识的人一看就知道不严谨,但对小学一年级入门数学殿堂是有帮助的。


这个人说这话的本意,我觉得是希望打消听话人对线程的恐惧,就把它当做一个可以同时执行的函数,赶紧动手试一试。一跑程序结果对了,自然会带来更多的自信,会让人继续深入学习下去,随着学习的深入,自然会知道线程的本质到底是什么。我见过太多一提到线程、进程、并行、C语言,还没说要干啥呢,就吓得扭头就跑的汉子了……对于这些人,有一个可以降低恐惧感的老师,还是很有帮助的,哪怕说得话不严谨。


所以,要问这种说法对不对,其实说话人本身估计也知道漏洞很多。然而,他的目的是让别人赶紧钻进线程学习里,而不是一定要保证自己每句话都是无懈可击的。所以,感觉倒也无伤大雅,没必要非要去杠。

我给别人培训的时候,就说过这样的话:我给你们讲东西,从来都不怕讲错了。因为,我讲错了,你以后用到了,只要认真做测试,就会发现错了,反而印象更深刻;我讲错了,你以后用不到,既然用不到,对错又有何妨呢?这就是我为什么讲计算机知识,不讲医学知识的缘故。


user avatar   Ivony 网友的相关建议: 
      

说这种话的才是外行吧,

因为函数这玩意儿,本来就是C语言带坏的。这货应该说是带参数的子过程,C语言不管子过程带不带参数都叫做函数,这本来就很离谱。


至于什么正在运行的函数这更离谱,搞得函数好像有个状态叫做运行一样……



与其说什么多线程和线程是用来迷惑外行的,倒不如说是用来筛选从业者的。毕竟这玩意儿都要什么理解本质,什么直观形象,你压根儿就不适合干这行才对……


user avatar   haozhi-yang-41 网友的相关建议: 
      

我看了一下现在的回答,似乎大都围绕着实现细节说了,少数在概念层面解释的,却都被绕进了“调度/时间片/并发”这个圈子里绕不出来了。然而,我必须指出,线程/进程在概念上,并不和这些概念捆绑——虽然大多数情况下,确实如此。


其实,线程/进程并不是一个真实存在的实体,是一个凭空抽象出来的逻辑概念。和所有凭空抽象出来的概念一样,它必然是为了某个目的而出现的,那这个目的,不是“并发/并行”,而是一个已经“濒临失传”的概念:状态机

回到一个最简单最基础的计算机环境,一个单核单U的平台,加电后就从0地址加载第一条指令开始执行一个程序。这时候,整个程序的指令和流程必然就实现了一部状态机,而这部状态机的核心,就是一张状态转移表。这个程序所执行的各种操作,实际上都是围绕着这张状态转移表来执行的。

然而,随着软件规模的逐步扩大,各种功能逐步加强,状态的数量会越来越多,这张状态转移表也就会越来越大,越来越复杂,以至于难以维护,各种逻辑bug频出。而这个时候,再仔细看那张非常复杂的状态转移表,往往会发现,对于某个特定状态而言,它所能转移的状态一般是有限的,也就是说大多数其它状态是和它无关的。那么,为了降低编程的复杂度,为了屏蔽某些处理中不相关细节,就需要利用这点,把这张庞大的状态转移表拆分(高相关性的状态单独成表)。而这些拆分出去的子表,实际上就构成了子状态机,执行它的程序,也就成了子程序,这就是:child process。至于线程……一回事。

所以,进程/线程概念的出现,本质上是一种屏蔽无关细节,降低程序设计复杂度和难度而出现的编程概念——至于怎么实现它,那是后话(事实上在上古年代,实现方式还真的是百花齐放奇葩迭出的)。

于是有了那句著名但比较偏激的话:

A Computer is a state machine. Threads are for people who can't program state machines.
---- Alan Cox
计算机就是状态机。线程是为不懂状态机的程序员准备的。

进程线程的概念说清楚了,那稍微扩展一点,说一下它为什么现在基本上和“并发”概念给绑定了吧:

在最早期的时候,计算机还普遍处在单核单U的年代,就算拆了多个子状态机,实际上也是无法并行的。那么怎么在不同的子状态机之间进行切换呢?一种传统的办法是在子状态机内部主动设定某些条件进行主动切换,这就是所谓的用户态线程。另外一种就是大家所熟知的:统一由os接管,为每个状态机执行调度——当然,os本身也是一个状态机,无非是调度状态机的状态机而已。

早年在单核单U的年代,用户态线程,也就是所谓的“伪并发”是编程的主流。因为这种切换有明确的目的和时机,所以成本开销最小(例如说不需要加锁),因此最适合当年硬件水平还不高的环境。实际上这种架构在现今一些硬件性能不高(非常低端的嵌入式)、延迟极度敏感(游戏引擎)、极度追求性能(用户态协议栈)的项目中还能看到。

后来,随着cpu硬件的发展,尤其是多U多核的出现,并行并发任务的需求开始井喷。这时候,再让每个应用程序都继续实现用户态切换就不现实了。所以,由os统一切换和管理的“内核态线程”开始流行,于是线程/进程的概念就和“os/并发/时间片/上下文”等相关概念给深度捆绑了。

到现在,毕竟内核没有办法深度了解子程序内部逻辑,所以它的调度必然是无序而且粗暴的——所以它必须设定“时间片/优先级”之类的概念,而且每次切换都必须完整保留上下文(因为它不知道那些有用哪些没用)。同时,为了对付无序的切换,各种额外的开销(并发编程/锁)也必然很大。所以,为了降低这些开销,上古年代的用户态伪并发玩法被重新拿出来,封装了一下之后,没再用线程/进程这个名字,而是给了一个新鲜的名字:协程。


user avatar   DBinary 网友的相关建议: 
      

如果我的学生问我线程是什么,我的回答基本也会和「线程的本质就是一个正在运行的函数」差不多

因为这是最容易让初学者理解并能够帮助他们应用到实践中的方式。对于我来说,我施教的目的达到了。

但如果在知乎上这么说,就经常就会有一些写了几年代码的“大神”们跳出来,指正我的不对,然后举出一堆专业术语来告诉我线程不是那么一回事。同时给我扣个误人子弟,不懂装懂之类的帽子。这让我很难受。

所以回答这种问题,我还是真的挺纠结的,一来我实在不想一上来就直接就扯什么调度器,时间切片,上下文切换,信号量之类的玩意,二来我又想把这个知识点科普出去,还得让“大神”们不至于太鄙视我,思来想去干脆完成实现一个“多线程”程序。来演示“线程”这一个概念是如何运作的----准确来说,我的意思是实现一个编译器,将代码中的函数编译为特定指令流,然后在此基础上实现一个虚拟机执行环境,执行该指令流并设计一个基于指令计数切片的调度器,完成这个线程的调度工作。然后我们进一步科普线程间的协同关系并引入更多的装逼术语。当然我们的多线程的实现机制是纯算法实现且平台无关的,你不用纠结一些平台相关的额外的设计模式把你绕的云里雾里最后问一句:“听上去感觉听屌的,但线程到底是个啥”这种疑问,同时这种设计思想也许因为平台或硬件支持关系有所不同,但核心思想在多数的平台上大同小异,所以你也不用纠结我这个是不是实现的不够高大上。如果你听不明白上面说的是什么东西没关系,下面的内容,通俗易懂,老少咸宜。

我们先来写一段代码

       #name "main" #include "stdlib.h" #runtime thread 8  //线程1的函数 export void thread1() {   while(1)   {     print("I'm thread 1");      sleep(1000);   } }  //线程2的函数 export void thread2() {    while(1)   {     print("I'm thread 2");      sleep(1000);   } }  //线程3的函数 export void thread3() {    while(1)   {     print("I'm thread 3");      sleep(1000);   } }  //主线程 export int main() {   CreateThread("thread1");//开始线程1   CreateThread("thread2");//开始线程2   CreateThread("thread3");//开始线程3   while(1) sleep(1000);   return 0; }     

上面的代码,应该只要稍微懂一点C语言(虽然它不是),都很容易看懂,首先我们实现了3个线程函数,作用非常简单,就是每隔1秒print一段文本,然后一直循环下去.

因此,我们直观的理解就是,这三个函数是同时运行的,那么在屏幕上,我们大致会看到下面这种结果

那么这个是怎么实现的呢,首先,我们的函数代码会被编译为中间指令,一种类似于汇编语言的结构,可以这么说,函数里的代码,最终会编译为这种汇编指令结构,至于为什么呢,我们可以这么想,如果让CPU直接理解代码里的表达式,可能会让电路设计变得非常非常的复杂,所以呢,我们把表达式的内容这个大问题拆分成小问题,把小问题一步一步解决了,大问题就解决了,就像你计算1+2x3,我们先计算2x3=6,然后1+6=7,一步一步走,也就是这个意思

如果能理解到这一步,事情就变得很简单了,多线程怎么实现的呢,非常非常的简单,就拿我们上面这个例子来说,我们先执行线程1函数的第一条指令,比如上面的第9条指令,然后我们跳到线程2的第一条指令,也就是第33条指令..然后我们又回到线程1函数执行它的第二条指令,就是第10行指令,再到线程2函数执行它的第二条指令,就是第34条指令......一直执行下去,直到执行到这个函数的ret,也就是函数结束的返回,因为计算机的执行速度很快,所以最终看起来,这些函数就像同时运行的一样

实际上简单来说就是函数1的工作先做一点,然后跑去函数2的做一点,再去函数3的做一点....通通都只做一点点,直到事情做完为止,让人感觉我们同时在做很多事情一样.

如果我们只讨论算法方面的实现而不考虑例如一些硬件辅助,好了,这就是线程的本质,剩下的,就是纠结一些细节问题,然后发明出一堆听上去高大上的术语让别人觉得我们很牛逼了.

什么是上下文切换

我们先来直面第一个问题,要完成这种切换工作,我们需要什么?

最简单的,既然我们每个函数都做了一点工作,那么我们是不是应该拿个小本本记录一下,不同的线程分别执行到哪了,比如线程1,我们执行了2条指令,这个时候,我们就要记录:恩,线程1已经做完2条指令了,下一次要从第三条指令开始执行.这样我们下一次回到线程1时,我们就知道我们之前已经做完哪些工作了,下一次应该从哪里继续开始

那么,这个记录这个信息的东西,就叫做线程的上下文,我不知道为什么会翻译为上下文这种那么拗口的中文名,如果叫"运行状态",绝对比上下文好理解,而传说中的那个上下文切换,实际上就是当你运行了线程1的指令--->保存状态-->读取线程2上一次的运行状态--->运行线程2这一个简单的过程.

为什么要栈帧

好了,现在让我们来思考另一个问题,你想啊,既然线程1和线程2外面看起来就是两个相互独立同时运行的函数,那么是不是说,它们应该有各自独立的内存来保存自己计算过程的中间结果,这块内存区域一般放在栈中,这也就是为什么常常每个线程有自己独立的一个栈帧,当然,这块内存到底在哪了,长度有多大,一般也是在上下文中保存的

什么是时间片

这个时候,你发现了一个问题,如果线程1每次只执行1条指令,然后就跑去下一个线程执行下一个指令,显然是非常不经济实惠的,因为这种切换也会带来性能开销,你不能说为了看起来像做多件事,结果大部分时候不是在做事情,而是在各个线程中来回跑,这就非常划不来了,所以你这样这么来,定一个时间,比如线程1执行个10毫秒,然后线程2执行个10毫秒,这样就不会显得疲于奔命,这个就叫时间片,但时间片一定是按时间来的么,不一定,你也可以按指令数来,比如线程1执行个100条指令,线程2执行100条...以此类推也一样,当然两种做法各有优劣,总之时间片一个简单的概述就是在某个线程做多少/多久的事情这一个简单的概念

什么是信号量/锁

比如一个工作,要等线程1和线程2都做完了才能接着往下做,或者说线程1要等线程2做完了才能继续往下做,怎么办呢,简单啊,我们定义一个变量,初始值为0来说,线程2做完了,就把它的值设置为1(或者通知其它线程检查这个变量),而线程1呢,它会检查这个变量,如果说如果它看到这个值是0,它就跑去睡大觉了(线程挂起)直到有人通知他它才醒来再检查一次,那么这个过程就叫信号量,如果它不断检查这个变量而不去睡大觉直到它变为1,那么这个过程叫锁(自旋锁),当然基于这点还可以拓展实现出一些临界区,互斥量之类的叫法,然而换汤不换药,本质上这个打tag然后检查的这个过程实现并没有太多变动.

什么是原子操作

这堆代码没有执行完之前,不!准!进!行!上!下!文!切!换!,哪怕已经没有时间片了

碰到阻塞函数怎么办

比如上面的sleep函数,比如你想从硬盘读数据这种可能花的时间比较久的函数,或者说你在等待网络有一个数据包过来,常常是一些IO类的函数,执行到这个函数,即使时间片还没有用完也不要瞎等了,直接上下文切换该干什么干什么不要浪费CPU的人生.

那什么是调度器

好了,实现可能包括但不限于这些功能,然后把它们拼在一块的玩意,就叫调度器.

现在我们基本讲完一个线程的大部分内容了

文章的最后,如果你真对这个线程实现感兴趣

这里有编译器到带有线程调度功能的虚拟机的完整C语言实现

最后,学习过程中真的应该珍惜有这种愿意用最好懂的方式或语言给你讲解知识点的朋友或老师,如果一个人明知道他说的这些你听不懂,而仍然坚持要跟你这么说,那只能说明他的目的并不是想教会你,而是想告诉你我有多凡尔赛多牛逼.当然我也没有贬低谁的意思,毕竟这种事情你知我知,大家都爱干.


user avatar   intopass 网友的相关建议: 
      

函数恰恰是被线程执行的标的。

线程拥有着函数执行所需的资源。

简单的说,两者并不是一回事。


user avatar   ling-jian-94 网友的相关建议: 
      

多线程的难度又不在理解 你理解了,so what?就知道怎么写程序不会死锁、不会性能降级了吗?我也算写很多年多线程程序了,上周刚写出个64核比32核还慢的OpenMP程序……


user avatar   rabbit-ch2 网友的相关建议: 
      

先问一个问题,有没有办法把一个线程保存到硬盘的.img 二进制文件中,想让它运行的时候再读出来让它继续跑?听起来是不是很像《赛博朋克 2077》里的“灵魂杀手”以及 Relic 芯片?还真有这么一个东西:

把 Linux 界的“灵魂杀手”装上:

       # centos yum install criu # ubuntu apt-get install criu     

我们假定一个进程里只有一个主线程,该线程为 thread leader。考虑到一个进程运行时的pid不一定会在恢复的时候可以重新申请的到,我们用一个工具 newns 借助 pid namespace 将此进程设置在自己的 namespace 中为 init process,也就是 pid 为 1(自身视角)

这里插一句,在 Linux 上进程与线程本质没有什么区别,都是 clone产生的,我们给 clone 传一个CLONENEWPID就可以让开启的进程每次 get_pid 都能得到自身 pid 为 1
       #define _GNU_SOURCE #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <errno.h> #include <sys/mount.h> #include <sys/types.h> #include <sys/stat.h> #include <sys/wait.h> #include <sys/param.h> #include <sys/mman.h> #include <fcntl.h> #include <signal.h> #include <sched.h>  #define STACK_SIZE (8 * 4096)  static int ac; static char **av; static int ns_exec(void *_arg) {         close(0);  setsid();  execvp(av[1], av + 1);  return 1; }  int main(int argc, char **argv) {  void *stack;  pid_t pid;   ac = argc;  av = argv;   stack = mmap(NULL, STACK_SIZE, PROT_WRITE | PROT_READ,     MAP_PRIVATE | MAP_GROWSDOWN | MAP_ANONYMOUS, -1, 0);  clone(ns_exec, stack + STACK_SIZE,    CLONE_NEWPID | CLONE_NEWIPC | SIGCHLD, NULL);  return 0; }     

编译为工具 newns:

       gcc -o newns newns.c     

然后编写一个 sample app,就以一个每秒打印日期的 bash 脚本为例:

       #!/bin/sh while :; do     sleep 1     date done     

下面我们跑一个线程:

       [root@VM-8-5-centos thread]# ./newns bash test.sh [root@VM-8-5-centos thread]# 2021年 07月 06日 星期二 19:55:53 CST 2021年 07月 06日 星期二 19:55:54 CST 2021年 07月 06日 星期二 19:55:55 CST 2021年 07月 06日 星期二 19:55:56 CST 2021年 07月 06日 星期二 19:55:57 CST     

然后用 criu 把它做成 checkpoint:

       ps -ef| grep 'bash test.sh'  | head -1 | awk '{print $2}' | xargs -I PID criu dump -t PID --shell-job  --images-dir /home/thread/checkpoint     

然后再把它 restore 回去:

       [root@VM-8-5-centos thread]# cd checkpoint/ [root@VM-8-5-centos checkpoint]# ls core-14.img   fdinfo-3.img  fs-24.img   inventory.img    mm-24.img       pages-1.img  stats-dump core-1.img    files.img     ids-14.img  ipcns-var-9.img  pagemap-14.img  pages-2.img  tty-info.img core-24.img   fs-14.img     ids-1.img   mm-14.img        pagemap-1.img   pstree.img fdinfo-2.img  fs-1.img      ids-24.img  mm-1.img         pagemap-24.img  seccomp.img [root@VM-8-5-centos checkpoint]# criu restore --images-dir /home/thread/checkpoint --shell-job & [1] 17235 [root@VM-8-5-centos checkpoint]#  2021年 07月 06日 星期二 19:56:26 CST 2021年 07月 06日 星期二 19:56:27 CST 2021年 07月 06日 星期二 19:56:28 CST 2021年 07月 06日 星期二 19:56:29 CST 2021年 07月 06日 星期二 19:56:30 CST     

神奇的事情发生了,世界上真的有 “Relic 芯片”,这个线程像强尼银手一样“复活”了

事实上这个线程并不完全是以前的那一个,与 Relic 芯片原理一样,criu 会选一个宿主,然后侵占宿主的身体,将其替换为之前的线程的“意识”。改造完成到一定地步,就可以认为这是以前的那个他了

那么我们看一看硬盘上的“神谕”里保存了一开始那个线程的哪些数据

文件名 说明
core-1.img 1号进程(同线程)的task_struct核心数据
files.img 打开的文件
mm-1.img 虚拟内存表
pagemap-1.img 页目录
pages-1.img 内存页数据
fdinfo-1.img 文件描述符
pstree.img 进程树

只捡了几个主要的,先来看 core-1.img 里是啥:

       [root@VM-8-5-centos thread]# crit decode -i /home/thread/checkpoint/core-1.img --pretty >> /home/thread/checkpoint/core-1.json     

主要是寄存器的数据,代表了一瞬间的状态

然后看看 files 里是啥:

是打开的文件的路径以及对应的文件描述符 id,再看 mm 里是什么:

代码段、数据段、各种段在虚拟内存里的地址,然后看看 pagemap:

哪个虚拟地址有几页需要从 pages 里读,再看看 pstree:

很好理解,1 个父进程+1个子进程,它们各有一个主线程

好了,看完了,问题回到了线程的本质是什么

答案是:线程称不上本质是函数,函数是简单的,线程是复杂的。函数只是代码段里数据,但是线程牵涉的远远不止是代码段。为了运行一个线程,系统还需要打开一堆的文件描述符、分配一段内存数据段+栈段给它,同时寄存器的状态都需要额外的内存来记录,表面的函数只是冰山一角


user avatar   xi-yang-86-73 网友的相关建议: 
      

瞎扯淡。线程本质是共享一部分资源的CPU调度单位。


user avatar   mai-cui-ya-96 网友的相关建议: 
      

一个线程不好理解,有两个就好理解了。

郭德纲:嫂子刚生完孩子,于老师住院了。嫂子一会儿给孩子喂喂奶,一会儿给于老师喂喂药……喂喂奶、喂喂药、喂喂奶、喂喂药……




     

相关话题

  linux的TCP连接数量最大不能超过65535个,那服务器是如何应对百万千万的并发的? 
  硅谷的中国超能力者(软件工程师)圈是怎样一种存在? 
  你遇到过哪些代码优雅的C#项目? 
  程序员这几年吃了很大一块蛋糕,但随着互联网技术的深耕,大多数程序员的技术将变得廉价,未来何去何从? 
  给RISC-V设计自定义指令,有什么需要注意的事情吗? 
  西安电信一码通项目此前报道中提到「两天两夜把 1m 图片优化到100kb」,图像压缩技术难度是怎样的? 
  tkinter可以做出多复杂的界面? 
  你看过/写过哪些有意思的代码? 
  程序员是不是公司地位最低的? 
  在中电(中国电子科技集团)XX所工作的感受是怎样的? 

前一个讨论
2021 有什么神 CPU 三年不用换?
下一个讨论
为什么现在手机 256G 越来越不够用了?有哪些内存管理的技巧?





© 2024-06-03 - tinynew.org. All Rights Reserved.
© 2024-06-03 - tinynew.org. 保留所有权利