Maison > Java > javaDidacticiel > Sujets : Comment définir et limiter l'exécution visant la performance ?

Sujets : Comment définir et limiter l'exécution visant la performance ?

DDD
Libérer: 2024-12-20 15:23:09
original
302 Les gens l'ont consulté

Threads: Como definir e limitar a execução visando a performance?

Threads, un outil qui aide et devient indispensable au développement de solutions modernes et performantes. Quelle que soit la langue, la capacité d’effectuer des tâches en parallèle est très intéressante. Mais il y a évidemment la célèbre citation d'Oncle Ben : « Un grand pouvoir implique de grandes responsabilités. » Comment utiliser au mieux cette solution, en visant la performance, une meilleure utilisation des ressources et la santé des applications ? Tout d’abord, il est nécessaire de comprendre les concepts de base de ce sujet.

Qu'est-ce qu'un « fil » ?

Les threads sont les unités de base d'exécution d'un processus dans un système d'exploitation. Ils permettent à un programme d'effectuer plusieurs opérations simultanément au sein du même processus. Chaque thread partage le même espace mémoire que le processus principal, mais peut s'exécuter indépendamment, ce qui est utile pour les tâches pouvant être effectuées en parallèle, telles que les opérations d'entrée/sortie (E/S), les calculs complexes ou les mises à jour de données. .

Sur de nombreux systèmes, les threads sont gérés par le système d'exploitation, qui alloue du temps CPU à chaque thread et gère le changement de contexte entre eux. Dans les langages de programmation comme Java, Python et C, il existe des bibliothèques et des frameworks qui facilitent la création et la gestion de threads.

Pourquoi utiliser des fils de discussion ?

Les threads sont principalement utilisés pour améliorer l'efficacité et la réactivité d'un programme. Les raisons d'utiliser les threads, en particulier en se concentrant sur le backend, sont :

  • Parallélisme : les threads vous permettent d'effectuer plusieurs opérations simultanément, en utilisant mieux les ressources CPU disponibles, en particulier sur les systèmes à plusieurs cœurs.

  • Performances : Dans les opérations d'E/S, telles que la lecture et l'écriture de fichiers ou la communication réseau, les threads peuvent aider à améliorer les performances en permettant au programme de continuer à effectuer d'autres tâches en attendant la fin de celles-ci. opérations.

  • Modularité : Les threads peuvent être utilisés pour diviser un programme en parties plus petites et plus gérables, chacune effectuant une tâche spécifique.

Cependant, il est important de gérer les threads avec soin, car une utilisation incorrecte peut entraîner des problèmes tels que des conditions de concurrence critique, des blocages et des difficultés de débogage. Pour une meilleure gestion de ceux-ci, une solution de pool de threads est utilisée.

Qu’est-ce qu’un pool de threads et pourquoi en ai-je besoin ?

Un pool de threads est un modèle de conception logicielle qui implique la création et la gestion d'un pool de threads pouvant être réutilisés pour effectuer des tâches. Au lieu de créer et de détruire à plusieurs reprises des threads pour chaque tâche, un pool de threads maintient un nombre fixe de threads prêts à exécuter des tâches selon les besoins. Cela peut améliorer considérablement les performances des applications qui doivent gérer de nombreuses tâches simultanées. Les points positifs de l'utilisation d'un pool de threads sont :

  • Performances améliorées : La création et la destruction de threads sont une opération coûteuse en termes de ressources. Un pool de threads minimise ce coût en réutilisant les threads existants.

  • Gestion des ressources : Contrôle le nombre de threads en cours d'exécution, évitant ainsi la création excessive de threads qui peuvent surcharger le système.

  • Facilité d'utilisation : Simplifie la gestion des threads, permettant aux développeurs de se concentrer sur la logique de l'application plutôt que sur la gestion des threads.

  • Évolutivité : Aide à faire évoluer les applications pour gérer efficacement un grand nombre de tâches simultanées.

Comment définir la limite de threads dans le pool

Ok, bien sûr, je dois créer un pool de threads pour mieux utiliser cette fonctionnalité, mais une question qui revient rapidement est : "Combien de threads le pool doit-il contenir ?". Suivant la logique de base, plus on est de fous, non ? Si tout peut se faire en parallèle, cela ne tardera pas à se faire, car cela sera plus rapide. Il est donc préférable de ne pas limiter le nombre de threads, ni de définir un nombre élevé, afin que cela ne soit pas un problème. C'est vrai ?
C'est une déclaration juste, alors testons-la. Le code de ce test a été écrit en Kotlin uniquement pour plus de familiarité et de facilité d'écriture des exemples. Ce point est indépendant du langage.
4 exemples ont été réalisés en explorant différentes natures de systèmes. Les exemples 1 et 2 ont été réalisés pour utiliser le CPU, faire beaucoup de calculs, c'est-à-dire avoir un traitement massif. L'exemple 3 est axé sur les E/S, l'exemple étant une lecture d'un fichier et enfin, dans l'exemple 4 c'est une situation d'appels API en parallèle, se concentrant également sur les E/S. Ils ont tous utilisé des pools de tailles différentes, respectivement de 1, 2, 4, 8, 16, 32, 50, 100 et 500 threads. Tous les processus se produisent plus de 500 fois.

Exemple 1 - Code qui calcule combien de nombres premiers il y a entre 1 et 100 000

import kotlinx.coroutines.*
import kotlin.math.sqrt
import kotlin.system.measureTimeMillis

fun isPrime(number: Int): Boolean {
    if (number <= 1) return false
    for (i in 2..sqrt(number.toDouble()).toInt()) {
        if (number % i == 0) return false
    }
    return true
}

fun countPrimesInRange(start: Int, end: Int): Int {
    var count = 0
    for (i in start..end) {
        if (isPrime(i)) {
            count++
        }
    }
    return count
}

@OptIn(DelicateCoroutinesApi::class)
fun main() = runBlocking {
    val rangeStart = 1
    val rangeEnd = 100_000
    val numberOfThreadsList = listOf(1, 2, 4, 8, 16, 32, 50, 100, 500)

    for (numberOfThreads in numberOfThreadsList) {
        val customDispatcher = newFixedThreadPoolContext(numberOfThreads, "customPool")
        val chunkSize = (rangeEnd - rangeStart + 1) / numberOfThreads
        val timeTaken = measureTimeMillis {
            val jobs = mutableListOf<Deferred<Int>>()
            for (i in 0 until numberOfThreads) {
                val start = rangeStart + i * chunkSize
                val end = if (i == numberOfThreads - 1) rangeEnd else start + chunkSize - 1
                jobs.add(async(customDispatcher) { countPrimesInRange(start, end) })
            }
            val totalPrimes = jobs.awaitAll().sum()
            println("Total de números primos encontrados com $numberOfThreads threads: $totalPrimes")
        }
        println("Tempo levado com $numberOfThreads threads: $timeTaken ms")
        customDispatcher.close()
    }
}
Copier après la connexion
Exemple 1 : sortie console
Total de números primos encontrados com 1 threads: 9592
Tempo levado com 1 threads: 42 ms
Total de números primos encontrados com 2 threads: 9592
Tempo levado com 2 threads: 17 ms
Total de números primos encontrados com 4 threads: 9592
Tempo levado com 4 threads: 8 ms
Total de números primos encontrados com 8 threads: 9592
Tempo levado com 8 threads: 8 ms
Total de números primos encontrados com 16 threads: 9592
Tempo levado com 16 threads: 16 ms
Total de números primos encontrados com 32 threads: 9592
Tempo levado com 32 threads: 12 ms
Total de números primos encontrados com 50 threads: 9592
Tempo levado com 50 threads: 19 ms
Total de números primos encontrados com 100 threads: 9592
Tempo levado com 100 threads: 36 ms
Total de números primos encontrados com 500 threads: 9592
Tempo levado com 500 threads: 148 ms
Copier après la connexion

Exemple 2 - Code qui calcule le 30ème nombre sur l'échelle de Fibonacci

import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.launch
import kotlinx.coroutines.newFixedThreadPoolContext
import kotlinx.coroutines.runBlocking
import kotlin.system.measureTimeMillis

fun fibonacci(n: Int): Long {
    return if (n <= 1) n.toLong() else fibonacci(n - 1) + fibonacci(n - 2)
}

@OptIn(DelicateCoroutinesApi::class)
fun main() = runBlocking {
    val numberOfThreadsList = listOf(1, 2, 4, 8, 16, 32, 50, 100, 500)

    for (numberOfThreads in numberOfThreadsList) {
        val customDispatcher = newFixedThreadPoolContext(numberOfThreads, "customPool")
        val numbersToCalculate = mutableListOf<Int>()
        for (i in 1..1000) {
            numbersToCalculate.add(30)
        }
        val timeTaken = measureTimeMillis {
            val jobs = numbersToCalculate.map { number ->
                launch(customDispatcher) {
                    fibonacci(number)
                }
            }
            jobs.forEach { it.join() }
        }
        println("Tempo levado com $numberOfThreads threads: $timeTaken ms")
        customDispatcher.close()
    }
}
Copier après la connexion
Exemple 2 de sortie console
Tempo levado com 1 threads: 4884 ms
Tempo levado com 2 threads: 2910 ms
Tempo levado com 4 threads: 1660 ms
Tempo levado com 8 threads: 1204 ms
Tempo levado com 16 threads: 1279 ms
Tempo levado com 32 threads: 1260 ms
Tempo levado com 50 threads: 1364 ms
Tempo levado com 100 threads: 1400 ms
Tempo levado com 500 threads: 1475 ms
Copier après la connexion

Exemple 3 - Code qui lit un fichier de nombres aléatoires et les ajoute en fin de lecture

import kotlinx.coroutines.*
import java.io.File
import kotlin.system.measureTimeMillis

@OptIn(DelicateCoroutinesApi::class)
fun main() = runBlocking {
    val file = File("numeros_aleatorios.txt")

    if (!file.exists()) {
        println("Arquivo não encontrado!")
        return@runBlocking
    }
    val numberOfThreadsList = listOf(1, 2, 4, 8, 16, 32, 50, 100, 500)
    for (numberOfThreads in numberOfThreadsList) {
        val customDispatcher = newFixedThreadPoolContext(numberOfThreads, "customPool")
        val timeTaken = measureTimeMillis {
            val jobs = mutableListOf<Deferred<Int>>()
            file.useLines { lines ->
                lines.forEach { line ->
                    jobs.add(async(customDispatcher) {
                        processLine(line)
                    })
                }
            }
            val totalSum = jobs.awaitAll().sum()
            println("Total da soma com $numberOfThreads threads: $totalSum")
        }
        println("Tempo levado com $numberOfThreads threads: $timeTaken ms")
        customDispatcher.close()
    }

}

fun processLine(line: String): Int {
    return line.toInt() + 10
}
Copier après la connexion
Exemple 3 de sortie console
Total da soma de 1201 linhas com 1 threads: 60192
Tempo levado com 1 threads: 97 ms
Total da soma de 1201 linhas com 2 threads: 60192
Tempo levado com 2 threads: 28 ms
Total da soma de 1201 linhas com 4 threads: 60192
Tempo levado com 4 threads: 30 ms
Total da soma de 1201 linhas com 8 threads: 60192
Tempo levado com 8 threads: 26 ms
Total da soma de 1201 linhas com 16 threads: 60192
Tempo levado com 16 threads: 33 ms
Total da soma de 1201 linhas com 32 threads: 60192
Tempo levado com 32 threads: 35 ms
Total da soma de 1201 linhas com 50 threads: 60192
Tempo levado com 50 threads: 44 ms
Total da soma de 1201 linhas com 100 threads: 60192
Tempo levado com 100 threads: 66 ms
Total da soma de 1201 linhas com 500 threads: 60192
Tempo levado com 500 threads: 297 ms
Copier après la connexion

Exemple 4 - Code qui appelle une API 500 fois

import io.ktor.client.*
import io.ktor.client.engine.cio.*
import io.ktor.client.request.*
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.launch
import kotlinx.coroutines.newFixedThreadPoolContext
import kotlinx.coroutines.runBlocking
import kotlin.system.measureTimeMillis

@OptIn(DelicateCoroutinesApi::class)
fun main() = runBlocking {
    val client = HttpClient(CIO)

    try {
        val numberOfThreadsList = listOf(1, 2, 4, 8, 16, 32, 50, 100, 500)
        for (numberOfThreads in numberOfThreadsList) {
            val customDispatcher = newFixedThreadPoolContext(numberOfThreads, "customPool")
            val timeTaken = measureTimeMillis {
                repeat(500) {
                    val jobs = launch(customDispatcher) { client.get("http://127.0.0.1:5000/example") }
                    jobs.join()
                }
            }
            println("Tempo levado com $numberOfThreads threads: $timeTaken ms")
            customDispatcher.close()
        }
    } catch (e: Exception) {
        println("Erro ao conectar à API: ${e.message}")
    } finally {
        client.close()
    }
}
Copier après la connexion
Exemple 4 de sortie console
Tempo levado com 1 threads: 7104 ms
Tempo levado com 2 threads: 4793 ms
Tempo levado com 4 threads: 4170 ms
Tempo levado com 8 threads: 4310 ms
Tempo levado com 16 threads: 4028 ms
Tempo levado com 32 threads: 4089 ms
Tempo levado com 50 threads: 4066 ms
Tempo levado com 100 threads: 3978 ms
Tempo levado com 500 threads: 3777 ms
Copier après la connexion

Les exemples 1 à 3 ont un comportement commun, ils deviennent tous plus performants jusqu'à 8 threads, puis le temps de traitement augmente à nouveau, mais pas l'exemple 4, qu'est-ce que cela montre ? N'est-il pas intéressant de toujours utiliser le plus de fils de discussion possible ?

La réponse simple et rapide est non.

Le processeur de ma machine a 8 cœurs, c'est-à-dire qu'il peut effectuer 8 tâches en même temps, de plus le temps augmente à mesure que le temps de gestion des états de chaque thread finit par dégrader les performances.

Ok, cela répond aux exemples 1 à 3, mais qu'en est-il de l'exemple 4 ? Pourquoi les performances s’améliorent-elles à mesure que de threads sont lancés ?

Simple, comme il s'agit d'une intégration, la machine n'a aucun traitement, elle attend essentiellement une réponse, elle reste "en veille" jusqu'à ce que la réponse arrive, donc oui, ici le nombre de threads peut être plus grand. Mais attention, cela ne veut pas dire qu'il peut y en avoir le plus possible, les threads provoquent un épuisement des ressources, les utiliser sans discernement a un effet inverse qui affectera la santé globale du service.
Par conséquent, pour définir le nombre de threads dont disposera votre pool, le moyen le plus simple et le plus sûr est de séparer la nature de la tâche qui sera effectuée. Ils sont séparés en deux :

  • Tâches ne nécessitant pas de traitement :
    Lorsque le type de tâche ne nécessite pas de traitement, plus de threads peuvent être créés qu'il n'y a de cœurs de processeur sur la machine. Cela se produit car il n'est pas nécessaire de traiter les informations pour terminer le thread, essentiellement les threads de cette nature attendent pour la plupart des réponses des intégrations, telles que l'écriture dans une base de données ou une réponse d'une API.

  • Tâches nécessitant un traitement :
    Lorsque la solution comporte un traitement, c'est-à-dire que la machine effectue réellement un travail, le nombre maximum de threads doit être égal au nombre de cœurs du processeur de la machine. En effet, un cœur de processeur est incapable de faire plus d’une chose en même temps. Par exemple, si le processeur sur lequel la solution s'exécute possède 4 cœurs, alors votre pool de threads doit avoir la taille des cœurs de votre processeur, un pool de 4 threads.

Conclusion

Le premier point à définir lorsqu'on réfléchit à un pool de threads n'est pas forcément le nombre qui limitera sa taille, mais plutôt la nature de la tâche effectuée. Les threads aident beaucoup aux performances des services, mais ils doivent être utilisés de la meilleure façon possible afin qu'ils n'aient pas l'effet inverse et ne dégradent pas les performances, ou pire encore, n'affectent pas la santé de l'ensemble du service. Il est clair que les pools plus petits finissent par favoriser les tâches nécessitant beaucoup de traitement, en d’autres termes les tâches limitées par le processeur. Si vous n'êtes pas sûr que la solution dans laquelle les threads seront utilisés a un comportement dans lequel les traitements seront utilisés massivement, faites preuve de prudence, limitez votre pool au nombre de processeurs sur la machine, croyez-moi, cela économisera tu as beaucoup de maux de tête.

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

source:dev.to
Déclaration de ce site Web
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn
Tutoriels populaires
Plus>
Derniers téléchargements
Plus>
effets Web
Code source du site Web
Matériel du site Web
Modèle frontal