Heim > Java > javaLernprogramm > Themen: Wie definiert und begrenzt man die Ausführung mit dem Ziel der Leistung?

Themen: Wie definiert und begrenzt man die Ausführung mit dem Ziel der Leistung?

DDD
Freigeben: 2024-12-20 15:23:09
Original
256 Leute haben es durchsucht

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

Threads, ein Tool, das bei der Entwicklung moderner, leistungsstarker Lösungen hilft und unverzichtbar wird. Unabhängig von der Sprache ist die Möglichkeit, Aufgaben parallel ausführen zu können, von großem Reiz. Aber natürlich gibt es da noch das berühmte Zitat von Onkel Ben: „Mit großer Macht geht große Verantwortung einher.“ Wie kann diese Lösung optimal genutzt werden, um Leistung, bessere Ressourcennutzung und Anwendungsgesundheit zu erreichen? Zunächst ist es notwendig, die Grundkonzepte dieses Themas zu verstehen.

Was ist ein „Thread“?

Threads sind die grundlegenden Ausführungseinheiten eines Prozesses in einem Betriebssystem. Sie ermöglichen einem Programm die gleichzeitige Ausführung mehrerer Vorgänge innerhalb desselben Prozesses. Jeder Thread teilt sich den gleichen Speicherplatz wie der Hauptprozess, kann jedoch unabhängig ausgeführt werden, was für Aufgaben nützlich ist, die parallel ausgeführt werden können, wie z. B. Eingabe-/Ausgabeoperationen (I/O), komplexe Berechnungen oder Datenaktualisierungen .

Auf vielen Systemen werden Threads vom Betriebssystem verwaltet, das jedem Thread CPU-Zeit zuweist und den Kontextwechsel zwischen ihnen verwaltet. In Programmiersprachen wie Java, Python und C gibt es Bibliotheken und Frameworks, die das Erstellen und Verwalten von Threads erleichtern.

Warum Threads verwenden?

Threads werden hauptsächlich verwendet, um die Effizienz und Reaktionsfähigkeit eines Programms zu verbessern. Die Gründe für die Verwendung von Threads, insbesondere mit Schwerpunkt auf dem Backend, sind:

  • Parallelität: Mit Threads können Sie mehrere Vorgänge gleichzeitig ausführen und so die verfügbaren CPU-Ressourcen besser nutzen, insbesondere auf Systemen mit mehreren Kernen.

  • Leistung: Bei E/A-Vorgängen wie dem Lesen und Schreiben von Dateien oder der Netzwerkkommunikation können Threads zur Verbesserung der Leistung beitragen, indem sie dem Programm ermöglichen, weiterhin andere Aufgaben auszuführen, während es auf deren Abschluss wartet Operationen.

  • Modularität: Threads können verwendet werden, um ein Programm in kleinere, besser verwaltbare Teile zu unterteilen, die jeweils eine bestimmte Aufgabe ausführen.

Es ist jedoch wichtig, Threads sorgfältig zu verwalten, da eine falsche Verwendung zu Problemen wie Race Conditions, Deadlocks und Debugging-Schwierigkeiten führen kann. Für eine bessere Verwaltung wird eine Thread-Pool-Lösung verwendet.

Was ist ein Thread-Pool und warum brauche ich ihn?

Ein Thread-Pool ist ein Software-Designmuster, bei dem ein Thread-Pool erstellt und verwaltet wird, der zur Ausführung von Aufgaben wiederverwendet werden kann. Anstatt für jede Aufgabe wiederholt Threads zu erstellen und zu zerstören, hält ein Thread-Pool eine feste Anzahl von Threads bereit, die bei Bedarf Aufgaben ausführen können. Dies kann die Leistung von Anwendungen, die viele gleichzeitige Aufgaben bewältigen müssen, erheblich verbessern. Die positiven Aspekte der Verwendung eines Thread-Pools sind:

  • Verbesserte Leistung: Das Erstellen und Zerstören von Threads ist ein ressourcenintensiver Vorgang. Ein Thread-Pool minimiert diese Kosten durch die Wiederverwendung vorhandener Threads.

  • Ressourcenverwaltung: Steuert die Anzahl der laufenden Threads und vermeidet übermäßige Thread-Erstellung, die das System überlasten kann.

  • Benutzerfreundlichkeit: Vereinfacht die Thread-Verwaltung, sodass sich Entwickler auf die Anwendungslogik statt auf die Thread-Verwaltung konzentrieren können.

  • Skalierbarkeit: Hilft bei der Skalierung von Anwendungen, um eine große Anzahl gleichzeitiger Aufgaben effizient zu bewältigen.

So legen Sie das Thread-Limit im Pool fest

Ok, natürlich muss ich einen Thread-Pool erstellen, um diese Funktion besser nutzen zu können, aber eine Frage, die schnell auftaucht, ist: „Wie viele Threads sollte der Pool enthalten?“. Der Grundlogik folgend gilt: Je mehr, desto besser, oder? Wenn alles parallel erledigt werden kann, wird es bald erledigt sein, da es schneller geht. Daher ist es besser, die Anzahl der Threads nicht zu begrenzen oder eine hohe Anzahl festzulegen, damit dies kein Problem darstellt. Richtig?
Es ist eine faire Aussage, also testen wir sie. Der Code für diesen Test wurde in Kotlin geschrieben, nur um ihn vertrauter zu machen und das Schreiben der Beispiele zu erleichtern. Dieser Punkt ist sprachunabhängig.
Es wurden 4 Beispiele erstellt, die verschiedene Systemnaturen untersuchen. Beispiel 1 und 2 wurden entwickelt, um die CPU zu nutzen, viel zu rechnen, also eine enorme Verarbeitungsleistung zu haben. Beispiel 3 konzentriert sich auf E/A, wobei das Beispiel das Lesen einer Datei ist, und schließlich handelt es sich in Beispiel 4 um eine Situation paralleler API-Aufrufe, die sich ebenfalls auf E/A konzentriert. Sie alle verwendeten Pools unterschiedlicher Größe, jeweils mit 1, 2, 4, 8, 16, 32, 50, 100 und 500 Threads. Alle Prozesse finden mehr als 500 Mal statt.

Beispiel 1 – Code, der berechnet, wie viele Primzahlen es zwischen 1 und 100.000 gibt

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()
    }
}
Nach dem Login kopieren
Beispiel 1 Konsolenausgabe
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
Nach dem Login kopieren

Beispiel 2 – Code, der die 30. Zahl auf der Fibonacci-Skala berechnet

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()
    }
}
Nach dem Login kopieren
Beispiel 2 Konsolenausgabe
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
Nach dem Login kopieren

Beispiel 3 – Code, der eine Datei mit Zufallszahlen liest und diese am Ende des Lesevorgangs hinzufügt

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
}
Nach dem Login kopieren
Beispiel 3 Konsolenausgabe
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
Nach dem Login kopieren

Beispiel 4 – Code, der eine API 500 Mal aufruft

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()
    }
}
Nach dem Login kopieren
Beispiel 4 Konsolenausgabe
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
Nach dem Login kopieren

Die Beispiele 1 bis 3 haben ein gemeinsames Verhalten, sie werden alle bis zu 8 Threads performanter, dann erhöht sich die Verarbeitungszeit wieder, aber nicht Beispiel 4, was zeigt das? Ist es nicht interessant, immer so viele Threads wie möglich zu verwenden?

Die einfache und schnelle Antwort ist Nein.

Der Prozessor meiner Maschine verfügt über 8 Kerne, das heißt, er kann 8 Aufgaben gleichzeitig ausführen. Darüber hinaus erhöht sich die Zeit, da die Zeit für die Verwaltung der Zustände jedes Threads letztendlich die Leistung beeinträchtigt.

Ok, das beantwortet Beispiel 1 bis 3, aber was ist mit Beispiel 4? Warum verbessert sich die Leistung, je mehr Threads gestartet werden?

Einfach, da es sich um eine Integration handelt, hat die Maschine keine Verarbeitung, sie wartet grundsätzlich auf eine Antwort, sie bleibt „schlafend“, bis die Antwort eintrifft, also ja, hier kann die Anzahl der Threads größer sein. Aber seien Sie vorsichtig, das bedeutet nicht, dass es so viele wie möglich geben kann. Threads verursachen eine Erschöpfung der Ressourcen. Ihre wahllose Verwendung hat den umgekehrten Effekt, der sich auf den Gesamtzustand des Dienstes auswirkt.
Um die Anzahl der Threads zu definieren, die Ihr Pool haben soll, ist es daher am einfachsten und sichersten, die Art der auszuführenden Aufgabe zu trennen. Sie sind in zwei Teile geteilt:

  • Aufgaben, die keiner Bearbeitung bedürfen:
    Wenn die Art der Aufgabe keine Verarbeitung erfordert, können mehr Threads erstellt werden, als Prozessorkerne auf der Maschine vorhanden sind. Dies geschieht, weil es nicht notwendig ist, die Informationen zu verarbeiten, um den Thread abzuschließen. Grundsätzlich erwarten Threads dieser Art größtenteils Antworten von Integrationen, z. B. das Schreiben in eine Datenbank oder eine Antwort von einer API.

  • Aufgaben, die bearbeitet werden müssen:
    Wenn die Lösung verarbeitet wird, d. h. die Maschine tatsächlich arbeitet, muss die maximale Anzahl an Threads der Anzahl der Kerne im Prozessor der Maschine entsprechen. Dies liegt daran, dass ein Prozessorkern nicht in der Lage ist, mehr als eine Aufgabe gleichzeitig zu erledigen. Wenn der Prozessor, auf dem die Lösung ausgeführt wird, beispielsweise über 4 Kerne verfügt, muss Ihr Thread-Pool die Größe der Kerne Ihres Prozessors haben, ein 4-Thread-Pool.

Abschluss

Der erste zu definierende Punkt, wenn man über einen Thread-Pool nachdenkt, ist nicht unbedingt die Anzahl, die seine Größe begrenzt, sondern vielmehr die Art der ausgeführten Aufgabe. Threads tragen wesentlich zur Leistung von Diensten bei, müssen jedoch optimal genutzt werden, damit sie nicht den gegenteiligen Effekt haben und die Leistung beeinträchtigen oder, noch schlimmer, dazu führen, dass der Zustand des gesamten Dienstes beeinträchtigt wird. Es ist klar, dass kleinere Pools am Ende Aufgaben mit hoher Verarbeitungsauslastung, also CPU-beschränkte Aufgaben, bevorzugen. Wenn Sie nicht sicher sind, ob die Lösung, in der Threads verwendet werden, ein Verhalten aufweist, bei dem die Verarbeitung massiv beansprucht wird, gehen Sie auf Nummer sicher und begrenzen Sie Ihren Pool auf die Anzahl der Prozessoren auf der Maschine. Glauben Sie mir, das wird sparen Du hast jede Menge Kopfschmerzen.

Das obige ist der detaillierte Inhalt vonThemen: Wie definiert und begrenzt man die Ausführung mit dem Ziel der Leistung?. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Quelle:dev.to
Erklärung dieser Website
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn
Beliebte Tutorials
Mehr>
Neueste Downloads
Mehr>
Web-Effekte
Quellcode der Website
Website-Materialien
Frontend-Vorlage