请问一些操作系统基础,线程能够进入等待/阻塞状态的底层原理是什么?

2021-03-26 11:26:19 +08:00
 LeeReamond

如题,学习 Go 的过程中认识到管道这种进程间通信方式,感觉确实是比直接共享内存更合适的线程间同步模型。

学习过程中产生一些问题,涉及到语言如何和操作系统相关联

1 、管道通常有一个阻塞读取功能,比如消费者调用 recv()后进入阻塞,然后由生产者向管道内丢入一个数据,消费者就可以解除阻塞并获取数据,相当于被唤醒,这在语言层面上是如何实现的呢,我们在业务代码中似乎没有办法显式地让一个线程进入阻塞,然后再用另一个线程去 trigger 这个线程。

2 、这种操作非常类似 socket 表现的性状,但我觉得怎么也不会用 socket 实现管道吧,我觉得对于统一进程内间的线程同步,socket 是一种非常昂贵的通信手段。

3 、我们是否有办法显示地管理线程,比如子线程主动进入 block,而后主线程主动唤醒子线程,这种管理是否是可以实现的。类似地,还有主动地关闭一个线程的需求,不过据我所知线程本身似乎是并未设计在外部干预下关闭的这么一个功能。

4 、类似地,java 语言中还有一个 wait 的状态,与 block 不同,wait 通常是指等待锁,而不是等待 IO,其他语言可以释放锁并唤醒一个 wait 线程,这与 block 又有什么区别呢? block 和 wait 的概念是 java 的概念还是操作系统的概念?

问的比较乱,基础不扎实,有了解的朋友请帮忙回复一下,谢谢大家。

2582 次点击
所在节点    问与答
28 条回复
dongcidaci
2021-03-26 12:12:00 +08:00
楼主可以思考的很深入
learningman
2021-03-26 12:15:37 +08:00
所有的等待本质都是轮询,如果你没有操作这个,说明语言或者系统已经帮你完成了。
lcdtyph
2021-03-26 12:30:02 +08:00
操作系统会提供一些原语来支持条件唤醒,比如 linux 下的 wait_queue 或者用户台的条件变量

wait 实际上可以分成 blocked wait 和 busy wait,后者不会使线程让出时间片。wait 只表示等待一个事件的出现,事件可以是 IO 就绪,锁被释放,信号 /中断到来等等,而等待的方式则可以是 blocked 或者 busy 的。
lcdtyph
2021-03-26 12:32:59 +08:00
@learningman
现代硬件支持非忙等待,比如中断形式,当内核没有要执行的指令的时候可以执行 mhalt 让 cpu 进入睡眠状态,从而等待下一个事件到来再唤醒内核
sujin190
2021-03-26 12:36:49 +08:00
谁说不能让一个线程显式进入阻塞的,否则多线程中锁、信号量啥的是用来干嘛的,go 这种和线程其实不是一回事,想知道线程阻塞调度啥的,那就好好看多线程编程啥的比较靠谱
unixeno
2021-03-26 12:38:07 +08:00
chan 的底层其实就是个锁+缓冲区(buffered chan),你可以去看下 golang 的 channel 底层实现
你用互斥锁一样可以实现你说的这种 a 线程 trigger b 线程的效果
zmxnv123
2021-03-26 12:47:14 +08:00
所有的自旋锁例如 java 的 wait 到都操作系统层级都是一个汇编指令,忘了叫什么了
楼主可以学一下 6.828
OysterQAQ
2021-03-26 13:10:19 +08:00
看一下操作系统的线程部分,线程是操作系统中一个最基本的抽象,你说的调度是操作系统的功能,关于线程切换的问题就涉及到线程的实现了,有内核实现,也有用户空间实现的,由进程来管理,也有折中混合的
baiyi
2021-03-26 13:23:36 +08:00
Go 由于有自己的调度模型,所以它的 channel 阻塞不是依赖于线程的。而是由 Go 自己的运行时来保存 goroutine 的上下文,然后等待唤醒。

线程的并发可以看看 CSAPP 的第十二章 并发编程
zhongrs232
2021-03-26 13:55:32 +08:00
Go 只有协程吧,Go 的协程和操作系统的线程是不一样的。不了解 Go 协程,但用 C 实现过一个协程库,我可以回答一下 Linux 下协程的切换原理,简单来说,可以将协程看成某个函数,每个协程(函数)在执行时都有一个上下文,用于保存协程执行时的状态,比如栈地址,PC 指针,返回地址等,这个上下文在 Linux 里面可以用 ucontext_t 结构体来表示。协程切换时,会先将自己的 ucontext_t 保存起来,然后调度器会根据调度策略找到另一个协程保存好的 ucontext_t,根据这个 ucontext_t 可以完全复原另一个正在运行的协程。这就是两个协程切换的原理。
mrgeneral
2021-03-26 14:35:37 +08:00
Go 有自己的调度器,协程的执行在底层也是线程在运行,主是由调度器来决定线程运行哪个协程,协程自己也可以让出线程资源。

你提到的关于阻塞调用如果是阻塞 IO,涉及到系统调用涉及到内核通信,都需要 Linux 底层 IO 模型( epoll ),为了感知这类调用,Go 把系统调用封装了一层,也就是协程只能通过一个系统调用中间层来实现系统调用,从而达到调度器 hook 的目的,最后的链路就是:协程->中间层->操作系统;操作系统->中间层->唤醒协程。
mrgeneral
2021-03-26 14:38:17 +08:00
@mrgeneral 协程的沉睡和唤醒,就是调度器是否给他分配运行中的线程资源
nevin47
2021-03-26 14:42:49 +08:00
@learningman #2 别误导人。。。。。
Jirajine
2021-03-26 14:45:51 +08:00
wanguorui123
2021-03-26 15:03:00 +08:00
可以了解下进程管理,等待 /阻塞本质上是进程调度的不同状态
IsaacYoung
2021-03-26 15:03:01 +08:00
test and set
learningman
2021-03-26 16:26:50 +08:00
@nevin47 #13 一般这种发言后面应该接上正确的纠正,否则和口嗨有什么区别呢?
Jooooooooo
2021-03-26 16:44:40 +08:00
@nevin47 定时器当然都是轮询, 2l 说的没问题.

最后都是操作系统底层有个轮询帮你完成了定时.
hfc
2021-03-26 17:03:15 +08:00
4 、Java 里面的 wait 并不是单纯的指等待锁,而是因为某些原因(比如等待一个资源)而主动放弃 CPU 时间。这个时候线程如果是 wait 状态,那么需要第三方唤醒才能开始竞争 CPU 时间;如果是 time_wait 状态,那么过一段时间后线程就会主动醒来开始竞争 CPU 时间。而 block 状态指的是竞争锁的状态,此时因为锁已经被其它线程获取到了,当前线程只能进入 block 状态但并不会放弃 CPU 时间。
LeeReamond
2021-03-26 17:21:26 +08:00
@sujin190 我表述有问题,我不是指 java 无法显式进入阻塞,而是我想知道如果翻译成 c/c++代码,这个阻塞是如何实现的,涉及那些操作系统或者硬件级别的操作,我不理解这个过程。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/765341

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX