< 返回版块

LongRiver 发表于 2023-07-16 11:17

我本来是以为Rust的channel中,sender写了数据后,是通过pthread_cond_signal()来唤醒receiver的。

但刚才写了个示例程序,然后用 strace 执行,并没有发现任何的系统调用,receiver就被唤醒了。

那是用什么方法唤醒的? 多线程间的唤醒,是可以不经过系统调用的吗?

评论区

写评论
hax10 2023-07-17 10:24

是的,并且pthread_cond_signal在常用的glibc库里头也是使用futex实现的,于是性能跟Rust的消息渠道应该不分上下。

--
👇
WuBingzheng: 跟我最开始想的一样,通过 r_waiting 来控制是否需要调用 pthread_cond_signal来唤醒receiver。

作者 LongRiver 2023-07-16 19:35

看有很多star,应该不错。我使用一下。

另外,看了下 发送消息的代码,跟我最开始想的一样,通过 r_waiting 来控制是否需要调用 pthread_cond_signal来唤醒receiver。

--
👇
hax10: 你可以看看chan

--
👇
WuBingzheng:

顺便再问下,你知道C语言有这种成熟的channel库吗?

--

作者 LongRiver 2023-07-16 19:17

我明白你的意思。你是从具体实现的角度来解释的。

我刚才说的 ”因为他知道receiver后续肯定会执行recv()的“ 是从业务逻辑角度理解的。大概就类似于 https://stackoverflow.com/questions/5536759/condition-variable-why-calling-pthread-cond-signal-before-calling-pthread-co 这个问题。

多谢解惑。

--
👇
Pikachu: > 如果此时receiver还没有执行recv(),那么sender都不会调用syscall,我猜应该是因为他知道receiver后续肯定会执行recv()的,届时肯定会检查channel的,于是就不需要syscall来唤醒。

这个理解是错的。它的实际行为是这样的:

hax10 2023-07-16 19:08

你可以看看chan

--
👇
WuBingzheng:

顺便再问下,你知道C语言有这种成熟的channel库吗?

--

Pikachu 2023-07-16 16:02

如果此时receiver还没有执行recv(),那么sender都不会调用syscall,我猜应该是因为他知道receiver后续肯定会执行recv()的,届时肯定会检查channel的,于是就不需要syscall来唤醒。

这个理解是错的。它的实际行为是这样的:

  1. receiver在调用recv或者recv_timeout的时候,会先检查channel里面是否已经有消息。如果有,那么可以不必等待,直接返回;如果没有,那么它会把当前thread加入到selectors中,然后park当前thread,等待将来有消息后被唤醒。(selectors是channel内部的一个Vec,存储了所有待唤醒的thread。)
  2. sender在完成写入之后,会尝试unpark selectors中所有的receivers。如果receiver所在线程是parked状态,那么通过syscall唤醒它;如果receiver所在线程是running状态,那么就可以省去这次syscall。

你所看到的recv调用前,sender不会调用syscall,其原因是receiver所在的线程还没有被加入selectors,而不是因为“后续肯定会recv”。

作者 LongRiver 2023-07-16 14:41

按照你的思路,又改了几次代码然后用strace检查,发现确实比我想象的高级很多。

比如哪怕sender在发送第一条消息的时候,如果此时receiver还没有执行recv(),那么sender都不会调用syscall,我猜应该是因为他知道receiver后续肯定会执行recv()的,届时肯定会检查channel的,于是就不需要syscall来唤醒。

之前一直听说futex很复杂,不建议直接学习。果然如此。

顺便再问下,你知道C语言有这种成熟的channel库吗?

--
👇
Pikachu: 感觉上不太有必要。

unpark的内部实现是这样的:首先对一个atomic变量做swap,如果swap出来的值是PARKED的话,才会调用syscall。也就是说,你想要的“省略不必要的syscall”,标准库已经帮你做掉了。

std::sys_common::thread_parking::futex

当然如果你想进一步省去atomic带来的cache line相关的开销的话,那就超出我的知识范围了。你可以去看一下Rust atomic and locks有没有相关的内容。

Pikachu 2023-07-16 14:21

感觉上不太有必要。

unpark的内部实现是这样的:首先对一个atomic变量做swap,如果swap出来的值是PARKED的话,才会调用syscall。也就是说,你想要的“省略不必要的syscall”,标准库已经帮你做掉了。

std::sys_common::thread_parking::futex

当然如果你想进一步省去atomic带来的cache line相关的开销的话,那就超出我的知识范围了。你可以去看一下Rust atomic and locks有没有相关的内容。

作者 LongRiver 2023-07-16 13:45

多谢回复。

我查这个问题的本意是源于另外一个想法:对于channel的场景,很多时候sender会持续发送多条消息。如果每发送一条就要调用一次syscall的话,是不是有些浪费,这里有没有优化空间?

比如,向一个空channel(即之前所有消息都被消费了)里发消息,肯定是需要syscall来通知对方的。但是,当发送一条消息后,如果检查channel里还有消息没有被消费,那说明之前已经调用过syscall,并且receiver还没消费或者正在消费中。而receiver消费一般都是一个loop一直读channel,读到空为止(而不是被syscall通知了几次就读几次(我猜的))。那后面的这个消息,是不是就不用调用syscall了。因为receiver的loop肯定可以读到最新的这个消息。

这只是一个大概的思路,里面应该有一些边界条件会有数据竞争,但应该也是可以克服的。

这个思路可行吗?

--
👇
Pikachu: 这个时候读源码一定是最容易的。

首先,channel唤醒thread用的是Thread::unpark。在多数系统上,unpark是基于futex实现的。std::sys_common::thread_parking::futex

再进一步看futex的实现,基本就追溯到了syscall。std::sys::unix::futex

我不太确定为什么你用strace看不到syscall,但源码里确实用了syscall。要不你再检查一下你的参数有没有问题,或者gdb单步调试一下试试看?

作者 LongRiver 2023-07-16 13:37

知道了,是我的问题。。。。

strace默认不监控子线程的系统调用,要加-f参数显式指定才行。 加了-f后发现还是有系统调用的。那跟预期相符了。

Pikachu 2023-07-16 13:20

这个时候读源码一定是最容易的。

首先,channel唤醒thread用的是Thread::unpark。在多数系统上,unpark是基于futex实现的。std::sys_common::thread_parking::futex

再进一步看futex的实现,基本就追溯到了syscall。std::sys::unix::futex

我不太确定为什么你用strace看不到syscall,但源码里确实用了syscall。要不你再检查一下你的参数有没有问题,或者gdb单步调试一下试试看?

1 共 10 条评论, 1 页