我想使用无锁队列编写多线程安全记录器。记录线程将消息推送到队列,记录器将弹出它们并发送到输出。我考虑如何解决该问题 - 发送到输出。 我想尽可能避免使用互斥锁/锁。 所以,让我们假设我将使用C ++流写入文件/控制台。我们可以假设目标系统是Linux。
好的,写入流必须只是Unix write
提供的系统调用的包装器(可能是高级包装器)。据我所知,系统调用是原子的(只有一个进程可以同时执行系统调用)。因此,很难不使用锁来安全地写入文件。
但write
是一个系统调用,但它并不能保证写入"整个输出"。它返回成功写入文件的字节数。
基本上,我的问题是: 怎么解决?是否可以避免互斥? (我认为这是不可能的)。请注意我的考虑,我错了吗?
答案 0 :(得分:2)
Igor是对的:只需要一个线程执行所有日志写入。请记住,内核必须执行锁定以同步对打开文件描述符(跟踪文件位置)的访问,因此通过从多个内核执行写操作会导致内核中的争用。更糟糕的是,您正在从多个内核进行系统调用,这意味着内核的代码/数据访问会破坏多个内核上的缓存。
有关在系统调用完成后进行系统调用对用户空间代码性能的影响的更多信息,请参阅this paper。 (关于不常见的系统调用,内核中的数据/指令缓存未命中)。让一个线程执行所有系统调用(至少是所有写系统调用),将进程的足迹部分隔离到一个核心,这是绝对有意义的。以及内核中的锁定争用。
FlexSC论文是关于批量系统调用以减少用户 - >内核 - >用户转换的想法,但它们也测量正常同步系统调用方法的开销。更重要的是通过系统调用来讨论缓存污染。
或者,如果您可以让多个线程写入您的日志文件,您可以这样做,而不是完全使用该队列。
不能保证大写不会中断,但是中小型写应该(几乎?)总是在大多数操作系统上复制整个缓冲区。特别是如果你写的是文件,而不是管道。 IDK Linux write()在被抢占时的表现如何,但我希望它通常会恢复完成写入而不是在没有写入所有请求的字节的情况下返回。当被信号中断时,部分写入可能更有可能。
保证来自两个write()
系统调用的字节不会混合在一起;一个字节中的所有字节将位于另一个字节之前或之后。不过,你是正确的,部分写入是一个潜在的问题。我忘记了glibc系统调用包装器是否会在EINTR
上为你恢复通话。虽然在这种情况下,它意味着实际上没有写入字节,或者它会以字节数返回成功。
您应该对此进行测试,以进行部分写入和性能测试。内核空间锁定可能比无锁队列的开销便宜,但是从生成日志消息的每个线程进行系统调用可能会对性能造成影响。 (当你测试这个时,请确保你在用户空间进程中发生了一些真正的工作,而不仅仅是只调用写入的循环。)