Come funziona il blocco / sblocco sincronizzato di @ in Objective-C?


201

@Synchronized non utilizza "lock" e "unlock" per ottenere l'esclusione reciproca? Come si blocca / sblocca quindi?

L'output del seguente programma è solo "Hello World".

@interface MyLock: NSLock<NSLocking>
@end

@implementation MyLock

- (id)init {
    return [super init];
}

- (void)lock {
    NSLog(@"before lock");
    [super lock];
    NSLog(@"after lock");
}

- (void)unlock {
    NSLog(@"before unlock");
    [super unlock];
    NSLog(@"after unlock");
}

@end


int main (int argc, const char * argv[]) {
    NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];

    MyLock *lock = [[MyLock new] autorelease];
    @synchronized(lock) {
        NSLog(@"Hello World");
    }

    [pool drain];
}


10
Non è necessario sovrascrivere init se non è necessario. Il runtime chiama automaticamente l'implementazione della superclasse se non si ignora un metodo.
— Constantino Tsarouhas,

3
Una cosa importante da notare è che il codice sopra non è sincronizzato. L' lockoggetto viene creato ad ogni chiamata, quindi non ci sarà mai un caso in cui un @synchronizedblocco ne blocca un altro. E questo significa che non c'è esclusione reciproca.) Naturalmente, l'esempio sopra sta eseguendo l'operazione main, quindi non c'è nulla da escludere comunque, ma non si dovrebbe copiare ciecamente quel codice altrove.
— Hot Licks,

3
Dopo aver letto questa pagina SO, ho deciso di indagare un po 'più a fondo su @synchronized e di scrivere un post sul blog. Potrebbe esserti
— rjkaplan

Risposte:


323

La sincronizzazione a livello di linguaggio Objective-C utilizza il mutex, proprio come NSLockfa. Semanticamente ci sono alcune piccole differenze tecniche, ma è fondamentalmente corretto pensarle come due interfacce separate implementate su un'entità comune (più primitiva).

In particolare con a NSLockhai un blocco esplicito mentre con @synchronizedte hai un blocco implicito associato all'oggetto che stai usando per sincronizzare. Il vantaggio del blocco a livello di lingua è che il compilatore lo comprende in modo da poter gestire i problemi di scoping, ma meccanicamente si comportano sostanzialmente allo stesso modo.

Puoi pensare @synchronizeda una riscrittura del compilatore:

- (NSString *)myString {
  @synchronized(self) {
    return [[myString retain] autorelease];
  }
}

si trasforma in:

- (NSString *)myString {
  NSString *retval = nil;
  pthread_mutex_t *self_mutex = LOOK_UP_MUTEX(self);
  pthread_mutex_lock(self_mutex);
  retval = [[myString retain] autorelease];
  pthread_mutex_unlock(self_mutex);
  return retval;
}

Ciò non è esattamente corretto perché la trasformazione effettiva è più complessa e utilizza blocchi ricorsivi, ma dovrebbe ottenere il punto.


17
Stai anche dimenticando la gestione delle eccezioni che @synchronized fa per te. E a quanto ho capito, gran parte di questo è gestito in fase di esecuzione. Ciò consente l'ottimizzazione su blocchi non contenti, ecc.
— Quinn Taylor

7
Come ho detto, le cose generate in realtà sono più complesse, ma non mi andava di scrivere direttive di sezione per costruire le tabelle di svolgimento DWARF3 ;-)
— Louis Gerbarg

E non posso biasimarti. :-) Nota anche che OS X utilizza il formato Mach-O invece di DWARF.
— Quinn Taylor,

5
Nessuno utilizza DWARF come formato binario. OS X utilizza DWARF per i simboli di debug e utilizza le tabelle di svolgimento DWARF per eccezioni a costo zero
— Louis Gerbarg

7
Per riferimento, ho scritto backend del compilatore per Mac OS X ;-)
— Louis Gerbarg

40

In Objective-C, un @synchronizedblocco gestisce automaticamente il blocco e lo sblocco (nonché eventuali eccezioni). Il runtime genera essenzialmente un NSRecursiveLock associato all'oggetto su cui si sta sincronizzando. Questa documentazione di Apple lo spiega in modo più dettagliato. Questo è il motivo per cui non vedi i messaggi di registro della tua sottoclasse NSLock: l'oggetto su cui sincronizzi può essere qualsiasi cosa, non solo un NSLock.

Fondamentalmente, @synchronized (...)è un costrutto pratico che semplifica il tuo codice. Come la maggior parte delle astrazioni semplificanti, ha un sovraccarico associato (pensalo come un costo nascosto), ed è bene esserne consapevoli, ma le prestazioni grezze non sono probabilmente l'obiettivo supremo quando si usano tali costrutti.


1
Tale link è scaduto. Ecco il link aggiornato: developer.apple.com/library/archive/documentation/Cocoa/…
— Ariel Steiner

31

In realtà

{
  @synchronized(self) {
    return [[myString retain] autorelease];
  }
}

si trasforma direttamente in:

// needs #import <objc/objc-sync.h>
{
  objc_sync_enter(self)
    id retVal = [[myString retain] autorelease];
  objc_sync_exit(self);
  return retVal;
}

Questa API è disponibile da iOS 2.0 e importata utilizzando ...

#import <objc/objc-sync.h>

Quindi non fornisce alcun supporto per gestire in modo pulito le eccezioni generate?
— Dustin,

Questo è documentato da qualche parte?
— jbat100,

6
C'è un tutore sbilanciato lì.
— Potatoswatter l'

@Dustin in realtà lo fa, dai documenti: "Come misura precauzionale, il @synchronizedblocco aggiunge implicitamente un gestore di eccezioni al codice protetto. Questo gestore rilascia automaticamente il mutex nel caso in cui venga generata un'eccezione."
— Pieter,

objc_sync_enter probabilmente utilizzerà pthread mutex, quindi la trasformazione di Louis è più profonda e corretta.
— jack

3

L'implementazione di Apple di @synchronized è open source e può essere trovata qui . Mike Ash ha scritto due post davvero interessanti su questo argomento:

In breve ha una tabella che mappa i puntatori agli oggetti (usando i loro indirizzi di memoria come chiavi) ai pthread_mutex_tblocchi, che sono bloccati e sbloccati secondo necessità.


Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.