Mutex vs Semaphore: come implementarli _non_ in termini di altro?

5

Recentemente ho dovuto implementare un semaforo usando un mutex e una variabile condizionale (questa combinazione è anche conosciuta come monitor) per un esercizio all'università:

the Semaphore's decrement operation blocks until its counter is more than zero before decrementing,

and the increment operation increments the counter and then notifies one waiting thread.

Tuttavia, ho anche imparato che:

a Mutex is a binary semaphore with the extra restriction that only the thread decremented it can increment it later.

Poiché queste due definizioni sono chiaramente reciprocamente ricorsive, mi chiedo come i semafori e i mutex possano essere implementati direttamente (in pseudocodice), senza usando l'altro tipo di dati nelle loro implementazioni.

    
posta Qqwy 16.01.2017 - 16:25
fonte

2 risposte

14

Penso che la tua domanda riguardi maggiormente le primitive utilizzate per implementarle e la risposta è - dipende. Cercherò di concentrarmi sulle implementazioni moderne.

In primo luogo, Linux utilizza futex syscall ( F ast U serspace mu TEX ). Le due operazioni più comuni sono FUTEX_WAIT :

This operation tests that the value at the futex word pointed to by the address uaddr still contains the expected value val, and if so, then sleeps waiting for a FUTEX_WAKE operation on the futex word.

e FUTEX_WAKE :

This operation wakes at most val of the waiters that are waiting (e.g., inside FUTEX_WAIT) on the futex word at the address uaddr.

È molto importante che FUTEX_WAIT verifichi il valore prima di andare a dormire, poiché ciò garantisce che il thread non si dimentichi dei wake-up.

Usando futex(2) è possibile implementare il mutex che non richiede alcun syscall se il mutex non è contestato, ma è in grado di sospendere altrimenti il thread chiamante. Esempio di seguito:

struct mutex { int ftx; };

enum { UNLOCKED, LOCKED, CONTESTED };

void mutex_init(struct mutex *mtx)
{
    mtx->ftx = UNLOCKED;
}

void mutex_lock(struct mutex *mtx)
{
    if (atomic_cmpxchg(&mtx->ftx, UNLOCKED, LOCKED) == UNLOCKED)
        return;

    while (atomic_xchg(&mtx->ftx, CONTESTED) != UNLOCKED)
        futex_wait(&mtx->ftx, CONTESTED);
}

void mutex_unlock(struct mutex *mtx)
{
    if (atomic_xchg(&mtx->ftx, UNLOCKED) == CONTESTED)
        futex_wake(&mtx->ftx, 1);
}

Il semaforo può essere implementato in questo modo:

struct sema { int ftx; int waiters; };

void sema_init(struct sema *sem)
{
    sem->ftx = 0;
    sem->waiters = 0;
}

bool sema_trywait(struct sema *sem)
{
    int val = atomic_load(&sem->ftx);

    do {
        if (val == 0)
            return false;
   } while ((val = atomic_cmpxchg(&sem->ftx, val, val - 1)) != val);

    return true;
}

void sema_wait(struct sema *sem)
{
    if (sema_trywait(sem))
        return;

    atomic_add(&sem->waiters, 1);

    do {
        futex_wait(&sem->ftx, 0);
    } while (!sema_trywait(sem));

    atomic_sub(&sem->waiters, 1);
}

void sema_post(struct sema *sem, int n)
{
    int val = atomic_load(&sem->ftx);

    while ((val = atomic_cmpxchg(&sem->ftx, val, val + n)) != val)
    { }

    // This accesses semaphore after unlocking. What if it was destroyed?
    // You can try to think about how this can be fixed.
    // I won't put correct implementation here due to higher complexity.
    if (atomic_load(&sem->waiters) != 0)
        futex_wake(&sem->ftx, n); // no point in waking more than 'n'
}

Le variabili di condizione sono più difficili. La complessità e le prestazioni di implementazione dipendono da quanto si vuole attenersi a un'interfaccia "classica", principalmente se si consente "notifica" per essere chiamato al di fuori della sezione critica o no.

La variabile stato di condizione potrebbe dover essere protetta da un mutex separato, quindi è normale che le implementazioni mantengano un altro mutex all'interno di una variabile di condizione. Se si consente di chiamare "notify" solo all'interno della sezione critica, non è richiesto un blocco mutex aggiuntivo, rendendo l'implementazione più semplice e veloce. Tale combinazione di mutex e una variabile di condizione che consente solo di chiamare "notify" dalla sezione critica di solito è chiamata "monitor".

Lingue come Java e C # utilizzano entrambi i monitor anziché i mutex e le variabili di condizione.

Un altro trucco futex(2) è un'operazione FUTEX_REQUEUE che può essere utilizzato per spostare i camerieri da un futex a un altro e può essere utilizzato per rendere la condizione varaible molto efficace.

C'è un buon documento "Futexes Tricky" sui futex scritti da Ulrich Drepper. Spiega come funziona futex(2) e mostra come può essere utilizzato per implementare i primitivi comuni. Puoi trovarlo qui .

Un altro articolo decente che mostra l'utilizzo di futex(2) è qui , ma questa implementazione è troppo "orientata al benchmark".

Come per altri sistemi operativi, oggi la maggior parte fornisce l'equivalente di Linux futex(2) :

  • WaitOnAddress , WakeOnAddress su Windows 8 e versioni successive.
  • _umtx_op(2) su OS X, FreeBSD (mai usato quelli, puoi leggere più qui ).
  • Penso che OpenBSD implementa anche parzialmente futex(2) .

Le versioni precedenti di Windows (7, Vista, XP) sono speciali. Forniscono API alternative non documentate denominate "Eventi con chiave". Proprio come futex(2) , gli eventi con chiave utilizzano la tabella delle code di attesa con hash per indirizzo. Ci sono due operazioni: "aspetta" e "rilascia" (scia). Questo meccanismo garantisce che i wake-up non vengano persi rendendo il blocco delle operazioni di "rilascio", ad es. chiamare i thread è bloccato fino a quando non sveglia effettivamente qualcuno. L'utilizzo di tale API richiede il contatore dei camerieri nello spazio utente. Puoi leggere più qui .

Se vuoi vedere più Eventi con chiave in azione, dovresti controllare le fonti di Wine e la loro implementazione di RTL_SRWLOCK e RTL_CONDITION_VARIABLE .

Gli eventi con chiave sono adatti per le variabili di condizione, rendendo l'implementazione estremamente semplice. La variabile di condizione è solo un contatore da 4 byte dei camerieri.

Il codice seguente è semplificato, puoi controllare l'originale qui .

void WINAPI RtlWakeConditionVariable( RTL_CONDITION_VARIABLE *variable )
{
    if (interlocked_dec_if_nonzero( (int *)&variable->Ptr ))
        NtReleaseKeyedEvent( keyed_event, &variable->Ptr, FALSE, NULL);
}

void WINAPI RtlWakeAllConditionVariable( RTL_CONDITION_VARIABLE *variable )
{
    int val = interlocked_xchg( (int *)&variable->Ptr, 0 );
    while (val-- > 0)
        NtReleaseKeyedEvent( keyed_event, &variable->Ptr, FALSE, NULL);
}

void RtlSleepConditionVariableSRW(
    RTL_CONDITION_VARIABLE *variable, RTL_SRWLOCK *lock)
{
    interlocked_xchg_add( (int *)&variable->Ptr, 1 );
    RtlReleaseSRWLockExclusive( lock );
    NtWaitForKeyedEvent( keyed_event, &variable->Ptr, FALSE, timeout 
    RtlAcquireSRWLockExclusive( lock );
}

Un altro modo è di emulare funzionalità simili a futex dallo spazio utente. ParkingLot di WebKit ne è un esempio. Puoi leggere più qui . Il meccanismo è fondamentalmente uguale a futex(2) , ma poiché è implementato in userspace, consente l'esecuzione di codice utente personalizzato dall'interno del blocco della coda. Ciò consente di memorizzare meno informazioni nell'oggetto di sincronizzazione e di essere globalmente più intelligente con la schedulazione dei thread. Sfortunatamente, le prestazioni in caso di contestazione molto probabilmente saranno più lente, poiché utilizzeranno comunque futex(2) per bloccare il thread chiamante, quindi c'è un ulteriore livello di codice e blocco che deve essere eseguito.

L'implementazione di WebKit non ha equivalenti di FUTEX_REQUEUE , sebbene sia possibile aggiungerla se necessario.

    
risposta data 17.12.2017 - 00:49
fonte
2

Ecco un'altra implementazione mutex basata su futex, che evita il futex_wake spurio:

typedef struct {
    // the number of threads that want to use the mutex at the moment.
    // if any, exactly one of them has the mutex and the rest are blocked
    volatile int num_users;
    // the last owner sets this to 1 when there are waiters when he leaves the mutex;
    // whichever of the waiters manages to change it from 1 to 0, is unblocked
    volatile int wake_signal;
} mutex_t;

void mutex_lock(mutex_t *m)
{
    int spin;

    // buys-wait for a while, hoping the mutex will be released soon.
    // (this should be disabled in single-processor situations)
    for (spin = 1000; spin && m->num_users; spin--) PAUSE();

    // increment the users count (to include us now)
    if (ATOMIC_ADD_AND_FETCH(&m->num_users, 1) == 1)
        // if we changed it from 0 to 1, the mutex is ours
        return;

    // block until signaled
    while (!ATOMIC_SWAP(&m->wake_signal, 0))
        futex(&m->wake_signal, FUTEX_WAIT, 0);
}

void mutex_unlock(mutex_t *m)
{
    // decrement the users count
    if (ATOMIC_ADD_AND_FETCH(&m->num_users, -1) == 0)
        // if we changed from 1 to 0, we were the only user of the mutex so don't signal
        return;

    // signal one waiter
    m->wake_signal = 1;
    futex(&m->wake_signal, FUTEX_WAKE, 1);
}

Il mutex sopra può essere facilmente modificato per usare molti altri primitivi di sistema. Ecco un esempio con pipe unix:

typedef struct {
    volatile int num_users;
    int pipe[2]; // create with the unix pipe() function
} mutex_t;

void mutex_lock(mutex_t *m)
{
    int spin, buf;
    for (spin = 1000; spin && m->num_users; spin--) PAUSE();

    if (ATOMIC_ADD_AND_FETCH(&m->num_users, 1) == 1)
        return;

    read(m->pipe[0], &buf, 4);
}

void mutex_unlock(mutex_t *m)
{
    int buf;

    if (ATOMIC_ADD_AND_FETCH(&m->num_users, -1) == 0)
        return;

    write(m->pipe[1], &buf, 1);
}

E un altro esempio con eventi win32:

typedef struct {
    volatile int num_users;
    HANDLE event; // a win32 auto-reset event
} mutex_t;

void mutex_lock(mutex_t *m)
{
    int spin;
    for (spin = 1000; spin && m->num_users; spin--) PAUSE();

    if (ATOMIC_ADD_AND_FETCH(&m->num_users, 1) == 1)
        return;

    WaitForSingleObject(m->event, INFINITE);
}

void mutex_unlock(mutex_t *m)
{
    if (ATOMIC_ADD_AND_FETCH(&m->num_users, -1) == 0)
        return;

    SetEvent(m->event);
}
    
risposta data 01.11.2018 - 13:35
fonte

Leggi altre domande sui tag