std::future in Klasse noch Threadsafe?



  • Hallo an alle,

    ich habe einen Workerthread, der für bestimmte Aufgaben Parameter verwendet. Zur überprüfung ob eine Aufgabe bereits fertig ist, wollte ich ein std::packeged_task bzw. ein std::future verwenden. Soweit gibt es keine Probleme. Da ich im Falle einer Ausnahme im Workerthread die Behandlung im Mainthread machen möchte, eignet sich std::future::get() natürlich hervorragend. Jedoch möchte ich bei einer Ausnahme gerne eine Fehlermeldung passend zu den verwendeten Parametern generieren. Die Idee war, in der Parameterklasse das Rückgabe-future zu speichern. Ich bin mir jetzt jedoch nicht sicher ob das nicht u.U. Threadunsafe ist.
    Ich habe mal ein Pseudobeispiel erstellt, damit das Problem verständlicher ist:

    #include<functional>
    #include<thread>
    #include<queue>
    #include<mutex>
    #include<condition_variable>
    #include<future>
    #include<memory>
    #include<iostream>
    #include<string>
    
    using namespace std;
    
    queue<function<void()>> tasks; 
    mutex					m;
    condition_variable		cv;
    bool					shutdown{ false };
    
    void worker_function()
    {
    	function<void()> task;
    
    	while( true )
    	{
    		{
    			unique_lock<mutex> lock( m );
    
    			while( tasks.empty() && !shutdown )
    				cv.wait( lock );
    
    			if( shutdown )
    				return;
    
    			task = move( tasks.front() );
    			tasks.pop();
    		}
    
    		task();
    		task = nullptr;
    	}
    }
    
    // Ein simples Data-Objekt als Beispiel
    struct objekt
    {
    	string 				   data; // Parameter
    	std::future<long long> result; // Ergebnis
    };
    
    long long bsp_task( const shared_ptr<objekt>& bsp_data )
    {
    	return stoll( bsp_data->data );
    }
    
    int main()
    {
    	thread worker{ worker_function };
    
    	shared_ptr<objekt> shared_data{ make_shared<objekt>() };
    	shared_data->data = "42";
    
    	auto ptask{ make_shared<std::packaged_task<long long()>>( bind( bsp_task , cref( shared_data ) ) ) };
    
    	shared_data->result = ptask->get_future();
    
    	{
    		lock_guard<mutex> lock( m );
    		tasks.emplace( [ptask] { ( *ptask )(); } );
    	}
    
    	cv.notify_one();
    
    	// Hier ist der eigentliche Knackpunkt:
    	// Ist der Aufruf von result.get() noch threadsafe, obwohl u.U. auf den data-Member zugegriffen wird (in anderen Beispielen evtl. auch schreibend)
    	cout << shared_data->result.get() << endl;
    
    	cin.get();
    
    	{
    		lock_guard<mutex> lock( m );
    		shutdown = true;
    	}
    	cv.notify_one();
    	worker.join();
    
    	return 0;
    }
    


  • Ich hoffe ich habe dich jetzt richtig verstanden und versuche mich mal an einer Beantwortung.
    Der Aufruf von shared_data->result.get() ist sicher, da der Rückgabetyp long long kopiert wird.
    Bei einem Referenztyp als Shared State wäre die Sache kritischer.
    Generell:
    Sobald du auf shared_data->data schreibend in einem weiteren Thread zugreifst hast du einen data race.

    Also:

    ...
     cv.notify_one();
    //der Workerthread greift lesend auf shared_data->data zu
    //der Mainthread greift schreibend auf shared_data->data zu
    // -> Potentieller data race
    shared_data->data = "55";
    
    cout << shared_data->result.get() << endl;
    ...
    

    Wenn du wartest bis der WorkerThread fertig ist und dann auf shared_data->data schreibend zugreifst ist alles gut:

    ...
    cv.notify_one();
    
    cout << shared_data->result.get() << endl;
    //der Workerthread ist mit der Bearbeitung des Tasks fertig.
    //der Mainthread kann sicher auf shared_data->data zugreifen.
    shared_data->data = "55";
    ...
    

Log in to reply