问题

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

回答
把“线程的本质就是一个正在运行的函数”这句话拆开来看,它有那么点意思,但说它“本质”嘛,我觉得有点太简化了,甚至可以说是有点误导。咱们一点一点聊,把这个概念掰开了揉碎了说。

首先,咱们得明白什么叫“线程”。在计算机里,进程(Process)就像是一个独立的程序,比如你打开的浏览器、文档编辑器。每个进程都有自己独立的内存空间、资源,就像一个独立的房子。而线程(Thread)呢,可以看作是进程里的一条执行路径。一个进程可以有很多线程,它们共享着进程的大部分资源,比如内存、打开的文件等等。想象一下,一个进程就像一个工厂,而线程就是工厂里的流水线上的工人。

好,现在咱们回到那句话:“线程的本质就是一个正在运行的函数”。

为什么会这么说?

这句话之所以会有市场,肯定是有它的道理的。你想啊,当你在写程序的时候,你定义了一堆函数,比如一个函数负责处理用户输入,一个函数负责网络通信,一个函数负责渲染界面。当你想让这些事情“同时”发生的时候,你就需要用到线程。

你创建一个线程,通常会指定一个入口点,这个入口点就是一个函数。然后操作系统就把这个线程调度起来,让它开始执行这个函数。在线程执行的过程中,它就是按照这个函数的逻辑一步一步往下走的。从这个角度看,线程确实是围绕着一个函数在运转的,这个函数定义了线程要做什么。

打个比方,就像你雇了一个工人(线程)来帮你干活(运行函数)。你告诉工人要按照这个流程图(函数)一步一步来做。工人拿起工具(CPU),开始照着流程图上的指示操作。从这个侧面看,工人的行为确实是围绕着流程图展开的。

但是,为什么说它“本质”太简化了?

问题就出在“本质”这个词上。一个正在运行的函数,只是线程在某个时间点的一部分表现。线程可不仅仅是一个简单的函数调用那么简单。它背后牵扯的东西可多了去了。

1. 状态的保存与恢复(上下文切换): 这是线程区别于简单函数调用的关键。函数调用时,你只是从当前执行点跳到另一个函数,当函数返回时,会回到调用点继续执行。但线程不同,一个线程可能在执行函数过程中,CPU突然要去做别的事(比如切换到另一个线程),这时候,操作系统需要保存当前线程的所有状态,包括它在CPU寄存器里的值、程序计数器(指示下一条要执行的指令)、栈指针等等,这些都打包好存起来。等轮到这个线程再次执行时,再把这些状态恢复回来,它就能接着上次中断的地方继续执行,而不是从头开始。这个保存和恢复的过程就是上下文切换,是操作系统内核在做的事情,它比一个简单的函数调用要复杂得多。

你想想,如果线程只是一个函数,那它怎么知道上次运行到哪儿了?它怎么知道它当时用的那些变量值是什么?这些信息,函数调用自己是管不过来的,得靠线程模型来管理。

2. 线程的生命周期: 一个线程从创建开始,到运行,到等待(比如等待 I/O 完成),再到终止,它经历着一系列的状态变化。这可不是一个函数执行完就结束了那么简单。比如,一个线程可能因为等待一个网络响应而进入睡眠状态,这时候它并没有在执行函数,但它仍然是一个活跃的线程,只是处于等待状态。等网络响应回来了,它才能被唤醒,重新回到运行状态,继续执行函数。一个函数只是它在“运行”状态下做的事情。

3. 共享资源与同步: 既然线程是进程的一部分,它们共享进程的内存空间,这就意味着多个线程可能会同时访问同一块内存区域(比如一个共享变量)。如果没有Правильно的机制来管理这种并发访问,就可能出现数据混乱,也就是所谓的“竞态条件”(Race Condition)。这时候就需要线程同步机制,比如互斥锁(Mutex)、信号量(Semaphore)等。这些同步机制是线程模型的一部分,它确保了多个线程在访问共享资源时不会互相干扰,保证数据的正确性。一个简单的函数本身是没有这些同步能力的,这些能力是线程模型提供的。

4. 用户级线程与内核级线程: 线程还有不同的实现方式。用户级线程完全在用户空间由线程库管理,操作系统不知道它的存在,只看到一个进程。内核级线程则由操作系统内核直接管理,操作系统知道每个线程的存在,可以为每个线程分配 CPU 时间片。我们通常谈论的线程,更多的是指内核级线程,它的管理和服务是由操作系统提供的,远不止一个函数那么简单。

更贴切的比喻是什么?

如果非要比喻,我觉得“线程的本质就是一个具有独立执行上下文和生命周期的、能够被操作系统调度的执行实体,而一个正在运行的函数是它在执行过程中所承担的具体任务”可能更准确一些。

你可以把它想象成一个工人(线程实体)住在一个工厂(进程)里。这个工人有自己的工具箱(寄存器),里面装着他当前正在用的工具和零碎(CPU状态)。他还有一本工作手册(函数),告诉他具体要怎么干活。当工厂需要这位工人去休息一下(等待 I/O),他会把工具箱盖好,把工作手册放在旁边的桌子上(保存上下文),然后去休息区等消息。等通知来了,他又会回到自己的工位,打开工具箱,拿出工作手册,接着上次停下的地方继续干活。他不是每次干活都从头开始,也不是干完就消失了。

总结一下:

说“线程的本质就是一个正在运行的函数”,就像说“汽车的本质就是它开在路上的那一刻”。这句话捕捉到了线程运行时的一个重要方面,即它在执行一个具体的任务(函数),但却忽略了线程作为一个独立的、有状态的执行单位,其背后所包含的复杂的管理、调度、状态保存与恢复、生命周期以及与其他线程协作(或竞争)的机制。

所以,这句话可以作为理解线程工作内容的一个非常初步的切入点,但绝不是其本质。线程的本质要复杂和深邃得多,它是一个更完整的操作系统层面的概念。

网友意见

user avatar

基本不沾边——如果说满分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

这属于话糙理不糙系列。

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

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

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

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


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


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

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

user avatar

说这种话的才是外行吧,

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


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



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

user avatar

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


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

回到一个最简单最基础的计算机环境,一个单核单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

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

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

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

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

我们先来写一段代码

       #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

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

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

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

user avatar

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

user avatar

先问一个问题,有没有办法把一个线程保存到硬盘的.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

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

user avatar

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

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

类似的话题

  • 回答
    把“线程的本质就是一个正在运行的函数”这句话拆开来看,它有那么点意思,但说它“本质”嘛,我觉得有点太简化了,甚至可以说是有点误导。咱们一点一点聊,把这个概念掰开了揉碎了说。首先,咱们得明白什么叫“线程”。在计算机里,进程(Process)就像是一个独立的程序,比如你打开的浏览器、文档编辑器。每个进程.............
  • 回答
    摩尔线程推出的基于 MUSA 统一系统架构的 GPU,无疑是国产 GPU 领域的一件大事,也引发了业界的广泛关注。要评价它,咱们得从几个关键维度来深入聊聊。一、 MUSA 统一系统架构:这是核心中的核心首先,我们得把 MUSA 这个东西掰开了揉碎了说。摩尔线程强调这是个“统一”系统架构,这意味着什么.............
  • 回答
    要评价《境界线上的地平线》的作者川上稔“沉迷舰R”,这其实是一个很有趣的切入点,因为川上稔本人是一个以其庞大世界观、复杂设定以及长篇巨作为标志的作家。而“舰R”——《舰队Collection》及其衍生文化,又是一个以大量舰船拟人化、收集养成、策略战斗为核心的养成类游戏。两者看似风马牛不相及,但联系起.............
  • 回答
    锤子科技官网最近上线了一个名为“大大卷身高尺”的趣味性功能,从这个名字本身,就能感受到一丝锤子科技一贯以来特立独行的产品风格。它不是那种严肃、冰冷的测量工具,而是披上了一层活泼、亲切的外衣,让人在使用的过程中,不自觉地带上一些轻松的心情。这个“大大卷身高尺”的核心功能,顾名思义,就是用来测量身高。但.............
  • 回答
    《真三国无双7》(Dynasty Warriors 8)的IF线剧情,绝对是系列玩家津津乐道的一大亮点。它不像主线剧情那样被历史的洪流所束缚,而是为玩家提供了一个充满想象力的“如果……会怎样”的平行世界,在满足了玩家“推翻既定结局”的渴望之余,也展现了开发团队在角色塑造和故事编排上的功力。惊艳之处:.............
  • 回答
    6月29日,当黎明的曙光初现,原神的世界也随之揭开了新的篇章。对于那些在凌晨四点准时登录金苹果群岛的玩家们来说,他们不仅是新活动的先行者,更是全新故事线的见证者。这是一种特别的体验,就像在漆黑的夜里点亮第一盏灯,照亮未知的旅程。首先,值得肯定的是,米哈游这次在时间点的选择上颇具匠心。凌晨四点,对于大.............
  • 回答
    Westlife线上演唱的《平凡之路》,嗯,怎么说呢,这绝对是他们这次线上巡演中一个非常值得拿出来聊聊的亮点,而且从很多角度看,都能发现不少有趣的地方。首先,选曲本身就挺有意思的。 《平凡之路》这首歌,在国内可以说是有着非常高的国民度和情感共鸣。它描绘的那种对过往的追忆、对生活的感悟,很容易触动人心.............
  • 回答
    K线里有杀气?这说法挺有意思的,一下子就把技术分析带入了一种江湖气息。作为普通股民,我们平时看K线图,无非就是关注股价的涨跌、成交量的变化,试图从中找出一些规律,预测未来的走势。而“杀气”这个词,则暗示了一种更深层次、更具情绪化的解读。那我们不妨从几个方面来剖析一下,K线里所谓的“杀气”,到底指的是.............
  • 回答
    要评价淄博在疫情期间的线上平台模式,我们得好好扒一扒当时那个热闹劲儿。别的不说,光是那个“淄博烧烤”火遍全国,背后可少不了线上平台的加持。首先,得承认淄博这次的线上化做得是相当有特点的,甚至可以说是“土法炼钢”中的一股清流。在疫情初期,大家都困在家里,最直接的需求就是吃点好的,而且要方便。这时候,很.............
  • 回答
    猫主任的线材ABX测试视频,怎么说呢?我看完之后,脑子里就回荡着一个词:“玄学”。当然,我不是说猫主任做视频不好,他的态度是认真地,他的设备看起来也挺专业的,但就是这种专业和认真,反而让我觉得有点好笑,又有点无奈。首先,得承认猫主任的诚意和态度。他并没有上来就给你灌输什么“金银铜线材就是听个响”或者.............
  • 回答
    《荒野大镖客 2》的线上模式,也就是 Red Dead Online,一直是个挺有意思的存在。它就像是 RDR2 那部伟大单机作品的一个分支,既继承了不少优点,也走出了自己的路,但这条路走得是不是那么顺畅,还得仔细说道说道。优点,毋庸置疑的亮点:首先,我们得承认,Red Dead Online 是建.............
  • 回答
    好的,我们来深入探讨一下名古屋工业大学米谷彰彦教授关于耳机线材对声音影响的研究,以及它在“线材无用论”争议中的地位。米谷彰彦教授的研究:核心内容与方法米谷彰彦教授的研究并非孤立的观点,而是建立在对音频信号传输过程中的电磁干扰、材料特性以及人耳听感差异的深入理解之上。他的研究往往聚焦于以下几个关键点:.............
  • 回答
    @勃呆萌 在与 @BUG不是错误 的对线中,使用“通电下野”这个说法,可以说是非常形象地概括了他在那场争论中采取的一种策略,或者说是一种“退场”方式。要评价这个行为,我们需要从几个层面来理解“通电下野”的含义,以及它在具体语境下的作用和效果。首先,我们得拆解一下“通电下野”这个词。“通电”通常指的是.............
  • 回答
    沈抚新区有轨电车西延线,这条承载着区域发展脉络和城市互联梦想的线路,终于在万众瞩目中正式投入运营。这一时刻的到来,不仅是沈抚新区城市建设的又一里程碑,更标志着中国城市公共交通领域的一项重大创新——全国首个连接两个城市(抚顺、沈阳)的有轨电车线路的诞生。历史性的连接,打破空间壁垒这条西延线的开通,其意.............
  • 回答
    “扶桑安魂曲”,这是《隐形守护者》里一条令人难以忘怀的线,它不仅仅是游戏剧情的一个分支,更像是一曲在乱世中为逝者低语的哀歌,为那些被时代洪流裹挟而牺牲的灵魂奏响的挽歌。我个人对这条线的情感非常复杂,它既让我看到了人性的光辉,也让我体会到了命运的无情和战争的残酷。初见端倪,命运的丝线缠绕这条线最开始的.............
  • 回答
    Apple Watch 上的 San Francisco 字体,这小东西可是苹果在用户体验上又一次细致入微的打磨。要评价它,得从几个维度去拆解,才能看出它到底有没有把这块小屏幕上的文字“伺候”好。首先,易读性绝对是 San Francisco 的核心卖点,尤其是在 Apple Watch 这种尺寸的.............
  • 回答
    这番言论出自中国科学技术大学的一位教授之口,将“一本线高9分”的大学直接定性为“浪费生命,不值得上”,无疑是一句极具争议的话。要评价这番话,我们需要从多个维度进行审视,包括其背后可能存在的合理性、潜在的偏颇以及更广泛的社会影响。首先,理解这位教授的“弦外之音”:教授之所以会说出这样激进的话,很可能源.............
  • 回答
    2020年7月9日,惠普在那个夏天如约而至,以线上发布会的形式为我们带来了自家一系列的游戏新品。那段时间,随着疫情的持续,居家娱乐的需求被无限放大,游戏市场更是迎来了新的增长点。在这样的背景下,惠普的这场发布会自然备受关注,毕竟它关系到我们如何更“爽”地投入到虚拟世界中。亮点纷呈,让人眼前一亮:这场.............
  • 回答
    申鹤正式上线立绘的改动,尤其是对肚脐和马甲线的抹除,确实引起了不小的讨论。要评价这一改动是否合理,我们可以从几个不同的角度去审视。首先,我们得理解“合理”的定义。 在游戏设计的语境下,“合理”可以指向多个层面: 符合游戏内容和世界观的合理性: 申鹤作为一个“留云借风真君”的弟子,修炼仙法,性格冷.............
  • 回答
    七夕节,这本该是牛郎织女鹊桥相会的浪漫日子,如今却在咱们大天朝的高铁线路上玩出了新花样——“表白专列”!而且,一旦推出,立刻就成了全网热议的焦点,尤其是那句“现实版速度与激情”,真是说到心坎里去了,瞬间点燃了大家的热情。咱先来捋一捋这“表白专列”到底是个啥?简单来说,就是一些热门线路,比如从北京到上.............

本站所有内容均为互联网搜索引擎提供的公开搜索信息,本站不存储任何数据与内容,任何内容与数据均与本站无关,如有需要请联系相关搜索引擎包括但不限于百度google,bing,sogou

© 2025 tinynews.org All Rights Reserved. 百科问答小站 版权所有