ios - ¿@Synchronized garantiza la seguridad del hilo o no?
objective-c macos (6)
@synchronized solo no hace que el código sea seguro, pero es una de las herramientas que se utilizan para escribir el código de seguridad de subprocesos.
Con los programas de subprocesos múltiples, a menudo es el caso de una estructura compleja que desea mantener en un estado constante y desea que solo un subproceso tenga acceso a la vez. El patrón común es usar un mutex para proteger una sección crítica del código donde se accede y / o modifica la estructura.
Con referencia a esta answer , me pregunto si esto es correcto.
@synchronized no hace que ningún código sea "seguro para subprocesos"
Como traté de encontrar cualquier documentación o enlace para apoyar esta declaración, no tuve éxito.
Cualquier comentario y / o respuesta será apreciado en esto.
Para una mejor seguridad de los hilos, podemos buscar otras herramientas, esto es algo que conozco.
Creo que la esencia de la pregunta es:
¿Es el uso adecuado de sincronizar capaz de resolver cualquier problema de seguridad de subprocesos?
Técnicamente sí, pero en la práctica es aconsejable aprender y usar otras herramientas.
Responderé sin asumir conocimiento previo.
El código correcto es código que se ajusta a su especificación. Una buena especificación define
- invariantes que limitan el estado,
- condiciones previas y postcondiciones que describen los efectos de las operaciones.
El código Thread-safe es un código que permanece correcto cuando se ejecuta por múltiples hilos. Así,
- Ninguna secuencia de operaciones puede violar la especificación. 1
- Las invariantes y las condiciones se mantendrán durante la ejecución de múltiples subprocesos sin requerir sincronización adicional por parte del cliente 2 .
El punto clave de alto nivel es: la seguridad de subprocesos requiere que la especificación sea verdadera durante la ejecución de múltiples subprocesos. Para realmente codificar esto, tenemos que hacer una sola cosa: regular el acceso al estado compartido mutable 3 . Y hay tres formas de hacerlo:
- Prevenir el acceso
- Haz que el estado sea inmutable.
- Sincroniza el acceso.
Los primeros dos son simples. El tercero requiere evitar los siguientes problemas de seguridad de hilos:
- vida
- punto muerto : dos hilos bloquean permanentemente esperando el uno al otro para liberar un recurso necesario.
- livelock : un hilo está ocupado pero no puede hacer ningún progreso.
- inanición : a un hilo se le niega perpetuamente el acceso a los recursos que necesita para progresar.
- publicación segura : tanto la referencia como el estado del objeto publicado deben hacerse visibles para otros hilos al mismo tiempo.
- condiciones de carrera Una condición de carrera es un defecto donde la salida depende del tiempo de eventos incontrolables. En otras palabras, una condición de carrera ocurre cuando obtener la respuesta correcta se basa en el momento afortunado. Cualquier operación compuesta puede sufrir una condición de carrera, por ejemplo: "verificar y actuar", "poner-si-ausente". Un problema de ejemplo sería
if (counter) counter--;
, y una de varias soluciones sería@synchronize(self){ if (counter) counter--;}
.
Para resolver estos problemas, utilizamos herramientas como @synchronize
, volátil, barreras de memoria, operaciones atómicas, bloqueos específicos, colas y sincronizadores (semáforos, barreras).
Y volviendo a la pregunta:
¿Es el uso adecuado de @synchronize capaz de resolver cualquier problema de subprocesos?
Técnicamente sí, porque cualquier herramienta mencionada anteriormente se puede emular con @synchronize
. Pero daría como resultado un bajo rendimiento y aumentaría la posibilidad de problemas relacionados con la vida. En cambio, debe usar la herramienta adecuada para cada situación. Ejemplo:
counter++; // wrong, compound operation (fetch,++,set)
@synchronize(self){ counter++; } // correct but slow, thread contention
OSAtomicIncrement32(&count); // correct and fast, lockless atomic hw op
En el caso de la pregunta vinculada, puede usar @synchronize
, o un bloqueo de lectura y escritura GCD, o crear una colección con lock pecking, o lo que sea que la situación requiera. La respuesta correcta depende del patrón de uso. De cualquier forma que lo haga, debe documentar en su clase qué garantías seguras están ofreciendo.
1 Es decir, ver el objeto en un estado no válido o violar las condiciones pre / post.
2 Por ejemplo, si el hilo A itera una colección X, y el hilo B elimina un elemento, la ejecución falla. Esto no es seguro para subprocesos porque el cliente tendrá que sincronizarse en el bloqueo intrínseco de X ( synchronize(X)
) para tener acceso exclusivo. Sin embargo, si el iterador devuelve una copia de la colección, la colección se convierte en thread-safe.
3 Los objetos no compartidos de estado compartido o mutables no compartibles siempre son seguros para subprocesos.
Generalmente, @synchronized
garantiza la seguridad del hilo, pero solo cuando se usa correctamente. También es seguro adquirir el bloqueo de forma recursiva, aunque con limitaciones que detallo en mi respuesta here .
Hay varias maneras comunes de usar @synchronized
wrong. Estos son los mas comunes:
Usando @synchronized
para asegurar la creación de objetos atómicos.
- (NSObject *)foo {
@synchronized(_foo) {
if (!_foo) {
_foo = [[NSObject alloc] init];
}
return _foo;
}
}
Debido a que _foo
será nulo cuando se adquiere por primera vez el bloqueo, no se producirá ningún bloqueo y múltiples subprocesos potencialmente pueden crear su propio _foo
antes de que el primero se complete.
Usando @synchronized
para bloquear un nuevo objeto cada vez.
- (void)foo {
@synchronized([[NSObject alloc] init]) {
[self bar];
}
}
He visto este código bastante, así como el lock(new object()) {..}
equivalente C # lock(new object()) {..}
. Como intenta bloquear un objeto nuevo cada vez, siempre estará permitido en la sección crítica del código. Este no es un tipo de código mágico. No hace absolutamente nada para garantizar la seguridad del hilo.
Por último, bloquearse en self
.
- (void)foo {
@synchronized(self) {
[self bar];
}
}
Aunque no es un problema en sí mismo, si su código usa un código externo o es una biblioteca, puede ser un problema. Mientras internamente el objeto se conoce como self
, externamente tiene un nombre de variable. Si el código externo llama a @synchronized(_yourObject) {...}
y llama a @synchronized(self) {...}
, puede encontrarse en un punto muerto. Lo mejor es crear un objeto interno para bloquear que no esté expuesto fuera de su objeto. Añadiendo _lockObject = [[NSObject alloc] init];
dentro de tu función init es barato, fácil y seguro.
EDITAR:
Todavía me hacen preguntas sobre esta publicación, así que aquí hay un ejemplo de por qué es una mala idea usar @synchronized(self)
en la práctica.
@interface Foo : NSObject
- (void)doSomething;
@end
@implementation Foo
- (void)doSomething {
sleep(1);
@synchronized(self) {
NSLog(@"Critical Section.");
}
}
// Elsewhere in your code
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
Foo *foo = [[Foo alloc] init];
NSObject *lock = [[NSObject alloc] init];
dispatch_async(queue, ^{
for (int i=0; i<100; i++) {
@synchronized(lock) {
[foo doSomething];
}
NSLog(@"Background pass %d complete.", i);
}
});
for (int i=0; i<100; i++) {
@synchronized(foo) {
@synchronized(lock) {
[foo doSomething];
}
}
NSLog(@"Foreground pass %d complete.", i);
}
Debería ser obvio ver por qué sucede esto. Bloquear en foo
y lock
se invocan en diferentes órdenes en los hilos de fondo VS en primer plano. Es fácil decir que esto es una mala práctica, pero si Foo
es una biblioteca, es poco probable que el usuario sepa que el código contiene un bloqueo.
La directiva @synchronized
es una forma conveniente de crear bloqueos mutex sobre la marcha en código Objective-C.
efectos secundarios de los bloqueos mutex:
- puntos muertos
- inanición
La seguridad del subproceso dependerá del uso del bloque @synchronized
.
@synchronized
es thread safe
mecanismo thread safe
. La pieza de código escrita dentro de esta función se convierte en la parte de critical section
, a la que solo se puede ejecutar un hilo a la vez.
@synchronize
aplica el bloqueo implícitamente mientras que NSLock
aplica explícitamente.
Solo asegura la seguridad del hilo, no garantiza eso. Lo que quiero decir es que contratas un conductor experto para tu auto, pero no garantiza que el auto no sufra un accidente. Sin embargo, la probabilidad sigue siendo la más mínima.
Es compañero en GCD
(gran despacho central) es dispatch_once
. dispatch_once hace el mismo trabajo que @synchronized
.
@synchronized
hace que el código sea seguro si se usa correctamente.
Por ejemplo:
Digamos que tengo una clase que accede a una base de datos no segura para subprocesos. No quiero leer y escribir en la base de datos al mismo tiempo, ya que es probable que se produzca un bloqueo.
Entonces digamos que tengo dos métodos. storeData: y readData en una clase singleton llamada LocalStore.
- (void)storeData:(NSData *)data
{
[self writeDataToDisk:data];
}
- (NSData *)readData
{
return [self readDataFromDisk];
}
Ahora si tuviera que enviar cada uno de estos métodos a su propio hilo de la siguiente manera:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[[LocalStore sharedStore] storeData:data];
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[[LocalStore sharedStore] readData];
});
Es probable que tengamos un accidente. Sin embargo, si cambiamos nuestros métodos storeData y readData para usar @synchronized
- (void)storeData:(NSData *)data
{
@synchronized(self) {
[self writeDataToDisk:data];
}
}
- (NSData *)readData
{
@synchronized(self) {
return [self readDataFromDisk];
}
}
Ahora este código sería seguro para subprocesos. Es importante tener en cuenta que si @synchronized
una de las sentencias @synchronized
, el código ya no será seguro para subprocesos. O si tuviera que sincronizar diferentes objetos en lugar de self
.
@synchronized
crea un bloqueo mutex en el objeto que está sincronizando. Entonces, en otras palabras, si un código quiere acceder al código en un @synchronized(self) { }
, tendrá que alinearse detrás de todos los códigos anteriores que se ejecutan en ese mismo bloque.
Si tuviéramos que crear diferentes objetos localStore, el @synchronized(self)
solo bloquearía cada objeto individualmente. ¿Tiene sentido?
Piensa en esto, de esta manera. Usted tiene un montón de gente esperando en líneas separadas, cada línea está numerada del 1 al 10. Puedes elegir en qué línea quieres que espere cada persona (sincronizando línea por línea), o si no usas @synchronized
puedes saltar directamente al frente y saltar todas las líneas. Una persona en la línea 1 no tiene que esperar a que termine una persona en la línea 2, pero la persona en la línea 1 tiene que esperar a que todos en frente de ellos en la fila terminen.