Динамический вызов случайной функции с переменными аргументами

У меня есть список функций с переменными аргументами, и я хочу случайным образом выбрать одну из них во время выполнения и вызвать ее в цикле. Я хочу повысить производительность моего решения.

У меня есть функция, которая вычисляет аргументы на основе некоторой случайности, а затем (должна) возвращать указатель на функцию, которую я мог бы затем вызвать.

pub async fn choose_random_endpoint(
    &self,
    rng: ThreadRng,
    endpoint_type: EndpointType,
) -> impl Future<Output = Result<std::string::String, MyError>> {
    match endpoint_type {
        EndpointType::Type1 => {
            let endpoint_arguments = self.choose_endpoint_arguments(rng);
            let endpoint = endpoint1(&self.arg1, &self.arg2, &endpoint_arguments.arg3);
            endpoint
        }
        EndpointType::Type2 => {
            let endpoint_arguments = self.choose_endpoint_arguments(rng);
            let endpoint = endpoint2(
                &self.arg1,
                &self.arg2,
                &endpoint_arguments.arg3,
                rng.clone(),
            );
            endpoint
        }
        EndpointType::Type3 => {
            let endpoint_arguments = self.choose_endpoint_arguments(rng);
            let endpoint = endpoint3(
                &self.arg1,
                &self.arg2,
                &endpoint_arguments.arg3,
                rng.clone(),
            );
            endpoint
        }
    }
}

Ошибка, которую я получаю,

expected opaque type `impl Future<Output = Result<std::string::String, MyError>>` (opaque type at <src/calls/type1.rs:14:6>)
   found opaque type `impl Future<Output = Result<std::string::String, MyError>>` (opaque type at <src/type2.rs:19:6>)

. Компилятор советует мне await конечные точки, и это решает проблему, но есть ли при этом накладные расходы на производительность?

Внешняя функция:

Предположим, что есть цикл, вызывающий эту функцию:

pub async fn make_call(arg1: &str, arg2: &str) -> Result<String> {
    let mut rng = rand::thread_rng();
    let random_endpoint_type = choose_random_endpoint_type(&mut rng);
    let random_endpoint = choose_random_endpoint(&rng, random_endpoint_type);
    // call the endpoint
    Ok(response)
}

Теперь я хочу вызывать make_call каждые X секунд, но я не хочу, чтобы мой основной поток блокировался во время вызовов конечной точки, так как это дорого. Я полагаю, что правильный подход к этому — порождать новый поток каждые X секунд интервала, этот вызов make_call?

Кроме того, с точки зрения производительности: наличие такого количества клонов на ринге кажется довольно дорогим. Есть ли более производительный способ сделать это?

Пожалуйста, выложите полную ошибку из cargo check, а не из вашей IDE (у rust-analyzer сейчас есть способ ее показать), и если можете, воспроизводимый пример.

Chayim Friedman 30.01.2023 17:01

«это решает проблему, но есть ли накладные расходы на производительность» Имеет ли смысл сравнивать производительность сломанного и работающего кода?

cafce25 30.01.2023 17:04

Почему у вас есть «async» и «impl Future» в качестве возвращаемого типа?

Victor 30.01.2023 17:20
Почему Python в конце концов умрет
Почему Python в конце концов умрет
Последние 20 лет были действительно хорошими для Python. Он прошел путь от "просто языка сценариев" до основного языка, используемого для написания...
1
3
66
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

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

Ошибка, которую вы получаете, не связана с асинхронностью. Это то же самое, что вы получаете, когда пытаетесь вернуть два разных итератора из функции . Ваша функция, как написано, даже не должна быть асинхронной. Я собираюсь удалить async из него, когда он не нужен, но если вам нужен async (например, для реализации асинхронного признака), вы можете добавить его обратно, и он, вероятно, будет работать так же.

Я сократил ваш код до более простого примера с той же проблемой (площадка):

async fn a() -> &'static str {
    "a"
}

async fn b() -> &'static str {
    "b"
}

fn a_or_b() -> impl Future<Output = &'static str> {
    if rand::random() {
        a()
    } else {
        b()
    }
}

Что вы пытаетесь написать

Если вы хотите вернуть трейт, но конкретный тип, реализующий этот трейт, неизвестен во время компиляции, вы можете вернуть трейт-объект. Фьючерсы должны быть Unpin ожидаемыми, поэтому здесь используется закрепленная коробка (игровая площадка).

fn a_or_b() -> Pin<Box<dyn Future<Output = &'static str>>> {
    if rand::random() {
        Box::pin(a())
    } else {
        Box::pin(b())
    }
}

Вам может понадобиться, чтобы тип был чем-то вроде Pin<Box<dyn Future<Output = &'static str> + Send + Sync + 'static>> в зависимости от контекста.

Что вы должны написать

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

async fn a_or_b() -> &'static str {
    if rand::random() {
        a().await
    } else {
        b().await
    }
}

Это концептуально эквивалентно методу Pin<Box>, только без выделения Box. Вместо этого у вас есть непрозрачный тип, который реализует сам себя.

Блокировка

Поведение блокировки у них немного отличается. Future будет блокировать неасинхронные вещи, когда вы его вызываете, в то время как Pin<Box> блокирует неасинхронные вещи, где вы его ожидаете. Вероятно, это в основном случайная генерация.

Блокирующее поведение конечной точки такое же и зависит от того, что там происходит внутри. Он будет блокировать или не блокировать, где бы вы ни ждали.

Если вы хотите, чтобы несколько вызовов async происходили одновременно, вам все равно придется делать это вне функции. Используя среду выполнения tokio, это будет выглядеть примерно так:

use tokio::task;
use futures::future::join_all;
let tasks: Vec<_> = (0..100).map(|_| task::spawn(make_call())).collect();
let results = join_all(tasks).await;

Это также позволяет вам делать другие вещи во время работы фьючерсов между make_call и collect();.

Если что-то внутри вашей функции блокируется, вы хотите создать это с помощью let results (а затем дождаться этого дескриптора), чтобы вызов await в task::spawn_blocking не блокировался.

ГСЧ

Если ваша среда выполнения многопоточная, make_call будет проблемой. Вы можете создать тип, реализующий ThreadRng с помощью from_entropy, и передать его в свои функции. Или вы можете вызвать Rng + Send или даже просто thread_rng куда вам нужно. Это создает новый rng для каждого потока, но будет повторно использовать их при последующих вызовах, поскольку это статический поток, локальный. С другой стороны, если вам не нужна такая случайность, вы можете с самого начала использовать тип rand::random.

Если ваша среда выполнения не является многопоточной, вы сможете пройти Rng + Send полностью, при условии, что средство проверки заимствования достаточно умно. Однако вы не сможете передать его в асинхронную функцию, а затем &mut ThreadRng, поэтому вам придется создать новый внутри этой функции.

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