Command-Query-Separazione e interfacce di sicurezza multithreading

6

Mi piace il comando di separazione delle query modello (da OOSC / Eiffel - in pratica si restituisce un valore o si cambia lo stato della classe, ma non entrambi). Questo rende più facile ragionare sulla classe ed è più facile scrivere classi eccezionalmente sicure.

Ora, con il multi-threading, mi imbatto in un grosso problema: la separazione della query e il comando sostanzialmente invalida il risultato dalla query poiché qualsiasi cosa può accadere tra questi 2.

Quindi la mia domanda è: come gestisci la separazione della query dei comandi in un ambiente multi-thread?

Esempio di chiarimento:

Uno stack con la separazione delle query dei comandi avrebbe i seguenti metodi:

  • push (comando)
  • pop (comando - ma non restituisce un valore)
  • top (query - restituisce il valore)
  • vuoto (query)

Il problema qui è che posso essere svuotato come stato, ma in questo caso non posso contare sul recupero di un elemento, poiché tra la chiamata di vuoto e la chiamata di cima, lo stack potrebbe essere stato svuotato. Lo stesso vale per il pop & superiore. Se ottengo un oggetto che utilizza la parte superiore, non posso essere sicuro che l'elemento che popo sia lo stesso.

Questo può essere risolto usando i lock esterni - ma non è esattamente quello che chiamo design thread-safe.

    
posta Tobias Langner 03.04.2012 - 10:08
fonte

4 risposte

3

Con il multithreading, non è possibile separare query e comandi, a meno che non si disponga di un blocco esterno che serializza le chiamate.

L'approccio abituale è simile a questo:

// query + command
// moves value at top into external thread-local storage 'value'
// may fail and return 'false'
bool ThreadSafeStack::try_pop(Value& value) 
{
    lock_guard guard(m_lock); // or 'using(m_lock)', 'with m_lock:'

    if (m_stack.empty())
       return false;

    value = m_stack.top();
    m_stack.pop();
    return true;
}

Ma puoi usare un trucco: puoi trasformare value in un membro della classe.
Supponi di avere una struttura dati come questa:

struct StackAndLock {
   Stack stack;
   Mutex lock;
}; 

Quindi puoi creare una classe ThreadSafeStack :

class ThreadSafeStack
{
    Stack& m_stack;
    Mutex& m_lock;

    Value m_value;
public:
    ThreadSafeStack(StackAndLock& stackAndLock);

    void push(Value value); // command
    bool try_pop() // command, may fail
    {
        ...
        m_value = m_stack.top();
    }
    Value get_last() { return m_value; } // query
    bool empty(); // query
};

Quindi hai un oggetto StackAndLock condiviso e più oggetti ThreadSafeStack , uno per thread:

void thread_proc(StackAndLock& bundle)
{
     ThreadSafeStack stack(bundle);

     ...
     if (stack.try_pop())
     {
         ... stack.get_last() ...
     }
}

Non posso dire che sia una grande idea, ma sicuramente è la separazione Command-Query.

    
risposta data 03.04.2012 - 13:36
fonte
1

ci sono 2 approcci principali

  1. blocchi di lettura / scrittura esterni come ho detto nei commenti

  2. Comandi "disponibili" come il classico CompareAndSwap

    bool CaS(from,to){
        if(from==state){
            state=to;
            return true;
        }
        return false;
    }
    

    tuttavia ciò può comportare funzioni molto grandi per un flusso di problemi non banale

risposta data 03.04.2012 - 10:36
fonte
1

I tuoi commenti ti hanno aiutato. Proverò una nuova prospettiva. La nuova risposta ha due parti: 1. Alcuni comandi devono restituire il successo o il fallimento. Capisco che tradizionalmente i comandi non dovrebbero restituire valori, ma le cose diventano davvero imbarazzanti se non si consente almeno un risultato di successo booleano. 2. Documentazione. È necessario spiegare agli utenti della propria API che un valore restituito da una query indica semplicemente che il valore esisteva al momento della query e che potrebbe essere stato invalidato immediatamente dopo la query. Quindi una query può essere considerata un fatto assoluto sul passato, ma solo un suggerimento sul futuro.

Per l'esempio dello stack, l'API potrebbe essere simile a

  • push (valore) (comando, sempre ha esito positivo)
  • pop (valore) (comando, ha esito positivo se il valore può essere visualizzato)
  • top () (query, restituisce due cose: valore superiore e valore booleano vuoto. valore non definito se vuoto = vero)

Questo non è un comando pop convenzionale, ma un comando pop senza valore specificato o senza risultati di successo sarebbe inutile. Sarebbe un comando effettuare una modifica allo stack senza che il chiamante abbia modo di sapere con certezza quale cambiamento, se ne è stato fatto. Inoltre, top () deve restituire uno stato sul fatto che esistesse un valore massimo.

La tua implementazione dell'API è responsabile della sincronizzazione dei dati. Il blocco "esterno" (non stavo apprezzando ciò che intendevi per esterno) non è richiesto dagli utenti della tua API. Nella tua implementazione, puoi utilizzare qualsiasi sincronizzazione disponibile nella tua lingua.

    
risposta data 11.04.2012 - 19:33
fonte
0

Risposta breve è utilizzare la sincronizzazione per tutti gli accessi ai dati condivisi.

I blocchi consentono la sincronizzazione e l'utilizzo dei blocchi è un progetto sicuro. Non è un design che ti blocca o che previene errori nel tuo codice di blocco, ma è un design che ti permette di scrivere il codice multithread corretto.

Si noti che la sincronizzazione non è necessaria solo per i comandi di sequenza. Una query può restituire un valore corrotto oppure un comando può corrompere la struttura dei dati. In una situazione con multithreading, tutti gli accessi ai dati condivisi devono essere sincronizzati. Il blocco è un modo valido per farlo e la struttura StackAndLock di Aybx è una tecnica standard. Esistono tecniche di livello inferiore, ad esempio CAS, ma anche con i blocchi, è necessario scrivere codice che utilizza correttamente i blocchi. Le tecniche di livello superiore implicano l'uso di librerie (o meglio ancora lingue) che implementano cose come STM, attori, CSP o altri tipi di trasmissione di messaggi.

    
risposta data 09.04.2012 - 23:43
fonte

Leggi altre domande sui tag