为啥 Rust RwLock 在 fork 中表现异常?

Posted

技术标签:

【中文标题】为啥 Rust RwLock 在 fork 中表现异常?【英文标题】:Why does Rust RwLock behave unexpectedly with fork?为什么 Rust RwLock 在 fork 中表现异常? 【发布时间】:2020-09-10 13:50:07 【问题描述】:

当我使用 RwLock 和分叉时,我看到了一些我无法解释的行为。基本上,子进程报告 RwLock 仍被获取,而父进程没有,即使它们都运行相同的代码路径。我的理解是子进程应该收到父进程内存空间的独立副本,包括锁,所以他们应该报告不同的结果是没有意义的。

预期的行为是孩子和父母都报告“互斥锁持有:假”。有趣的是,当使用 Mutex 而不是 RwLock 时,这可以正常工作。

Rust Playground link

use libc::fork;
use std::error::Error;
use std::sync::RwLock;

fn main() -> Result<(), Box<dyn Error>> 
    let lock = RwLock::new(());

    let guard = lock.write();
    let res = unsafe  fork() ;
    drop(guard);

    match res 
        0 => 
            let held = lock.try_write().is_err();
            println!("CHILD mutex held: ", held);
        
        _child_pid => 
            let held = lock.try_write().is_err();
            println!("PARENT mutex held: ", held);
        
    
    Ok(())

输出:

PARENT mutex held: false
CHILD mutex held: true

【问题讨论】:

工作原理是这只是未定义的行为:“如果调用线程未持有读写锁 rwlock,则结果未定义。” pubs.opengroup.org/onlinepubs/9699919799/functions/… 看来即使fork出的进程有一个父级内存的副本,但是锁的状态里面肯定有线程id,导致它行为不端。 ^ 与这些同步抽象通常构建在操作系统原语 (std::sync) 之上的事实相结合,这对我来说似乎是一个答案,因此可能会泄露实现细节,尤其是unsafe. 【参考方案1】:

我假设您在这里运行的是 Linux 系统。 Rust 这样做是因为 glibc 这样做,而 Rust 的 RwLock 是基于 glibc 在使用 glibc 的 Linux 系统上的 pthreads 实现。

您可以使用等效的 C 程序来确认此行为:

#include <pthread.h>
#include <unistd.h>
#include <stdio.h>

int main(void)

    pthread_rwlock_t lock = PTHREAD_RWLOCK_INITIALIZER;

    pthread_rwlock_wrlock(&lock);
    pid_t pid = fork();
    int res = pthread_rwlock_unlock(&lock);
    int res2 = pthread_rwlock_trywrlock(&lock);

    printf("%s unlock_errno=%d trywrlock_errno=%d\n", (pid == 0) ? "child" : "parent", res, res2);
    return 0;

打印以下内容:

parent unlock_errno=0 trywrlock_errno=0
child unlock_errno=0 trywrlock_errno=16

16 在我的系统上是EBUSY

glibc 出现这种情况的原因是 POSIX 为 rwlocks 指定了单个解锁函数,而 glibc 存储当前线程 ID 以确定当前线程持有的锁是读锁还是写锁。如果当前线程 ID 等于存储的值,则该线程具有写锁,否则,它具有读锁。所以你实际上并没有解锁孩子的任何东西,但你可能已经破坏了锁中的阅读器计数器。

如 cmets 中所述,根据 POSIX,这是子进程中未定义的行为,因为线程解锁不是持有锁的线程。为了让它工作,Rust 必须像 Go 一样实现自己的同步原语,这通常是一个主要的可移植性噩梦。

【讨论】:

以上是关于为啥 Rust RwLock 在 fork 中表现异常?的主要内容,如果未能解决你的问题,请参考以下文章

为啥||或者在rails中表现不同? [复制]

为啥相同的 MySql 查询在代码和工作台中表现不同?

为啥在具有一级索引的 MultiIndex 列的 pandas DataFrame 中表现不同?

为啥 clojure 的地图在 println 中表现得那样?

为啥“继续”在 Foreach-Object 中表现得像“中断”?

如何在 Rust 中表示指向 C 数组的指针?