Как разделить примитивы синхронизации pthread между C++ и Rust?

У меня есть программа на C++ и программа на Rust, и я успешно заставил их общаться через разделяемую память POSIX (C++ и rust).

Сейчас я пытаюсь их синхронизировать. Мне уже удалось создать работающую, но неэффективную примитивную систему с использованием атомарного лога (создание AtomicBool на стороне ржавчины вот так).

Однако мне бы очень хотелось использовать мьютекс/условную переменную для синхронизации между потоками, и именно здесь я застрял.

Кажется, я могу инициализировать его сторону C++, следуя этому примеру почти дословно.

Я попытался перевести это непосредственно в ржавчину:

    let raw_shm = shm.get_shm();

    let mut mtx_attrs = MaybeUninit::<nix::libc::pthread_mutexattr_t>::uninit();
    if unsafe { nix::libc::pthread_mutexattr_init(mtx_attrs.as_mut_ptr()) } != 0 {
        panic!("failed to create mtx_attrs");
    };
    let mtx_attrs = unsafe { mtx_attrs.assume_init() };

    let mut cond_attrs = MaybeUninit::<nix::libc::pthread_condattr_t>::uninit();
    if unsafe { nix::libc::pthread_condattr_init(cond_attrs.as_mut_ptr()) } != 0 {
        panic!("failed to create cond_attrs");
    };
    let cond_attrs = unsafe { cond_attrs.assume_init() };

    if unsafe {
        nix::libc::pthread_mutexattr_setpshared(
            &mtx_attrs as *const _ as *mut _,
            PTHREAD_PROCESS_SHARED,
        )
    } != 0
    {
        panic!("failed to set mtx as process shared");
    };

    if unsafe {
        nix::libc::pthread_condattr_setpshared(
            &cond_attrs as *const _ as *mut _,
            PTHREAD_PROCESS_SHARED,
        )
    } != 0
    {
        panic!("failed to set cond as process shared");
    };

    // I know that these offsets are correct, having used `offsetof` on the C++ side
    let mtx_start = unsafe { &raw_shm.as_slice()[3110416] };
    let mtx = unsafe { &*(mtx_start as *const _ as *const pthread_mutex_t) };
    let cond_start = unsafe { &raw_shm.as_slice()[3110440] };
    let cond = unsafe { &*(cond_start as *const _ as *const pthread_mutex_t) };

    if unsafe {
        nix::libc::pthread_mutex_init(&mtx as *const _ as *mut _, &mtx_attrs as *const _ as *mut _)
    } != 0
    {
        panic!("failed to init mtx");
    };
    if unsafe {
        nix::libc::pthread_cond_init(
            &cond as *const _ as *mut _,
            &cond_attrs as *const _ as *mut _,
        )
    } != 0
    {
        panic!("failed to init cond");
    };

Все это проходит с возвращаемыми значениями 0... пока все хорошо.

Теперь я могу проверить это одним из двух способов:

  1. Я могу запустить тривиальную программу на C++, и она перестанет ждать в condvar:
if (pthread_mutex_lock(&shmp->mutex) != 0)
    throw("Error locking mutex");
if (pthread_cond_wait(&shmp->condition, &shmp->mutex) != 0)
    throw("Error waiting for condition variable");

и в ржавчине:

let sig = unsafe { nix::libc::pthread_cond_signal(&cond as *const _ as *mut _) };
    dbg!(sig);

Несмотря на возврат 0 (т. е. успех), моя программа на C++ не выходит после condvar; он остается в ожидании, как если бы он никогда не получал сигнала.

  1. Я могу установить еще одну тривиальную программу на C++, которая бесконечно сигнализирует условной переменной в цикле:
    for (unsigned int count = 0;; count++) {
        if (pthread_cond_signal(condition) != 0)
            throw("Error")
        // sleep for a bit
    }

а затем в ржавчине, что-то вроде:

    loop {
        if unsafe { nix::libc::pthread_mutex_lock(&mtx as *const _ as *mut _) } > 0 {
            panic!("Failed to acquire lock")
        };
        if unsafe {
            nix::libc::pthread_cond_wait(&cond as *const _ as *mut _, &mtx as *const _ as *mut _)
        } > 0
        {
            panic!("Failed to acquire lock")
        };
    }

Делая это таким образом, вызов блокировки мьютекса выполняется успешно, но я получаю EINVAL на pthread_cond_wait определенный здесь, который я не могу исправить...

Я чувствую, что я близок ... есть мысли о том, как заставить это работать? (это в основном просто доказательство концепции).

Мой комментарий будет не очень полезен. Честно говоря, я думаю только о том, что это не лучшая идея. Хотя искренне желаю удачи.

Miiao 09.02.2023 23:24
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
2
1
65
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Для потомков мне удалось заставить это работать.

Чтобы прояснить, как устроена программа, есть два бинарных файла: один C++ и один rust. Программа Rust порождает программу C++, используя std::process::Command.

Обработка ошибок и импорт опущены для краткости.

  1. Программа rust запускается и создает новый блок разделяемой памяти (удаляя существующий блок, если он существует, чтобы гарантировать, что программа всегда запускается в новом состоянии). Я использую крейт shared_memory для обработки деталей за меня, и он также предоставляет полезные помощники, такие как доступ к необработанному указателю на начало блока памяти.

Блок общей памяти имеет следующую структуру:

#[repr(c)]
struct SharedMemoryLayout {
    ready: std::sync::atomic::AtomicBool,
    mutex: libc::pthread_mutex_t,
    condition: libc::pthread_cond_t,
}

Блоки общей памяти инициализируются нулями, поэтому ready всегда будет false с самого начала.

  1. Программа на ржавчине порождает программу на C++ с std::process::Command::spawn, а затем ждет в цикле, пока ready не станет true.
let proc = Command::new("/path/to/c++/binary").spawn().unwrap();

let ptr: *mut u8 = // pointer to first byte of shared memory block;
let ready: &AtomicBool =  unsafe { &*(ptr as *mut bool as *const AtomicBool) };

loop {
    if ready.load(Ordering::SeqCst) {
        break
    } else {
        thread::sleep(Duration::from_secs(1));
    }
}
  1. Программа C++ открывает блок общей памяти и помещает его в свое локальное адресное пространство.
struct SharedMemoryLayout
{
    std::atomic_bool ready;
    pthread_mutex_t mutex;
    pthread_cond_t condition;
};

int fd = shm_open("name_of_shared_memory_block", O_RDWR, S_IRUSR | S_IWUSR);
struct SharedMemoryLayout *sync = (SharedMemoryLayout *)mmap(NULL, sizeof(*sync), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
  1. Программа C++ продолжает инициализировать mmap и mutex, прежде чем пометить блок памяти как готовый.
pthread_mutexattr_t mutex_attributes;
pthread_condattr_t condition_attributes;

pthread_mutexattr_init(&mutex_attributes);
pthread_condattr_init(&condition_attributes);

pthread_mutexattr_setpshared(&mutex_attributes, PTHREAD_PROCESS_SHARED);
pthread_condattr_setpshared(&condition_attributes, PTHREAD_PROCESS_SHARED);

pthread_mutex_init(&sync->mutex, &mutex_attributes);
pthread_cond_init(&sync->condition, &condition_attributes);

pthread_mutexattr_destroy(&mutex_attributes);
pthread_condattr_destroy(&condition_attributes);

std::atomic_bool *ready = &syncp->ready;
ready->store(true);

А затем введите цикл сигнализации по условию:

for (unsigned int count = 0;; count++) {
    // do something
    sleep(1);
    pthread_cond_signal(&sync->condition);
}
  1. Теперь программа rust будет выпущена из цикла на шаге 2). Материализуйте мьютекс и условие, которые были инициализированы на шаге 4).
let mutex = unsafe {ptr.offset(4) as *mut pthread_mutex_t};
let condition = unsafe {ptr.offset(32) as *mut pthread_cond_t};

И теперь мы можем ждать условия, получая уведомление от программы C++.

loop {
    unsafe {
        pthread_mutex_lock(mutex);
        pthread_cond_wait(condition, mutex);
        pthread_mutex_unlock(mutex);
        
        // Do something
    }
}

Другие вопросы по теме

Избыточный мьютекс. Является ли атомарный объект заменой мьютекса? Генерирует ли мьютекс атомарность, как атомарные объекты?
Мьютекс, используемый в функции, определенной внутри класса, похоже, не работает, когда эта функция вызывается в потоке в основном
Сохраняет ли переключатель контекста (сохраняет состояние переменных) значение переменных при возобновлении задачи?
Pyside6 Многопоточная веб-камера Opencv
Как справиться с бесконечным циклом в потоках и избежать утечки памяти
Чтение глобального флага не работает для обмена данными CPU>GPU в CUDA
Атомарность С++ с использованием memory_order_acquire без соответствующего memory_order_release
Можно ли распараллелить удаление выделения памяти с помощью openmp? (С++)
Как избежать отсоединенного потока, вызывающего утечку памяти, С++
Модель памяти «получить-освободить» для двух последовательных атомарных операций