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.
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.
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.
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.
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.
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() } }
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
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() } }
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
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 }
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
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() } }
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
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.
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!