< 返回版块

LongRiver 发表于 2023-08-30 12:02

我最近在学习tokio,并在写一个练手的项目。遇到一个问题,这里请教下大家。

场景是这样的,这个服务在收到网络请求后,会按惯例生成多个task;并且按照不同的请求内容,还可能继续生成task;总之就是会有很多的task。在task中需要打印日志。不是tracing的那种调试日志,而是自定义格式的业务相关的日志。我的问题就是,怎么让多个task高效的(就是尽量减少竞争)向一个文件里打印日志。

我想到的几个方案如下:

  1. 用OnceCell定义一个全局的Mutex<File>,然后各个task需要打印日志时,就抢锁并打印。这样的竞争应该非常激烈。

  2. 创建一个专门写日志文件的task和配套的channel,其他task需要写日志时就把日志写到channel中。那么问题就在于如何找到channel的Sender tx:

    2.1. 每次创建一个task,都clone一个tx,并作为参数传给task。但是这样代码很麻烦,每个task都要传tx。后续还可能增加其他类型的日志,就要增加其他tx。而并不是每个task都需要所有tx的。

    2.2. 用OnceCell定义一个全局的tx,其他task直接访问这个tx。因为tx在调用send()时并不需要mut,所以我猜这里是没有竞争的(?)。

我觉得上面的2.2方案是最好的。不过看tokio或其他channel的例子里,都是要clone多份tx,并且给每个task传一个tx的,也就是方案2.1。这也就是mpsc里的m的意思吧。所以总感觉方案2.2很山寨。

我的问题是:方案2.2这种OnceCell<Sender>的做法是否有问题?是不是真的没有竞争?是不是最合理最高效的?

评论区

写评论
lithbitren 2023-09-04 02:28

如果不存在卡线程的竞争,大部分情况下同步锁的效率最高,如果流程中可能出现死锁,需要用通道,原则上无脑上crossbeam的同步通道,其他情况下才用tokio相关的异步通道,tokio的一次性通道好像只是语法限定上使代码阅读更清晰,性能上和本身的异步通道似乎没啥差别。

关于通道在多线程以及多协程下的性能对比测试

DDD 2023-08-31 10:14

不需要mut不代表没有竞争

asuper 2023-08-30 22:49

我指的是底层的竞争,刚看了mpsc的源码,sender内部有这么个结构

pub(crate) struct Tx<T, S> {
    inner: Arc<Chan<T, S>>,
}

其中S是一个信号量,就是说不管你上层怎么整,底层是有一个Arc的原子操作,加上信号量来控制多线程同时send时的排队,所以我说该竞争还是竞争。 但是在没有用到多线程的runtime中,异步调度都是在同一线程上进行,那么在每次走到Arc获取锁、信号量acquire时,实际上都不会阻塞,所以我说在这种情况下可能没有竞争。

--
👇
LongRiver: 2.1应该是没有竞争的。(除非说多个线程在同时向channel写消息时可能会有些内部的竞争。这部分应该是高度优化的,可以忽略的)。

我想确认的是2.2有没有竞争?2.2的这种做法是不是很山寨?

另外,你提的两个问题中,第1个是个问题,不过在我的场景下不会去关闭channel所以没有影响。第2个,是用的OnceCell:get()来访问,确实麻烦了些,但会封装到一个函数里,影响不大。

--
👇
asuper: 个人拙见,2.1和2.2没有本质区别,该竞争还是会竞争,不过你的runtime如果不是multithreading的,大概本身不存在竞争问题?不太确定。

另外,选择2.2除了少写几个clone,创建任务的时候看起来干净一点,其实还是有一些缺点

  1. rx原本可以在所有tx drop的时候得到通知,你这里tx不会自然的drop,那么rx没法得到通知,你程序退出时需要用其他机制来做。
  2. 用全局变量还要用unsafe或者其他方式来访问,也麻烦。
yuyidegit 2023-08-30 22:01

2.1 2.2应该差不多tx内部是一个Arc<_>

fakeshadow 2023-08-30 21:18

曾经写过类似的东西来回避原子开销,但实际使用中并没观测到任何提升。

use once_cell::sync::OnceCell;
use std::sync::Arc;

static G: OnceCell<Arc<()>> = OnceCell::new();

thread_local! {
    static L: Arc<()> = G.get_or_init(|| Arc::new(())).clone();
}

fn main() {
    L.with(|_| {});
}
作者 LongRiver 2023-08-30 17:36

2.1应该是没有竞争的。(除非说多个线程在同时向channel写消息时可能会有些内部的竞争。这部分应该是高度优化的,可以忽略的)。

我想确认的是2.2有没有竞争?2.2的这种做法是不是很山寨?

另外,你提的两个问题中,第1个是个问题,不过在我的场景下不会去关闭channel所以没有影响。第2个,是用的OnceCell:get()来访问,确实麻烦了些,但会封装到一个函数里,影响不大。

--
👇
asuper: 个人拙见,2.1和2.2没有本质区别,该竞争还是会竞争,不过你的runtime如果不是multithreading的,大概本身不存在竞争问题?不太确定。

另外,选择2.2除了少写几个clone,创建任务的时候看起来干净一点,其实还是有一些缺点

  1. rx原本可以在所有tx drop的时候得到通知,你这里tx不会自然的drop,那么rx没法得到通知,你程序退出时需要用其他机制来做。
  2. 用全局变量还要用unsafe或者其他方式来访问,也麻烦。
asuper 2023-08-30 15:34

个人拙见,2.1和2.2没有本质区别,该竞争还是会竞争,不过你的runtime如果不是multithreading的,大概本身不存在竞争问题?不太确定。

另外,选择2.2除了少写几个clone,创建任务的时候看起来干净一点,其实还是有一些缺点

  1. rx原本可以在所有tx drop的时候得到通知,你这里tx不会自然的drop,那么rx没法得到通知,你程序退出时需要用其他机制来做。
  2. 用全局变量还要用unsafe或者其他方式来访问,也麻烦。
1 共 7 条评论, 1 页