Primeros pasos con subprocesos múltiples
Interbloqueos
Un interbloqueo ocurre cuando cada miembro de un grupo de dos o más subprocesos debe esperar a que uno de los otros miembros haga algo (por ejemplo, para liberar un bloqueo) antes de que pueda continuar. Sin intervención, los hilos esperarán para siempre.
Un ejemplo de pseudocódigo de un diseño propenso a interbloqueos es:
thread_1 {
acquire(A)
...
acquire(B)
...
release(A, B)
}
thread_2 {
acquire(B)
...
acquire(A)
...
release(A, B)
}
Un interbloqueo puede ocurrir cuando thread_1
ha adquirido A
, pero aún no B
, y thread_2
ha adquirido B
, pero no A
. Como se muestra en el siguiente diagrama, ambos subprocesos esperarán para siempre.
Cómo evitar interbloqueos
Como regla general, minimice el uso de bloqueos y minimice el código entre el bloqueo y el desbloqueo.
Adquirir candados en el mismo orden
Un rediseño de thread_2
resuelve el problema:
thread_2 {
acquire(A)
...
acquire(B)
...
release(A, B)
}
Ambos hilos adquieren los recursos en el mismo orden, evitando así interbloqueos.
Esta solución se conoce como la “solución de jerarquía de recursos”. Fue propuesto por Dijkstra como una solución al “problema de los filósofos del comedor”.
A veces, incluso si especifica un orden estricto para la adquisición de bloqueos, dicho orden de adquisición de bloqueos estáticos se puede hacer dinámico en tiempo de ejecución.
Considere el siguiente código:
void doCriticalTask(Object A, Object B){
acquire(A){
acquire(B){
}
}
}
Aquí, incluso si el orden de adquisición de bloqueo parece seguro, puede causar un interbloqueo cuando thread_1 accede a este método con, por ejemplo, Object_1 como parámetro A y Object_2 como parámetro B y thread_2 lo hace en orden opuesto, es decir, Object_2 como parámetro A y Object_1 como parámetro B.
En tal situación, es mejor tener alguna condición única derivada usando Object_1 y Object_2 con algún tipo de cálculo, p. usando código hash de ambos objetos, por lo que cada vez que un subproceso diferente ingresa en ese método en cualquier orden paramétrico, cada vez que esa condición única derivará en el orden de adquisición de bloqueo.
p.ej. Say Object tiene una clave única, p. número de cuenta en caso de objeto Cuenta.
void doCriticalTask(Object A, Object B){
int uniqueA = A.getAccntNumber();
int uniqueB = B.getAccntNumber();
if(uniqueA > uniqueB){
acquire(B){
acquire(A){
}
}
}else {
acquire(A){
acquire(B){
}
}
}
}
Condiciones de carrera
Una carrera de datos o [condición de carrera] (https://en.wikipedia.org/wiki/Race_condition) es un problema que puede ocurrir cuando un programa multiproceso no está sincronizado correctamente. Si dos o más subprocesos acceden a la misma memoria sin sincronización, y al menos uno de los accesos es una operación de ’escritura’, se produce una carrera de datos. Esto conduce a un comportamiento del programa dependiente de la plataforma y posiblemente incoherente. Por ejemplo, el resultado de un cálculo podría depender de la programación del subproceso.
[Problema de lectores-escritores] (https://en.wikipedia.org/wiki/Readers%E2%80%93writers_problem):
writer_thread {
write_to(buffer)
}
reader_thread {
read_from(buffer)
}
Una solución sencilla:
writer_thread {
lock(buffer)
write_to(buffer)
unlock(buffer)
}
reader_thread {
lock(buffer)
read_from(buffer)
unlock(buffer)
}
Esta solución simple funciona bien si solo hay un hilo lector, pero si hay más de uno, ralentiza la ejecución innecesariamente, porque los hilos lectores podrían leer simultáneamente.
Una solución que evite este problema podría ser:
writer_thread {
lock(reader_count)
if(reader_count == 0) {
write_to(buffer)
}
unlock(reader_count)
}
reader_thread {
lock(reader_count)
reader_count = reader_count + 1
unlock(reader_count)
read_from(buffer)
lock(reader_count)
reader_count = reader_count - 1
unlock(reader_count)
}
Tenga en cuenta que reader_count
está bloqueado durante toda la operación de escritura, de modo que ningún lector puede comenzar a leer mientras la escritura no haya terminado.
Ahora, muchos lectores pueden leer simultáneamente, pero puede surgir un nuevo problema: es posible que reader_count
nunca llegue a 0
, de modo que el subproceso de escritura nunca pueda escribir en el búfer. Esto se llama inanición, existen diferentes soluciones para evitarlo.
Incluso los programas que pueden parecer correctos pueden ser problemáticos:
boolean_variable = false
writer_thread {
boolean_variable = true
}
reader_thread {
while_not(boolean_variable)
{
do_something()
}
}
Es posible que el programa de ejemplo no termine nunca, ya que es posible que el subproceso del lector nunca vea la actualización del subproceso del escritor. Si, por ejemplo, el hardware usa cachés de CPU, los valores pueden almacenarse en caché. Y dado que una escritura o lectura en un campo normal no conduce a una actualización de la memoria caché, es posible que el hilo de lectura nunca vea el valor modificado.
C++ y Java definen en el llamado modelo de memoria, lo que significa sincronizado correctamente: Modelo de memoria C++ , Modelo de memoria Java.
En Java, una solución sería declarar el campo como volátil:
volatile boolean boolean_field;
En C++ una solución sería declarar el campo como atómico:
std::atomic<bool> data_ready(false)
Una carrera de datos es un tipo de condición de carrera. Pero no todas las condiciones de carrera son carreras de datos. Lo siguiente llamado por más de un subproceso conduce a una condición de carrera pero no a una carrera de datos:
class Counter {
private volatile int count = 0;
public void addOne() {
i++;
}
}
Está correctamente sincronizado según la especificación Java Memory Model, por lo tanto no es carrera de datos. Pero aún conduce a condiciones de carrera, p. el resultado depende del entrelazado de los hilos.
No todas las carreras de datos son errores. Un ejemplo de una condición de carrera benigna es sun.reflect.NativeMethodAccessorImpl:
class NativeMethodAccessorImpl extends MethodAccessorImpl {
private Method method;
private DelegatingMethodAccessorImpl parent;
private int numInvocations;
NativeMethodAccessorImpl(Method method) {
this.method = method;
}
public Object invoke(Object obj, Object[] args)
throws IllegalArgumentException, InvocationTargetException
{
if (++numInvocations > ReflectionFactory.inflationThreshold()) {
MethodAccessorImpl acc = (MethodAccessorImpl)
new MethodAccessorGenerator().
generateMethod(method.getDeclaringClass(),
method.getName(),
method.getParameterTypes(),
method.getReturnType(),
method.getExceptionTypes(),
method.getModifiers());
parent.setDelegate(acc);
}
return invoke0(method, obj, args);
}
...
}
Aquí el rendimiento del código es más importante que la exactitud del recuento de numInvocación.
Hello Multithreading - Creando nuevos hilos
Este sencillo ejemplo muestra cómo iniciar varios subprocesos en Java. Tenga en cuenta que no se garantiza que los subprocesos se ejecuten en orden, y el orden de ejecución puede variar para cada ejecución.
public class HelloMultithreading {
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
Thread t = new Thread(new MyRunnable(i));
t.start();
}
}
public static class MyRunnable implements Runnable {
private int mThreadId;
public MyRunnable(int pThreadId) {
super();
mThreadId = pThreadId;
}
@Override
public void run() {
System.out.println("Hello multithreading: thread " + mThreadId);
}
}
}
¿Se puede ejecutar dos veces el mismo subproceso?
La pregunta más frecuente era que un mismo subproceso se puede ejecutar dos veces.
La respuesta a esto es saber que un subproceso puede ejecutarse solo una vez.
si intenta ejecutar el mismo subproceso dos veces, se ejecutará la primera vez, pero dará un error la segunda vez y el error será IllegalThreadStateException.
ejemplo:
public class TestThreadTwice1 extends Thread{
public void run(){
System.out.println("running...");
}
public static void main(String args[]){
TestThreadTwice1 t1=new TestThreadTwice1();
t1.start();
t1.start();
}
}
producción:
running
Exception in thread "main" java.lang.IllegalThreadStateException
Objetivo
Los subprocesos son las partes de bajo nivel de un sistema informático en el que se produce el procesamiento de comandos. Es compatible/proporcionado por hardware de CPU/MCU. También hay métodos de software. El propósito de subprocesos múltiples es hacer cálculos en paralelo entre sí si es posible. Por lo tanto, el resultado deseado se puede obtener en un intervalo de tiempo más pequeño.