スレッド。最新の高性能ソリューションの開発に役立ち、不可欠なものとなるツールです。言語に関係なく、タスクを並行して実行できることは大きな魅力です。しかし、明らかに、「大いなる力には大いなる責任が伴う」というベンおじさんの有名な言葉があります。パフォーマンス、リソースの有効活用、アプリケーションの健全性を目指して、このソリューションを最適な方法で使用するにはどうすればよいでしょうか?まず、このトピックの基本概念を理解する必要があります。
スレッドは、オペレーティング システムにおけるプロセスの基本的な実行単位です。これらにより、プログラムは同じプロセス内で複数の操作を同時に実行できます。各スレッドはメイン プロセスと同じメモリ空間を共有しますが、独立して実行できるため、入出力 (I/O) 操作、複雑な計算、データ ユーザー インターフェイスなど、並行して実行できるタスクに役立ちます。 .
多くのシステムでは、スレッドはオペレーティング システムによって管理され、オペレーティング システムが各スレッドに CPU 時間を割り当て、スレッド間のコンテキストの切り替えを管理します。 Java、Python、C などのプログラミング言語には、スレッドの作成と管理を容易にするライブラリとフレームワークがあります。
スレッドは主にプログラムの効率と応答性を向上させるために使用されます。スレッドを使用する理由、特にバックエンドに重点を置く理由は次のとおりです:
並列処理: スレッドを使用すると、複数の操作を同時に実行できるため、特に複数のコアを持つシステムで、利用可能な CPU リソースを有効に活用できます。
パフォーマンス: ファイルの読み書きやネットワーク通信などの I/O 操作において、スレッドは、プログラムがこれらのタスクの完了を待機している間に他のタスクの実行を継続できるようにすることで、パフォーマンスの向上に役立ちます。操作。
モジュール性: スレッドを使用すると、プログラムをより小さく管理しやすい部分に分割し、それぞれが特定のタスクを実行できます。
ただし、スレッドを慎重に管理することが重要です。スレッドを誤って使用すると、競合状態、デッドロック、デバッグの困難などの問題が発生する可能性があります。それらをより適切に管理するために、スレッド プール ソリューションが使用されます。
スレッド プールは、タスクを実行するために再利用できるスレッドのプールの作成と管理を含むソフトウェア設計パターンです。スレッド プールは、タスクごとにスレッドの作成と破棄を繰り返すのではなく、必要に応じてタスクを実行できる固定数のスレッドを維持します。これにより、多くのタスクを同時に処理する必要があるアプリケーションのパフォーマンスが大幅に向上します。スレッド プールを使用する利点は次のとおりです:
パフォーマンスの向上: スレッドの作成と破棄は、リソースの点でコストのかかる操作です。スレッド プールは、既存のスレッドを再利用することでこのコストを最小限に抑えます。
リソース管理: 実行中のスレッドの数を制御し、システムに過負荷をかける可能性のある過剰なスレッドの作成を回避します。
使いやすさ: スレッド管理が簡素化され、開発者はスレッド管理ではなくアプリケーション ロジックに集中できるようになります。
スケーラビリティ: アプリケーションを拡張して、多数の同時タスクを効率的に処理するのに役立ちます。
もちろん、この機能を有効に活用するにはスレッド プールを作成する必要がありますが、すぐに浮かぶ疑問は、「プールにはいくつのスレッドを含めるべきですか?」ということです。基本的なロジックに従えば、多ければ多いほど楽しいですよね?すべてを並行して実行できる場合は、処理が速くなるため、すぐに実行されます。したがって、これが問題にならないように、スレッド数を制限しない、または高い数値を設定しないことをお勧めします。そうですか?
これは公平なステートメントなので、テストしてみましょう。このテストのコードは、サンプルを理解しやすくするために Kotlin で書かれています。この点は言語に依存しません。
さまざまなシステムの性質を調査するために 4 つの例が作成されました。例 1 と 2 は、CPU を使用して大量の計算を行う、つまり大量の処理を行うように作られています。例 3 は I/O に焦点を当てており、この例はファイルの読み取りであり、最後の例 4 では、API 呼び出しが並行して行われている状況であり、これも I/O に焦点を当てています。これらはすべて、それぞれ 1、2、4、8、16、32、50、100、および 500 スレッドの異なるサイズのプールを使用しました。すべてのプロセスが 500 回以上発生します。
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
例 1 から 3 には共通の動作があり、8 スレッドまではすべてパフォーマンスが向上し、その後処理時間は再び増加しますが、例 4 はそうではありません。これは何を示していますか?常にできるだけ多くのスレッドを使用するのは興味深いことではないでしょうか?
簡単で簡単な答えは、いいえです。
私のマシンのプロセッサには 8 つのコアがあり、つまり 8 つのタスクを同時に実行できますが、各スレッドの状態を管理する時間が長くなり、最終的にパフォーマンスが低下します。
これで例 1 ~ 3 の答えは決まりましたが、例 4 はどうでしょうか?より多くのスレッドが起動されるとパフォーマンスが向上するのはなぜですか?
これは統合であるため単純で、マシンは処理を行わず、基本的に応答を待ち、応答が到着するまで「スリープ」状態になります。そのため、ここではスレッドの数を増やすことができます。ただし、できるだけ多く存在できるという意味ではないことに注意してください。スレッドはリソースの枯渇を引き起こし、無差別に使用すると逆効果があり、サービス全体の健全性に影響を及ぼします。
したがって、プールに含めるスレッドの数を定義する最も簡単で安全な方法は、実行されるタスクの性質を分離することです。それらは 2 つに分かれています:
処理を必要としないタスク:
タスクの種類が処理を必要としない場合、マシン上のプロセッサ コアよりも多くのスレッドを作成できます。これは、スレッドを完了するために情報を処理する必要がないために発生します。基本的にこの種のスレッドは、ほとんどの場合、DB への書き込みや API からの応答など、統合からの応答を期待します。
処理が必要なタスク:
ソリューションに処理がある場合、つまりマシンが実際に作業を行っている場合、スレッドの最大数はマシンのプロセッサのコア数である必要があります。これは、プロセッサ コアが同時に複数のことを実行できないためです。たとえば、ソリューションを実行するプロセッサに 4 つのコアがある場合、スレッド プールはプロセッサのコアのサイズ、つまり 4 スレッド プールである必要があります。
スレッド プールについて考えるときに定義する最初の点は、必ずしもそのサイズを制限する数ではなく、実行されるタスクの性質です。スレッドはサービスのパフォーマンスに大きく役立ちますが、逆効果でパフォーマンスが低下したり、さらに悪いことにサービス全体の健全性に影響を与えたりしないように、スレッドは最適な方法で使用する必要があります。より小さいプールは、処理使用量が多いタスク、つまり CPU に制限されたタスクを優先することになるのは明らかです。スレッドが使用されるソリューションに、処理が大量に使用される動作があるかどうかがわからない場合は、注意が必要ですが、マシン上のプロセッサの数までプールを制限してください。信じてください。節約できるでしょう。あなたはとても頭が痛いです。
以上がスレッド: パフォーマンスを目的とした実行をどのように定義して制限するか?の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。