天天财汇 购物 网址 万年历 小说 | 三峰软件 小游戏 视频
TxT小说阅读器
↓小说语音阅读,小说下载↓
一键清除系统垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放,产品展示↓
首页 淘股吧 股票涨跌实时统计 涨停板选股 股票入门 股票书籍 股票问答 分时图选股 跌停板选股 K线图选股 成交量选股 [平安银行]
股市论谈 均线选股 趋势线选股 筹码理论 波浪理论 缠论 MACD指标 KDJ指标 BOLL指标 RSI指标 炒股基础知识 炒股故事
商业财经 科技知识 汽车百科 工程技术 自然科学 家居生活 设计艺术 财经视频 游戏--
  天天财汇 -> 科技知识 -> 如何评价「线程的本质就是一个正在运行的函数」? -> 正文阅读

[科技知识]如何评价「线程的本质就是一个正在运行的函数」?

[收藏本文] 【下载本文】
听到有人说:"线程是非常简单的东西,就是一个正在运行的函数,有的人为了显示自己水平很高深,发明了线程这种词来迷惑外行。多线程就是一个正在运行的程序中有…
如果我的学生问我线程是什么,我的回答基本也会和「线程的本质就是一个正在运行的函数」差不多
因为这是最容易让初学者理解并能够帮助他们应用到实践中的方式。对于我来说,我施教的目的达到了。
但如果在知乎上这么说,就经常就会有一些写了几年代码的“大神”们跳出来,指正我的不对,然后举出一堆专业术语来告诉我线程不是那么一回事。同时给我扣个误人子弟,不懂装懂之类的帽子。这让我很难受。
所以回答这种问题,我还是真的挺纠结的,一来我实在不想一上来就直接就扯什么调度器,时间切片,上下文切换,信号量之类的玩意,二来我又想把这个知识点科普出去,还得让“大神”们不至于太鄙视我,思来想去干脆完成实现一个“多线程”程序。来演示“线程”这一个概念是如何运作的----准确来说,我的意思是实现一个编译器,将代码中的函数编译为特定指令流,然后在此基础上实现一个虚拟机执行环境,执行该指令流并设计一个基于指令计数切片的调度器,完成这个线程的调度工作。然后我们进一步科普线程间的协同关系并引入更多的装逼术语。当然我们的多线程的实现机制是纯算法实现且平台无关的,你不用纠结一些平台相关的额外的设计模式把你绕的云里雾里最后问一句:“听上去感觉听屌的,但线程到底是个啥”这种疑问,同时这种设计思想也许因为平台或硬件支持关系有所不同,但核心思想在多数的平台上大同小异,所以你也不用纠结我这个是不是实现的不够高大上。如果你听不明白上面说的是什么东西没关系,下面的内容,通俗易懂,老少咸宜。
我们先来写一段代码

#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语言实现
matrixcascade/PainterEngine?github.com/matrixcascade/PainterEngine


PainterEngine 一个由C语言编写的完整开源的跨平台图形应用框架?painterengine.com/
最后,学习过程中真的应该珍惜有这种愿意用最好懂的方式或语言给你讲解知识点的朋友或老师,如果一个人明知道他说的这些你听不懂,而仍然坚持要跟你这么说,那只能说明他的目的并不是想教会你,而是想告诉你我有多凡尔赛多牛逼.当然我也没有贬低谁的意思,毕竟这种事情你知我知,大家都爱干.
送礼物
还没有人送礼物,鼓励一下作者吧
别去纠结「X的本质是Y」之类的命题。计算机科学是一门应用科学,没有那么多的本质。试图把某个概念归纳为简单易懂的几句话,以为掌握了所谓本质就掌握了全部的学习方法是错误的。
要是说线程的本质就是一个正在运行的函数,那函数是啥?函数的数学定义(够本质了吧)是一个集合到另一个集合的映射,根本没有「运行」什么事,跟线程也不沾边。「函数式语言中的正在运行的纯函数也是线程」这种就真是想太多了。
我猜「线程的本质就是一个正在运行的函数」讲的应该是C系列语言里的所谓函数,跟lambda运算为基础的函数式语言里的函数不是一回事。其实说「线程是正在运行的过程」倒还比较有谱(以Pascal启蒙的小朋友会把过程(procedure)和函数(function)分得特别清楚,在C系列语言里可以暂且理解为返回值为void的函数,也不知道C语言作的什么孽把这两者混为一谈了)。为了解释「线程是正在运行的过程」我们可以稍微深入一点看一下内核。如果你在Linux环境里,Linus Torvalds自己说了: 没有什么进程线程之类的,都是context of execution. 从图灵和冯诺依曼的角度说,计算机其实就是个顺序执行指令操作内部状态的机器,「过程」是procedure的一个很好的翻译,强调了这个顺序执行的特性。Context of execution就是这个执行中的机器在某个时刻的所有状态,而支持多道执行的操作系统能管理多个这样的context of execution之间的切换让一台计算机能像很多台传统冯诺依曼机一样使用。所以Linus说得一点毛病都没有,Linux内核里进程和线程也确实就是一回事,但这只是Linux的实现方法,在别的操作系统里线程的实现又不一样了。
「线程是正在运行的过程」是足以解释计算的并行的,但是不足以解释并发过程间的通讯和同步,而这才是困难的部分。解释并发过程的常用理论模型有Actor model和CSP, 这些模型里「线程」这个词就没出现过,但理解它们能够帮助理解各种各样的线程库的设计。可是你不能说线程的本质就是CSP. 线程就是个在不同上下文中指代不一样的名字,线程没有本质。
计算机科学是应用的科学,不是探求万物本质的基础自然科学。很多的概念都是人先在实践中为了解决具体问题发明了有用的东西,然后为了交流,你得给这活起个名字吧,有了名字之后更多貌似相关的工作又会被联系到这个名字上。那句话怎么说来着?
There are only two hard things in Computer Science: cache invalidation and naming things.
-- Phil Karlton
所以在计算机科学里不要纠结概念的本质,要纠结起来它们中的大部分就是一些起得很糟糕的名字。只要有个解释你看得懂,在你目前的问题里这个解释能够自洽能够帮你理解和做事情就行了。
最后,警惕那些总是说「有的人为了显示自己水平很高深,发明了XXX这种词来骗外行,其实简单得很」的人,我善良地希望这些人只是单纯的不懂而已。他们永远不会错,你要认真上去blahblahblah解释一通,人家只会微微一笑:施主你只是道行不够还没看透事物的本质。知乎著名的特别爱说这话还要开班教授计算机科学本质的王垠大师,最近已经开始去戳穿相对论核能和阿波罗登月骗局了,这个行为发展就特别自洽。
又是本质侠是吧, 你这不够本质, 我也来点暴论:
线程本质其实是一个在无限大纸带上乱涂乱画的人。
连函数的概念都省了,小学生都听得懂。
而多线程就是一堆人在那挤来挤去抢着在纸带上作画的大型抽象行为艺术。
这么解释连小学生都能看出这个事情是多么的混沌。
所以说线程本质上是一个在无限大纸带上乱涂乱画的人.
而多线程并行就是一堆人在那挤来挤去抢着在纸带上作画的大型抽象行为艺术.
这堆人在那抢来抢去, 写来出的狗东西屁不通, 虽说汉字序顺并不定一影阅响读, 还有是人看不去下了.
规定作画必须在小房间里作画, 然后房间加个锁, 别人作画的时候你就眼巴巴的盯着锁吧.
虽然作画效率降低了, 但是听起来好像很合理...吧....应该....
然而, 这群抽象“艺术家”里面有的人会共同创作.
这群家伙特别喜欢先写完一部分, 然后等着别人把另一部分写完才继续创作.
没想到啊, 写另一部分的那个人, 也在等着他先画完!
麻了, 两个人就这样干等着, 谁也不动笔
太抽象了, 这种相互等待的抽象艺术比画作本身都要更加艺术.
于是更加抽象的事情发生了, 这群艺术家里选出了一个领导.
由领导来安排创作顺序, 领导会一个砸瓦鲁多的技能.
当领导喊出"砸瓦鲁多"的时候, 就算创作欲望在强烈, 但是谁也不准动笔, 只能等领导欣赏完画作.
当然这堆抽象的事情还没完.
有的抽象艺术家创作欲望简直嗨到不行, 一路画到了别人的画里面, 为了解决这个问题又发明了一堆屏障一堆保护机制.
最抽象的是因为这堆抽象的问题实在太多了, 抽象艺术家觉得不能再这样下去了, 发明了一种叫资源的东西.
画布, 颜料, 画笔都是资源, 资源获取的时候登记名字, 资源还可以相互借用, 然后定义了几千条看不懂的借用规则.
动不动就跳出来警告:
你不能作画, 因为不满足 E0382 规则: "你不能在欣赏别人的画的时候作画!"
你不能作画, 因为不满足 E0515 规则: "你不能在借用别人的工具后买新的工具!"
我带你们打, 太?抽象了, 我是来画画的, 不是来做律师的, 天天研究你那破规则, 老娘作画灵感都没了.-
送礼物
还没有人送礼物,鼓励一下作者吧
先问一个问题,有没有办法把一个线程保存到硬盘的.img 二进制文件中,想让它运行的时候再读出来让它继续跑?听起来是不是很像《赛博朋克 2077》里的“灵魂杀手”以及 Relic 芯片?还真有这么一个东西:
checkpoint-restore/criu?github.com/checkpoint-restore/criu




把 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.img1号进程(同线程)的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个子进程,它们各有一个主线程
好了,看完了,问题回到了线程的本质是什么?
答案是:线程称不上本质是函数,函数是简单的,线程是复杂的。函数只是代码段里数据,但是线程牵涉的远远不止是代码段。为了运行一个线程,系统还需要打开一堆的文件描述符、分配一段内存数据段+栈段给它,同时寄存器的状态都需要额外的内存来记录,表面的函数只是冰山一角
送礼物
还没有人送礼物,鼓励一下作者吧
我去!题主,这是哪位高手给你的指点?相当精准啊!
虽然“发明了线程这种词来迷惑外行”有点偏激,但站在比较高的抽象层级上来理解,起码“一个正在运行的函数”这个比喻完全没有任何问题!
不信来看看Linux操作系统的作者Linus是如何回应这个问题的:

On Mon, 5 Aug 1996, Peter P. Eiserloh wrote:
> 
> We need to keep a clear the concept of threads.  Too many people
> seem to confuse a thread with a process.  The following discussion
> does not reflect the current state of linux, but rather is an
> attempt to stay at a high level discussion.

NO!

There is NO reason to think that "threads" and "processes" are separate
entities. That's how it's traditionally done, but I personally think it's a
major mistake to think that way. The only reason to think that way is
historical baggage. 

Both threads and processes are really just one thing: a "context of
execution".  Trying to artificially distinguish different cases is just
self-limiting. 

A "context of execution", hereby called COE, is just the conglomerate of 
all the state of that COE. That state includes things like CPU state 
(registers etc), MMU state (page mappings), permission state (uid, gid) 
and various "communication states" (open files, signal handlers etc).

Traditionally, the difference between a "thread" and a "process" has been
mainly that a threads has CPU state (+ possibly some other minimal state),
while all the other context comes from the process. However, that's just
_one_ way of dividing up the total state of the COE, and there is nothing
that says that it's the right way to do it. Limiting yourself to that kind of
image is just plain stupid. 

The way Linux thinks about this (and the way I want things to work) is that
there _is_ no such thing as a "process" or a "thread". There is only the
totality of the COE (called "task" by Linux). Different COE's can share parts
of their context with each other, and one _subset_ of that sharing is the
traditional "thread"/"process" setup, but that should really be seen as ONLY
a subset (it's an important subset, but that importance comes not from
design, but from standards: we obviusly want to run standards-conforming
threads programs on top of Linux too). 

......

后面的内容我就不当搬运工了,感兴趣的可以自己去kernel mailing archive看。
所以啥是线程?啥是进程?Linus说了,抽象地看,根本就是一回事——Context of Execution。
啥是Execution?图灵奖得主Nicolas Wirth有本著名的书Algorithms + Data Structures = Programs,也就是把程序粗略地分成静态和动态两部分。Execution显然是动态部分,在现代编程语言里面,一般都实现为函数。
啥是Context呢?上下文。注意,只有对正在执行的函数,讨论上下文、现场这些概念才有意义。
那综合到一起,啥是线程/进程?可不就是“正在运行的函数”吗?
至于不少其他答案提到的局部变量、CPU状态、如何落盘等等等等,错倒是没错,但都太过深入某个OS/某个版本/某种硬件的实现细节,而忽视了站在更高的视角看待线程——“不识庐山真面目,只缘身在此山中”之谓也。
换句话说,如果OS和运行时能够提供良好的COE抽象,我们完全可以用“正在运行的函数”随心所欲地实现进程、线程、纤程(fiber)等很多概念——著名的goroutine了解一下?
最后只想感叹一句,为啥我当年学编程的时候没有这么一针见血的导师?!


我看了一下现在的回答,似乎大都围绕着实现细节说了,少数在概念层面解释的,却都被绕进了“调度/时间片/并发”这个圈子里绕不出来了。然而,我必须指出,线程/进程在概念上,并不和这些概念捆绑——虽然大多数情况下,确实如此。
其实,线程/进程并不是一个真实存在的实体,是一个凭空抽象出来的逻辑概念。和所有凭空抽象出来的概念一样,它必然是为了某个目的而出现的,那这个目的,不是“并发/并行”,而是一个已经“濒临失传”的概念:状态机。
回到一个最简单最基础的计算机环境,一个单核单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/并发/时间片/上下文”等相关概念给深度捆绑了。
到现在,毕竟内核没有办法深度了解子程序内部逻辑,所以它的调度必然是无序而且粗暴的——所以它必须设定“时间片/优先级”之类的概念,而且每次切换都必须完整保留上下文(因为它不知道那些有用哪些没用)。同时,为了对付无序的切换,各种额外的开销(并发编程/锁)也必然很大。所以,为了降低这些开销,上古年代的用户态伪并发玩法被重新拿出来,封装了一下之后,没再用线程/进程这个名字,而是给了一个新鲜的名字:协程。
基本不沾边——如果说满分100的话,这个说法看在字数挺多份上能拿0.5分。再多就有舞弊嫌疑。
想明白线程是什么,必须先明白进程是什么——课本上那句“进程是程序的一次运行”可是不够的。
用“标准比喻”说,程序就是能放进图灵机执行的一条“纸带”——存硬盘上就是个若干K或者若干M的字符串——然后图灵机有一个读写头,可以按顺序读入纸带内容、或者在纸带上按照程序指示前后移动。
比如,“纸带”内容可以是“#!bash\necho this is a program\n if ( cond ) then xxx else yyy end\n 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接受一个函数,哦,线程就是运行的函数……然后一通神奇操作、把项目彻底搞砸——这个责任,除了你,还能给谁背?
不懂不要紧,知道自己不懂就没有危害;不懂,还要跳,那我只能强迫你一边歇着去。
鲁迅说的好,无端的浪费别人的时间无异于谋财害命。
把在你身上浪费的一秒钟拿过来,撸一把猫,踢一脚狗,不都更有意义吗。
送礼物
还没有人送礼物,鼓励一下作者吧
多线程的难度又不在理解 你理解了,so what?就知道怎么写程序不会死锁、不会性能降级了吗?我也算写很多年多线程程序了,上周刚写出个64核比32核还慢的OpenMP程序……
这种说法应该是从某种高级编程语言的编程实际角度去理解线程,不能说错,但是绝对不是本质,甚至是有些本末倒置。
在当今大部分高级语言当中,程序是以一个一个“函数”进行组织的。开一个线程的时候,需要指定一个入口函数,新的线程就是从这个函数开始执行。
所以会有题目当中这种理解。
然而,函数本身也只是高级语言构造出的一种概念。当程序被编译或者解释,由CPU进行执行的时候,函数的概念其实是不复存在的。
在这个层级(汇编层级),对于CPU来说只有当前执行的代码位置(以x86 CPU为例,PC寄存器当中的值),和上下文(堆栈)。
线程的本质就是一个小小内存领域当中记录的很多不同的“当前执行位置”+堆栈。CPU在线程当中切换,其实就是根据这个记录切换这个“当前执行位置”和堆栈。
其实这就如同我们生活当中用的备忘录,会记录很多事情,大致的背景、下一步要做什么。我们自己就好像CPU,会不停的在其中选择一件事情推进,进行一会儿之后记录下进展,然后换另外一件事情。
而函数则是类似做事情的步骤,记录了做一件事情需要经过1、2、3、4、5这几个步骤。这些步骤自然而然成为我们在不同的工作之间切换的一个契机,但是它并不是我们可以“同时”进行多种工作的本质。
这个其实根本不需要评价。如果只需要一句话解释线程的话有远比他更精准的方式,比如:
线程的本质就是可与其他线程共享同一个内存空间的进程。
甚至,类似的定义还可以用来定义协程:
协程就是可以与其他协程同时在同一个线程内运行的线程。
这属于话糙理不糙系列。
从说话人的口吻来看,貌似是在给某人传授关于编程的知识和经验。
诚然,这段话本身是不严谨的,但对初学者来说,有益于入门多线程编程。
毕竟,你要把本质讲清楚了,一半的人吓跑了,剩下一半听睡着了,结果一个学会的都没有。
这就跟我们小学一开始只学自然数,老师会说1 - 3减不了、2 ÷ 3除不开一样,具有更多知识的人一看就知道不严谨,但对小学一年级入门数学殿堂是有帮助的。
这个人说这话的本意,我觉得是希望打消听话人对线程的恐惧,就把它当做一个可以同时执行的函数,赶紧动手试一试。一跑程序结果对了,自然会带来更多的自信,会让人继续深入学习下去,随着学习的深入,自然会知道线程的本质到底是什么。我见过太多一提到线程、进程、并行、C语言,还没说要干啥呢,就吓得扭头就跑的汉子了……对于这些人,有一个可以降低恐惧感的老师,还是很有帮助的,哪怕说得话不严谨。
所以,要问这种说法对不对,其实说话人本身估计也知道漏洞很多。然而,他的目的是让别人赶紧钻进线程学习里,而不是一定要保证自己每句话都是无懈可击的。所以,感觉倒也无伤大雅,没必要非要去杠。
我给别人培训的时候,就说过这样的话:我给你们讲东西,从来都不怕讲错了。因为,我讲错了,你以后用到了,只要认真做测试,就会发现错了,反而印象更深刻;我讲错了,你以后用不到,既然用不到,对错又有何妨呢?这就是我为什么讲计算机知识,不讲医学知识的缘故。
送礼物
还没有人送礼物,鼓励一下作者吧
说这话的人,不仅线程没真正理解,只怕是连函数是什么都没真正理解。
在你25那年,上帝一次性分配给你30个女朋友。
你竟然给自己制定出这样的计划:陪第一个女友到她死、再陪第二个女朋友到她死、再陪第三个女朋友到她死……
一、线程的本质是时间
是可以用来计算的时间。
当然,这里的“计算”,就是“处理”,就是“做事”的“做”。
计算机里当然不仅仅只有中央处理单元(CPU)可以处理可以计算(非中央的处理单元也可以嘛,并且往往更有花样),但为表达方便,我们就以CPU为例。
所以,线程的本质是CPU的时间片。
非要在再加点什么的话,那就说,给你独立的时间了,如果你确实觉得有必要,那还可以给你一点点独立的空间,嗯,就是(特意划分)给这个线程一份不被其它线程碰触的内存。
所以的所以,线程本质是什么?就是CPU的小三最希望得到的陪伴时间嘛。
“我会给你时间的,依据我80486的主频和我拥有的物理线程的个数来看,我至少会一周过来两次操作一下你……”。
“你想要钱?不是给你一个写着我的名字的银行帐户了吗?那上面的钱是包括主线程和所有和你一样的子线程(如果愿意)都可以去碰的……哦,你希望能有一份你独有的,别人看不到的帐户,你说你在意的不是钱,而和我之间的一份小秘密……嗯,那我给你开一个本人帐户吧……”
二、世上本没有线程,但很早就有CPU
物理世界里,是真有CPU的,就是那个长着密密麻麻针脚,跪上去会疼的。
物理世界里,也是真有内存的,就是那个让iPhone突然贵出1000元的东西,你愿意解剖手机话,那你就能看得到摸得到内存。
但这世界上,有看得到摸得着的物理“线程”吗?可能也有,估计支持和不支持多线程的CPU,在电子线路设计上会有一些不同——但那些线路设计与实现,本质是上让这颗CPU——编程意义上的“线程”则还需要和上层操作系统结合——拥有将时间分片的能力,因此,不能说这些线路上的电子元器件组合起来是“线程”——这和你拥有包小三的能力,但不能因此就说你拥有小三一样很好理解吧?甚至,都不能说明你对“拥有小三”有过幻想。
让我们把CPU(及其它必要的器件,代表客观能力),和上层操作系统(代表主观意愿)统统结合起来,简称“计算机”,然后再认真的学习一下比美国历史还要短一点的电子计算机的历史, 结论就是:幼年时期的计算机,他们所具备的核心器件,可以用来拉尿,却不具备包小三的能力;长大以后的CPU娶了老婆,并且他的核心能力跟随摩尔定律越来越强大:
80286:一秒6M次郎80366:一秒20M次郎iPhone6的某款CPU:1秒4G次郎……
一秒多少次有意思吗?就一件事情,无论你做多快,最多有人称赞你“好快!”,却不会有人夸你这颗CPU“真是时间管理大师啊!”
三、怎么样才能成为一颗令人景仰的,“时间管理大师”级的优秀CPU呢?
答案呼之欲出了。
第一你得会划分时间片;第二你得拥有将划分出来的时间片快速分配到不同事情上的主观意愿。
假设题主同时拥有30个女朋友;那么,题主要怎么分配自己的时间,才能成为社会景(唾)仰(骂)的时间管理大师呢?
一年仅陪一个女朋友,花了三十年才陪完30个女友……这没什么(其实也挺棒了);
每2秒陪一个女朋友,每1分钟都能让30个女友雨露均沾,最终结果是30个女友都觉得你(几乎)把所有时间片段都分给她自己一个人……时!间!管!理!大!师!
……
别幻想了,人是肉身做的,你不可能每2秒陪一个女朋友的(或者,你是肉身,但你的女朋友们不是肉身做的)……
电子计算系统不是肉身做的;所以它可以开开心心地,高速的(远高于2秒一次的频率)在各个小三(需要计算的事)快速切换。为了更清晰感受,我们举个例子——这个例子的重点当然在保持理论或原理是基本正确的情况下,然后为了让人类看得懂而设计的例子,显然不是指计算机真的就笨到如下两件小事都需要分时间片处理:
假设第一件事情是计算1+2+3+4+5(以下称为计算一);第二件事情是计算 10+11+12+13(以下称为计算二)。
再假设要计算这两件事的计算机,只有一颗CPU,并且还是单核的。
线程就是CPU的时间片。
假设当前这颗CPU闲着没事干太久了,于是它产生了强烈的,想同时干计算一和计算二的愿望,并且,它是有能力分时间片的,所以,下面是它的时间管理:
时间片一:来到计算一身边(这很重要,下同),计算1+2得到3;时间片二:来到计算二身边,计算10+11得到22;(这里我算错了,应为21)时间片三:来到计算一身边,计算3+3得到6;时间片四:来到计算二身边,计算22+12得到34;……
世上本没有计算机中的“线程”。线程是一个借用现实物体的术语。thread作为名词,原本应该是“棉线”、“细丝”、“螺纹”、“线索”、“思路”、“脉络”……,作为动词,是“穿”(穿针的穿),是“穿过(像线从针眼里穿过去那样的穿过)”。所以,什么是线程?就是一开始想到可以让CPU拥有时间片这个“想法”的计算机大拿们在聊这个事时,反反复复地说“下一个CPU开始去处理另一个计算的时间片段……”时,感觉累了,于是说“哎,就用'thread/线'表示吧”(反正每次在纸下画用于表达的CPU的时间片时,也是自然而然地就用一条条扭扭曲曲的线……)
这里面TMD有“函数”什么点事?函数这玩意儿,除了在数学世界里有用到但其实就相差挺大以外,在计算机的世界里,它距离CPU等物理世界更远更远了。
非要非要扯点关系的话,那线程就是“郎君是否有时间来到我身边……”,而函数则是“郎君来到我身边之后我想让他做点什么……”
在当前已经拥有的时间片里,郎君可以再给自己安排好一个新的时间分片需求——抱歉,这种情况下,他基本就是在准备(规划)另一件事(,并且是中断当前的事)。“时间片”里的“时间”完完全全就是物理世界里的时间。你可以不断地给自己计划出新的时间片——但你并不会因此长生不老(相反,可能因为操劳过度而提前驾崩)。
四、时间片和时间片,有区别吗?函数和函数,有区别吗?
假设线程T1、T2、T3,并且它们的创建关系是:T1创建T2,T2 又创建T3。当下你可能还不太能回答这样一个问题:T1、T2、T3 是什么关系?必须举个例子:
一、你正在舔自己的右手大拇指(主线程),眼前趴着一只猫一只狗一只鸡——这时候T1,T2,T3都还没产生,因为你一直就只是在(看似机械地,其实是嘴和手在大脑指挥的控制下)舔大拇指;二、你的大脑(程序员写的程序)突然想摸一下猫毛,于是,它在原本仅指挥你的嘴的大脑,迅速下达指令,你的左手伸出去摸了下猫毛;三、在摸猫毛的某个瞬间,你的大脑又下达指令让你摸狗毛,很快你的左手转移到狗身上;四、在摸狗毛时,大脑突然又想:再分出个时间片,也摸摸鸡毛吧。想完这个以后,你的左手一定就会放在鸡身上吗?不一定的,比如由于对“鸡毛”这个概念有误解,所以你迟疑了下,手变成在摸猫毛了,接着下一个时间片,你才开始在摸鸡毛。五、最终结果就是,你舔着右手大拇指,左手则高速的,并且基本是随机地,在一猫一狗一鸡身上摸来摸来去。
Q1: 用于摸猫毛和用于摸狗毛以及用于摸鸡毛,以及用于舔手指头的时间片,彼此有区别吗?
A1: 没有。
Q2: 上面描述的是 摸猫时规划了摸狗毛,如果某一次你的思绪是先想摸狗毛,再想摸猫毛,二者有本质区别吗?
A2: 也没有。
现在,对比一下函数。
假设有三个不同的函数:F1、F2、F3,并且它们的调用关系是:F1调用F2、F2调用F3。
Q1:F1、F2、F3 有区别吗?
A1:当然有。如果没有区别,干嘛不只保留一个。
再来:
Q2:上面描述是F1调用了F2,然后F2调用了F3,如果改成F1调用F3,F3又调用了F2,调用关系不同,会造成本质区别吗?
A2:当然有,本是F2调用了F3,现在变成F3调用了F2,区别之大,就是让新的F2不再是原来的F2,新的F3也不再是原来的F3——差不多就是一个孩子在问他妈妈:“妈,你当年要是不嫁给我爸,而是嫁给隔壁叔叔……”得到了吧,那就没现在的你了。
五、函数的本质?
函数的本质是(可以)在每次执行时,都拥有自己一小段独立内存的程序片段。
来了,来了,他来了……函数的本质就是程序!! 可不嘛,人的本质就是一种生物。当我们说一个事物“本质”时,可能会涉及到这个事物的“底层分类”,可是“本质”描述所包含的信息,不能就是“底层分类”啊!!!。请对比一下马克思的描述:
“人的本质不是单个人所固有的抽象物,在其现实性上,它是一切社会关系的总和。”
且不管对不对,但至少人家知道定义的基本游戏规则,说出一点不同的东西。
回到函数,再重复一下:
函数的本质是(可以)在每次执行时,都拥有自己一小段独立内存的程序片段。
线程是……“时间片段”。
函数是……“程序片段”。
来了来了,它又来了!!!“线程和函数的本质,都是片段,所以二者就是一类东西,只是一些人非要故作高深……”
去你的。
六、最后看一眼
“线程的本质就是一个正在运行的函数”?
不,线程最的大本事,就是它上一瞬间还在干这个函数,然后(往往这个函数还没干完),它下一个瞬间就在另一个函数身上了。
“为什么线程拥有这么快速切换的功能?”
因为线程它不是空间,它是时间是时间是时间啊!
函数更依赖空间,代码段在内存空间里,得有地址(函数),函数参数及函数里用到的局部变量,它们在调用栈上,每一次调用(包括同一函数递归调用)都是独立的互不干扰的。
线程不是函数。线程是函数执行所需要的无数个时间片。
干嘛要无数个时间片,为什么不能一个时间片就确保把一个函数干完?
在你25那年,上帝一次分配给你30个女朋友。你竟然给自己制定出这样的计划:陪第一个女友到她死、再陪第二个女朋友到她死、再陪第三个女朋友到她死……
来,喝一杯吧,我敬你是一位君子。
如果还是觉得线程就是一个正在执行中的函数的话……
也可以啦,但不够深刻,不如说:CPU就是一个正在执行中的函数。
顺带说下,学习编程最好不要过早进入“看破一切技术”似的哲学层面。比如,不搞清楚由于线程的存在,而造成同一个函数的同一次调用都有可能被打断(再恢复)的话,只是如题目那样“超然”认为多线程就是一个进程中多个在同时执行的函数(或同一函数的多次调用)的话,那多线程(本质是并发、并行)所带来的资源冲突等问题,以及由此引发的各类解决工具方法(锁),再以及后续的一些新的变化发展(异步、协程等),就很难理解到位,甚至这种极简化理解(此处是“线程就是……函数……其实简单得很”),基本会让你未来徒增莫名其妙的困扰。
初学者可以对比思考一下,为什么还要有TLS(上面提到的,小三自己的帐户);对比方法是:一是先接受“线程就是一个正在执行中的函数”,然后去看TLS的学习资料;另一种,是按线程是一组(不连续的)时间片去学习。
大家还可以这样对比思考 ,假设有个初学者,一开始从来不知道有“多线程”这事(相当于他活在一个特意营造出来 的没有线程概念的编程世界里),然后终于有一天他要面对线程了,这时候,他应该先引起他的本能敏感的,是“时间片”,还是“多个函数同时执行”呢?对比图如下:


意见区别可能就在这里,我肯定会先告诉初学者:
“你丫的,各位注意了!从今天开始,别再以为一个函数一旦执行就会一口气跑到尾,甚至!别以为你的一个表达式一开始求值(比如 a = a+b)就会(在连续时间轴上)一口气执行完成!”
(事实上,要不是这个问题强行拉入函数,我肯定是只用“表达式”求值过程会被打断来作例子)
另外一个老师则会告诉你:
"线程是非常简单的东西,就是一个正在运行的函数,有的人为了显示自己水平很高深,发明了线程这种词来迷惑外行。多线程就是一个正在运行的程序中有多个函数正在同时运行。什么线程,多线程只不过是骗外行罢了,其实简单得很”
说这话的人根本没有明白线程概念的难点在哪里。首先说“多线程只不过是骗外行罢了,其实简单得很。”这句话绝对是不正确的,并且恰恰相反,多线程很多时候一点都不简单。即便是一个能够熟练掌握线程技术的人,我认为也绝不应该因为自己已经会了就否认这项技术在实践中的困难性。
关于线程的原理其他回答说的挺多了,这里不再多说。实际上仅从原理出发来看,这确实不是一个多么难理解的概念,但是为什么现实中多线程仍然是一个经常带来麻烦的家伙呢?主要是因为代码一旦涉及到多线程,就必然会带来“当前代码在执行时到底在哪个线程”这个很不直观的问题,而且随着代码复杂度提升,这个问题往往会越发的不符合人类直观的思维方式,更不要提它还牵扯到更多诸如相关资源是否存在并发冲突等必须仔细思考才能确定的问题。正是因为这些隐含的关联问题,才使得多线程编程往往容易成为深坑,一不小心就崴脚。
如果你觉得只要细心就能避免这些坑,我得说是这种心态过于乐观或是傲慢了。就我的经验来看,要写好一个多线程模式的程序所需要的心智负担是非常重的,我就实际见过一些高素质+非常细心的工程师在这方面也会有困难,有些时候你就是很难帮助别人理解为什么一段代码会带来潜在的线程安全问题,除非说让他先做一段时间针对性的强化训练(而这在实际工作中往往是不可能的承受的代价)。
就现实来说,因为这种应用上的困难性,我更愿意尽量夸大多线程的难度,避免一般的开发人员盲目自信随便开线程,否则一旦代码搞出问题是很难收场的。从这个角度出发,就千万不要给人一种对这个问题能够一眼看穿本质就能了然于胸的感觉。毕竟本质再简单,也改变不了应用上到处是坑的特点。
说这种话的才是外行吧,
因为函数这玩意儿,本来就是C语言带坏的。这货应该说是带参数的子过程,C语言不管子过程带不带参数都叫做函数,这本来就很离谱。
至于什么正在运行的函数这更离谱,搞得函数好像有个状态叫做运行一样……
与其说什么多线程和线程是用来迷惑外行的,倒不如说是用来筛选从业者的。毕竟这玩意儿都要什么理解本质,什么直观形象,你压根儿就不适合干这行才对……
修改一下回答,我发现好多回答模棱两可,给人感觉这个问题很复杂,什么系统调度都考虑进来,难道没有操作系统,或者很多嵌入式操作系统没有线程这个概念,他就不能并行执行程序吗?
站在编程语言实现角度看,这个问题哪有那么复杂?线程在编程语言实现角度看,他就是一个函数,这是肯定的,你要考虑系统调度什么的,这多线程程序没法写了,不同系统,其任务调度方式大相径庭,难道我还要为每个操作系统设置不同的语法来适配任务调度?那程序还谈啥可移植性,跨平台性?
站在编程语言实现角度看,我的编程语言要实现并行执行,或伪并行执行,并不需要去考虑什么多线程,什么多进程,协程,什么多任务,中断等等概念,,,我也不需要考虑什么操作系统,Linux怎样,windows又怎样,macos,嵌入式的RTOS,很多芯片没有跑操作系统又有什么关系
对于并行程序的执行而言,我只需要关心,数据是否安全,这才是根本问题,常规做法就是给你要并行执行的程序一块单独的内存,这样数据是分开的,数据的写自然是安全的。你如果没有给相关数据独立的内存空间,那么自然要引入各种锁的概念来把并行的数据写,变成串行的数据写,这样也能确保数据安全。
站在数据安全的角度看,线程和进程,其实就很明白了,也就那么回事,进程间为什么要进程间通信?而线程为什么要同步?进程由于给了更多的内存区,比如数据区,进程也是独立的,所以进程之间的内存更加独立,数据倒是更加安全,但是由于数据区是独立的,你自然要考虑多个进程间怎么共享数据的问题,,,而线程,你只给他了一个独立的栈区内存,数据区还是和主程序共享的,所以为了数据区的数据安全,你自然要考虑多线程同步的问题,你要写数据区的数据,那可不得各种锁,来同步吗?此外由于进程占用更多的内存区域,而线程只需要一个栈内存区,在创建和销毁进程时,可不得更加耗费硬件资源吗?
只站在编程语言实现来看,还真是一个函数,以C语言的pthread库来讲,创建一个线程,还就是一个回调函数,这个函数和其他普通函数在语法概念和底层实现上没有什么不同和特别(除了要遵循开发者定义的原型)。后面都以C语言为例。
但是线程这个函数可以独立于主程序(进程),可以并行执行,就要做处理了。你想想执行一个函数最少必须要什么资源?由于函数内部的局部变量是在栈分配内存并执行表达式的,所以栈就是函数必须要的最小资源,所以你要给这个函数分派一个独立的栈空间。这样主程序用主程序的栈,线程使用自己的栈空间,这可不就能各玩各的了吗。
再回到题主主线,线程本质是个函数没错,但是是隐含了线程这个函数是必须要有自己独立的栈空间的,虽然普通函数也必须要栈,但是是用了调用者的栈(主程序调用就用主程序的栈,线程调用就用线程的栈),而每个线程函数你都要给他一个独立的栈。所以线程至少等于函数体+独立的栈。
所以我认为用线程概念没问题,在内存处理上还是有区别的。
你至少可以理解成独立于主线程序的另外一条线上执行的程序吧,这句话纯属娱乐 。
这个问题另一个问题相似,我把回答也复制过来,做一个扩展吧。
为什么说线程是程序执行的最小单位?
下面是原回答:
程序执行的最小单元,其实等于,执行某个程序需要的最小内存资源。下面都以C语言为例。
首先看一个程序被执行,要什么内存资源,程序被加载到内存后,从人的角度看,内存被分为,堆区,栈区,BSS区,数据区和代码区。
你写的代码被编译器依据语法和表达式类型被分到不同的内存区域,比如你的函数的局部变量和表达式的执行都在栈区,你的程序(已被编译成指令)被放在代码区,你要申请一块内存,则会在堆区给你内存。
再回到题主问题,线程为什么说是程序执行的最小单元?把这个问题换成:线程为什么占用最小内存资源?还有没有更小的占用资源的方式来执行程序呢?
答案是:没有了。线程占用的资源就是最少的了。要理解这个资源占用最少,首先要看,什么是线程?
我这里只会现在语言抽象层面来说明线程。当你要创建一个线程时,代码要怎么写?这简单啊,先写一个函数,然后调用API把这个函数作为参数穿进去,这样系统就会用一个CPU去执行这个线程了。
好,很明确,语言抽象层面来讲,线程首先是一个函数,接下来分析要让CPU来单独执行这个线程函数,注意是独立运行,而不是在主程序中运行,还需要什么?第三段已经说了,函数的局部变量和表达式的执行是放在栈区的,所以你还要单独为这个线程函数分配一个独立栈空间。这下就很明确了,线程只需要你给他一个独立的栈空间就能执行函数内的程序了,而一个主程序跑起来需要5大内存空间,所以一个线程函数的执行所需要的资源已经是最少的了。所以说线程就是程序执行的最小单元,你没有办法在精简了。就像目前物质现在最小就是中子,质子,电子,没法更小了了。
[收藏本文] 【下载本文】
   科技知识 最新文章
百度为什么越来越垃圾了?
百度为什么越来越垃圾了?
为什么程序员总是发现不了自己的Bug?
出现在抖音评论区里边的算命真不真?
你认为 C++ 最不应该存在的特性是什么?
为什么 Windows 的兼容性这么强大,到底用了
如何看待Nvidia禁止使用翻译工具将cuda运行
为何苹果搞了十年的汽车还是难产,小米很快
该不该和AI说谢谢?
为什么突破性的技术总是最先发生在西方?
上一篇文章      下一篇文章      查看所有文章
加:2025-05-14 13:27:12  更:2025-05-14 13:59:55 
 
 
股票涨跌实时统计 涨停板选股 分时图选股 跌停板选股 K线图选股 成交量选股 均线选股 趋势线选股 筹码理论 波浪理论 缠论 MACD指标 KDJ指标 BOLL指标 RSI指标 炒股基础知识 炒股故事
网站联系: qq:121756557 email:121756557@qq.com  天天财汇