Modifica: Come sembra, il problema era che in realtà non creavo un'istanza locale di lock_guard, ma semplicemente un anonimo temporaneo, che è stato distrutto immediatamente, come sottolineato dai commenti sotto.std :: lock_guard che causa un comportamento indefinito
Edit2: L'attivazione del disinfettante per filetti clang aiuta a individuare questi tipi di problemi in fase di esecuzione. Può essere attivata tramite
clang++ -std=c++14 -stdlib=libc++ -fsanitize=thread *.cpp -pthread
Questo è probabilmente in qualche modo una domanda duplicato, ma non ho trovato nulla, quindi se è davvero duplicare mi dispiace. Questa dovrebbe essere una domanda per principianti comunque.
stavo giocando intorno con una semplice classe "Counter", dicono in linea nel file di
Counter.hpp:
#ifndef CLASS_COUNTER_HPP_
#define CLASS_COUNTER_HPP_
#include <mutex>
#include <string>
#include <exception>
class Counter
{
public:
explicit Counter(std::size_t v = 0) : value_{v} {}
std::size_t value() const noexcept { return value_; }
// void increment() { ++value_; } // not an atomic operation : ++value_ equals value_ = value_ + 1
// --> 3 operations: read, add, assign
void increment() noexcept
{
mutex_.lock();
++value_;
mutex_.unlock();
}
// void decrement() noexcept
// {
// mutex_.lock();
// --value_; // possible underflow
// mutex_.unlock();
// }
void decrement()
{
std::lock_guard<std::mutex>{mutex_};
if (value_ == 0)
{
std::string message{"New Value ("+std::to_string(value_-1)+") too low, must be at least 0"};
throw std::logic_error{message};
}
--value_;
}
private:
std::size_t value_;
std::mutex mutex_;
};
#endif
In main.cpp si suppone un'istanza di contatore per essere incrementato e decrementato contemporaneamente:
main.cpp:
#include <iostream>
#include <iomanip>
#include <array>
#include <thread>
#include <exception>
#include "Counter.hpp"
int
main()
{
Counter counter{};
std::array<std::thread,4> threads;
auto operation = [&counter]()
{
for (std::size_t i = 0; i < 125; ++i)
counter.increment();
};
// std::for_each(begin(threads),end(threads),[&operation](auto& val) { val = std::thread{operation}; });
std::cout << "Incrementing Counter (" << std::setw(3) << counter.value() << ") concurrently...";
for (auto& t : threads)
{
t = std::thread{operation};
}
for (auto& t : threads)
t.join();
std::cout << " new value == " << counter.value() << '\n';
auto second_operation = [&counter]()
{
for (std::size_t i = 0; i < 125; ++i)
{
try
{
counter.decrement();
}
catch(const std::exception& e)
{
std::cerr << "\n***Exception while trying to decrement : " << e.what() << "***\n";
}
}
};
std::cout << "Decrementing Counter (" << std::setw(3) << counter.value() << ") concurrently...";
for (auto& t : threads)
t = std::thread{second_operation};
for (auto& t : threads)
t.join();
std::cout << " new value == " << counter.value() << '\n';
return 0;
La gestione delle eccezioni sembra funzionare come dovrebbe e il modo in cui lo capisco è che std :: lock_guard dovrebbe garantire lo sblocco di un mutex una volta che lock_guard esce dall'ambito.
Tuttavia sembra essere più complicato di così. Mentre l'incremento ha come risultato un valore finale di "500", il decremento, che dovrebbe portare a "0", non funziona. Il risultato sarà qualcosa tra "0" e "16" o così.
Se il tempo è cambiato, ad esempio utilizzando valgrind, sembra funzionare correttamente ogni volta.
Sono stato in grado di individuare il problema con l'utilizzo di std :: lock_guard. Se io definisco la funzione di decremento() come questo:
void decrement() noexcept
{
mutex_.lock();
--value_; // possible underflow
mutex_.unlock();
}
tutto funziona bene (a patto che non v'è alcuna underflow). Ma una volta che faccio una semplice modifica a:
void decrement() noexcept
{
std::lock_guard<std::mutex>{mutex_};
--value_; // possible underflow
}
il comportamento è come ho descritto sopra. Presumo di non aver capito il comportamento e di utilizzare i casi di std :: lock_guard. Lo apprezzerei davvero se tu potessi indicarmi la direzione giusta!
Il programma viene compilato tramite clang++ -std=c++14 -stdlib=libc++ *.cpp -pthread
.
@DarkFalcon Questo è ciò che si intende: viene generata un'eccezione il client prova a decrementare il valore se è già 0. Nel modo in cui un'istanza di classe viene inizializzata, può essere solo 0 o superiore (size_t deve essere senza segno in ogni caso), quindi se è 0, dovrebbe semplicemente rimanere così. Questa domanda non riguarda in realtà il tentativo di implementare un contatore perfetto in ogni caso, ma la comprensione di come funziona std :: unique_lock – mbw
È possibile ottenere prestazioni migliori se invece di bloccare il mutex si impiega l'incremento/decremento atomico. – SergeyA