Coroutines en Kotlin vs Java Threads

nascar

Se pueden encontrar muchos artículos que te hablan sobre las Corrutinas o Coroutines en Inglés en Kotlin, pero a veces cuesta entender cuales son las reales diferencias con los threads. Por ese motivo en este artículo Coroutines en Kotlin vs Java Threads, vamos a intentar enfocarlo en ver las principales diferencias.

Introducción

Para todos aquellos que llegamos a Kotlin desde Java, asociamos asíncronismo con threads. Así que si es así, seguro que te has pasado multitud de horas analizando bloqueos, condiciones de carrera, modificando los estados compartidos con synchronized etc… Y sí a veces es muy tedioso trabajar con asíncronismo, así que si estas usando kotlin ( o tienes pensado usarlo), la Coroutine tienen algo que ver con el asíncronismo.

La concurrencia, ha existido desde los años 60 y 70, como parte del modelo de concurrencia conocido como «Communicating Sequential Processes». Kotlin utiliza un modelo de concurrencia basado en las primitivas corrutinas y canales, las cuales hacen más sencillo el entendimiento sobre la concurrencia. Así que, resumiendo mucho, podemos decir que una corrutina es una manera de escribir código de manera asíncrona y no bloqueante.

Algo que hay que tener muy en cuenta es que concurrencia no significa en absoluto paralelismo. Podríamos decir que la concurrencia son procesos que se ejecutan de forma independiente, mientras que el paralelismo es la ejecución simultánea de tareas.

Concurrencia no es paralelismo





Diferencias entre Coroutines de Kotlin y Java Threads

Se suele entender una Coroutine como una manera más liviana a los Java Threads, ya que realmente no esta usando threads como se podrían usar en Java, sino que esta usando thread pool en segundo plano. Es decir, es una manera de utilizar asíncronismo, para tener un resultado similar. Una coroutine no está asociada a ningún hilo (thread) en concreto, tal y como pasa en java, la coroutine puede comenzar en un hilo, pararse y seguir en otro.

El encargado de administrar las Coroutines no es el Sistema Operativo, tal y como pasa con los Threads de Java, sino Kotlin Runtime. Cuando se utiliza un Thread y este se duerme, se queda bloqueado durante un período de tiempo. En este período, no esta siendo utilizado ni se esta aprovechando para realizar otras tareas, por lo que hasta que no termine la tarea en la que se encontraba, no podrá ser utilizado. En cambio, en las Corrutinas, podemos suspender la ejecución, y ese hilo que se encontraba en ejecución, podrá volver al pool y ser utilizado por otra Corrutina (Coroutine).

Kotlin Runtime - Coroutines en Kotlin vs Java Threads
Kotlin Runtime

La mejor manera de ver y entender su funcionamiento es verlo gráficamente con ejemplos:

Ejemplo Coroutines y Thread

Vamos a crear una aplicación con Kotlin y para la gestión de las dependencias usaremos maven para ello:

Maven dependencias:

		<dependency>
			<groupId>com.fasterxml.jackson.module</groupId>
			<artifactId>jackson-module-kotlin</artifactId>
		</dependency>
		<dependency>
			<groupId>org.jetbrains.kotlin</groupId>
			<artifactId>kotlin-reflect</artifactId>
		</dependency>
		<dependency>
			<groupId>org.jetbrains.kotlin</groupId>
			<artifactId>kotlin-stdlib-jdk8</artifactId>
		</dependency>

		<dependency>
			<groupId>org.jetbrains.kotlinx</groupId>
			<artifactId>kotlinx-coroutines-core</artifactId>
			<version>1.3.9</version>
		</dependency>

En nuestro fichero de maven añadimos las dependencias de kotlin y la dependencia de corrutinas.

A continuación mostramos el código necesario para poder realizar la prueba con threads de java y corrutinas de kotlin.

Ejemplo con Threads de Java

A continuación vamos a crear un ejemplo para ver el funcionamiento con Threads, en ese caso realizaremos una función que creará un Thread de Java de la manera tradicional con la única diferencia que será en Kotlin ;).

	fun testThread() {
		println("Test with java thread ")
		
		repeat(10) {

			Thread(Runnable {

				println(" Number ${it}: ${Thread.currentThread().name}")

			}).start()

		}

		Thread.sleep(100)
	}

Resultado

Test with java thread 
Number 0: Thread-1
Number 1: Thread-2
Number 2: Thread-3
Number 3: Thread-4
Number 4: Thread-5
Number 5: Thread-6
Number 6: Thread-7
Number 8: Thread-9
Number 9: Thread-10
Number 7: Thread-8

Como podemos ver con los Threads en Java se generará un Thread por ejecución un bucle con 10 ejecuciones serán 10 threads, a parte que como podemos ver el orden no esta controlado.

Ejemplo Corrutina con Delay

En el siguiente ejemplo vamos a bloquear mediante la llamada a delay nuestra corrutina. Para poder ver que después del bloque el trabajo restante es bloqueado en diferentes threads.

	fun testDelay() {

		println("Test to delay a coroutine: ")

		repeat(10) {

			GlobalScope.launch {

				println("Before  execution time $it: ${Thread.currentThread().name}")

				delay(10)

				println("After  execution time $it: ${Thread.currentThread().name}")
			}

		}

		Thread.sleep(200)
	}

Resultado

Test to delay a coroutine: 
Before  execution  0: DefaultDispatcher-worker-1 @coroutine#1
Before  execution  2: DefaultDispatcher-worker-5 @coroutine#3
Before  execution  4: DefaultDispatcher-worker-4 @coroutine#5
Before  execution  5: DefaultDispatcher-worker-2 @coroutine#6
Before  execution  6: DefaultDispatcher-worker-7 @coroutine#7
Before  execution  3: DefaultDispatcher-worker-6 @coroutine#4
Before  execution  7: DefaultDispatcher-worker-8 @coroutine#8
Before  execution  1: DefaultDispatcher-worker-3 @coroutine#2
Before  execution  9: DefaultDispatcher-worker-3 @coroutine#10
Before  execution  8: DefaultDispatcher-worker-7 @coroutine#9
After  execution  2: DefaultDispatcher-worker-4 @coroutine#3
After  execution  6: DefaultDispatcher-worker-8 @coroutine#7
After  execution  4: DefaultDispatcher-worker-1 @coroutine#5
After  execution  3: DefaultDispatcher-worker-3 @coroutine#4
After  execution  0: DefaultDispatcher-worker-6 @coroutine#1
After  execution  7: DefaultDispatcher-worker-2 @coroutine#8
After  execution  5: DefaultDispatcher-worker-7 @coroutine#6
After  execution  1: DefaultDispatcher-worker-5 @coroutine#2
After  execution  9: DefaultDispatcher-worker-3 @coroutine#10
After  execution  8: DefaultDispatcher-worker-3 @coroutine#9

Con este ejemplo podemos ver una de las principales diferencias que tenemos con los Threads de Java. Después del bloqueo, vemos como el resto de la ejecución se hace en threads diferentes.

Ejemplo con Corrutina

En el ejemplo siguiente crearemos una corrutina simple con GlobalScope. Con esta implementación el tiempo de vida de cada corrutina se encontrará limitada por el de la aplicación.

Una vez que hemos ejecutado el test vemos 3 diferentes threads durante su ejecución. El thread pool es DefaultDispatcher-worker.

	fun testCoroutine() {

		println("Coroutine test ")

		repeat(10) {

			GlobalScope.launch {

				println("Hi  Number  ${it}: ${Thread.currentThread().name}")

			}

		}

		Thread.sleep(100)
	}

Resultado

Hi Number  2: DefaultDispatcher-worker-2 @coroutine#3
Hi Number  6: DefaultDispatcher-worker-7 @coroutine#7
Hi Number  7: DefaultDispatcher-worker-8 @coroutine#8
Hi Number  3: DefaultDispatcher-worker-4 @coroutine#4
Hi Number  5: DefaultDispatcher-worker-6 @coroutine#6
Hi Number  1: DefaultDispatcher-worker-3 @coroutine#2
Hi Number  4: DefaultDispatcher-worker-5 @coroutine#5
Hi Number  0: DefaultDispatcher-worker-1 @coroutine#1
Hi Number  8: DefaultDispatcher-worker-3 @coroutine#9
Hi Number  9: DefaultDispatcher-worker-4 @coroutine#10

Como se puede ver los mensajes se han ejecutado de una manera impredecible, aunque como podemos ver que hay threads que se han repetido como el 3 y el 4, esta es una de las diferencias con los Threads de Java, ya que con Threads, habría 10 y no se hubieran reutilizado.

Ejemplo con Corrutina uncofined

En este ejemplo vamos a utilizar un dispatcher uncofined, que lo que hace es ejecutar la corrutina de manera inmediata en el thread y permite que la corrutina se pueda llegar a reanudar en cualquier subproceso que haya utilizado la función de suspensión.

    fun testCoroutineUncofined() {

        repeat(5) {

            GlobalScope.launch(Dispatchers.Unconfined) {

                println("$it: ${Thread.currentThread().name}")
            }
        }

        Thread.sleep(100)
    }

Resultado

0: main @coroutine#1
1: main @coroutine#2
2: main @coroutine#3
3: main @coroutine#4
4: main @coroutine#5

Al cambiar el thread pool para que use el principal thread podemos ver que siempre se va a ejecutar de manera secuencial.

Conclusión

En este artículo Coroutines en Kotlin vs Java Threads, hemos visto las principales diferencias entre usar threads y corrutinas así como su funcionamiento en ejecución. Espero que hay sido de utilidad el artículo como los ejemplos.

Otros artículos que te pueden interesar

Micronaut

¿Qué es Graph¿Que es GraphQL ?

gRPC con Spring


Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *