web-dev-qa-db-fra.com

Puis-je utiliser std :: async sans attendre la limitation future?

Haut niveau
Je souhaite appeler certaines fonctions sans valeur de retour en mode asynchrone sans attendre qu'elles se terminent. Si j'utilise std :: async, le futur objet ne se détruit pas tant que la tâche n'est pas terminée, l'appel n'est pas synchronisé dans mon cas.

Exemple  

void sendMail(const std::string& address, const std::string& message)
{
    //sending the e-mail which takes some time...
}

myResonseType processRequest(args...)
{
    //Do some processing and valuate the address and the message...

    //Sending the e-mail async
    auto f = std::async(std::launch::async, sendMail, address, message);

    //returning the response ASAP to the client
    return myResponseType;

} //<-- I'm stuck here until the async call finish to allow f to be destructed.
  // gaining no benefit from the async call.

Mes questions sont  

  1. Existe-t-il un moyen de surmonter cette limitation?
  2. si (1) est non, devrais-je implémenter une seule fois un fil qui prend ces "zombies" et les attend?
  3. Est-ce que (1) et (2) sont non, y at-il une autre option que de simplement construire mon propre pool de threads?

Remarque:
Je préfère ne pas utiliser l'option thread + detach (suggéré par @ galop1n) car la création d'un nouveau thread a une surcharge que je souhaite éviter. Tout en utilisant std :: async (au moins sur MSVC) utilise un pool de threads interne. 

Merci.

34
Roee Gavirel

Vous pouvez déplacer le futur dans un objet global. Ainsi, lorsque le destructeur du futur local est exécuté, il n'est pas nécessaire d'attendre la fin du thread asynchrone.

std::vector<std::future<void>> pending_futures;

myResonseType processRequest(args...)
{
    //Do some processing and valuate the address and the message...

    //Sending the e-mail async
    auto f = std::async(std::launch::async, sendMail, address, message);

    // transfer the future's shared state to a longer-lived future
    pending_futures.Push_back(std::move(f));

    //returning the response ASAP to the client
    return myResponseType;

}

N.B. Ce n'est pas sûr si le thread asynchrone fait référence à des variables locales dans la fonction processRequest.

Si vous utilisez std::async (au moins sur MSVC), vous utilisez un pool de threads internes.

En réalité, cela est non conforme. La norme indique explicitement que les tâches exécutées avec std::launch::async doivent être exécutées comme dans un nouveau thread, de sorte que les variables locales aux threads ne doivent pas persister d'une tâche à l'autre. Cela ne compte généralement pas cependant.

13
Jonathan Wakely

pourquoi ne commencez-vous pas juste un fil et détachez-vous si vous ne vous souciez pas de rejoindre?

std::thread{ sendMail, address, message}.detach();   

std :: async est lié à la durée de vie de std :: future, il retourne et leur alternative n’est pas autre chose.

Placer std :: future dans une file d'attente lue par un autre thread nécessitera le même mécanisme de sécurité qu'un pool recevant une nouvelle tâche, comme mutex autour du conteneur.

Votre meilleure option est alors un pool de threads pour consommer des tâches directement insérées dans une file d'attente sécurisée pour les threads. Et cela ne dépend pas d'une implémentation spécifique.

Sous une implémentation de pool de threads prenant tous les appels et arguments, les threads effectuent un poling sur la file d'attente. Une meilleure implémentation devrait utiliser des variables de condition ( coliru ):

#include <iostream>
#include <queue>
#include <memory>
#include <thread>
#include <mutex>
#include <functional>
#include <string>

struct ThreadPool {
    struct Task {
        virtual void Run() const = 0;
        virtual ~Task() {};
    };   

    template < typename task_, typename... args_ >
    struct RealTask : public Task {
        RealTask( task_&& task, args_&&... args ) : fun_( std::bind( std::forward<task_>(task), std::forward<args_>(args)... ) ) {}
        void Run() const override {
            fun_();
        }
    private:
        decltype( std::bind(std::declval<task_>(), std::declval<args_>()... ) ) fun_;
    };

    template < typename task_, typename... args_ >
    void AddTask( task_&& task, args_&&... args ) {
        auto lock = std::unique_lock<std::mutex>{mtx_};
        using FinalTask = RealTask<task_, args_... >;
        q_.Push( std::unique_ptr<Task>( new FinalTask( std::forward<task_>(task), std::forward<args_>(args)... ) ) );
    }

    ThreadPool() {
        for( auto & t : pool_ )
            t = std::thread( [=] {
                while ( true ) {
                    std::unique_ptr<Task> task;
                    {
                        auto lock = std::unique_lock<std::mutex>{mtx_};
                        if ( q_.empty() && stop_ ) 
                            break;
                        if ( q_.empty() )
                            continue;
                        task = std::move(q_.front());
                        q_.pop();
                    }
                    if (task)
                        task->Run();
                }
            } );
    }
    ~ThreadPool() {
        {
            auto lock = std::unique_lock<std::mutex>{mtx_};
            stop_ = true;
        }
        for( auto & t : pool_ )
            t.join();
    }
private:
    std::queue<std::unique_ptr<Task>> q_;
    std::thread pool_[8]; 
    std::mutex mtx_;
    volatile bool stop_ {};
};

void foo( int a, int b ) {
    std::cout << a << "." << b;
}
void bar( std::string const & s) {
    std::cout << s;
}

int main() {
    ThreadPool pool;
    for( int i{}; i!=42; ++i ) {
        pool.AddTask( foo, 3, 14 );    
        pool.AddTask( bar, " - " );    
    }
}
4
galop1n

Plutôt que de déplacer le futur dans un objet global (et de gérer manuellement la suppression des futurs non utilisés), vous pouvez le déplacer dans la portée locale de la fonction appelée de manière asynchrone.

"Laissez la fonction asynchrone prendre son propre avenir", pour ainsi dire.

Je suis venu avec ce modèle de wrapper qui fonctionne pour moi (testé sur Windows):

#include <future>

template<class Function, class... Args>
void async_wrapper(Function&& f, Args&&... args, std::future<void>& future,
                   std::future<void>&& is_valid, std::promise<void>&& is_moved) {
    is_valid.wait(); // Wait until the return value of std::async is written to "future"
    auto our_future = std::move(future); // Move "future" to a local variable
    is_moved.set_value(); // Only now we can leave void_async in the main thread

    // This is also used by std::async so that member function pointers work transparently
    auto functor = std::bind(f, std::forward<Args>(args)...);
    functor();
}

template<class Function, class... Args> // This is what you call instead of std::async
void void_async(Function&& f, Args&&... args) {
    std::future<void> future; // This is for std::async return value
    // This is for our synchronization of moving "future" between threads
    std::promise<void> valid;
    std::promise<void> is_moved;
    auto valid_future = valid.get_future();
    auto moved_future = is_moved.get_future();

    // Here we pass "future" as a reference, so that async_wrapper
    // can later work with std::async's return value
    future = std::async(
        async_wrapper<Function, Args...>,
        std::forward<Function>(f), std::forward<Args>(args)...,
        std::ref(future), std::move(valid_future), std::move(is_moved)
    );
    valid.set_value(); // Unblock async_wrapper waiting for "future" to become valid
    moved_future.wait(); // Wait for "future" to actually be moved
}

Je suis un peu surpris que cela fonctionne car je pensais que le destructeur du futur déplacé bloquerait jusqu'à ce que nous quittions async_wrapper. Il devrait attendre que async_wrapper soit de retour mais attend dans la fonction même. Logiquement, cela devrait être une impasse mais ce n'est pas le cas.

J'ai aussi essayé d'ajouter une ligne à la fin de async_wrapper pour vider manuellement le futur objet:

our_future = std::future<void>();

Cela ne bloque pas non plus.

2
Jan Noha

je n'ai aucune idée de ce que je fais, mais cela semble fonctionner:

// :( http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2012/n3451.pdf
template<typename T>
void noget(T&& in)
{
    static std::mutex vmut;
    static std::vector<T> vec;
    static std::thread getter;
    static std::mutex single_getter;
    if (single_getter.try_lock())
    {
        getter = std::thread([&]()->void
        {
            size_t size;
            for(;;)
            {
                do
                {
                    vmut.lock();
                    size=vec.size();
                    if(size>0)
                    {
                        T target=std::move(vec[size-1]);
                        vec.pop_back();
                        vmut.unlock();
                        // cerr << "getting!" << endl;
                        target.get();
                    }
                    else
                    {
                        vmut.unlock();
                    }
                }while(size>0);
                // ¯\_(ツ)_/¯
                std::this_thread::sleep_for(std::chrono::milliseconds(100));
            }
        });
        getter.detach();
    }
    vmut.lock();
    vec.Push_back(std::move(in));
    vmut.unlock();
}

il crée un fil de discussion dédié à chaque type d'avenir que vous lancez (par exemple, si vous donnez un avenir à l'avenir, vous aurez 2 fils. Si vous lui donnez 100 fois l'avenir, vous n'aurez toujours que 2 fils), et quand il y a un avenir avec lequel vous ne voulez pas vous occuper, il suffit de faire notget(fut); - vous pouvez aussi noget(std::async([]()->void{...})); fonctionne très bien, semble-t-il. warning, do not essaie d’obtenir la valeur d’un futur après avoir utilisé noget () dessus. c'est probablement UB et demander des ennuis.

0
hanshenrik