Threads는 최신 고성능 솔루션 개발에 도움을 주고 필수적인 도구입니다. 언어에 관계없이 작업을 병렬로 수행하는 능력은 매우 매력적입니다. 하지만 분명히 벤 삼촌의 유명한 인용문이 있습니다: "큰 힘에는 큰 책임이 따른다." 성능, 리소스 활용도 향상, 애플리케이션 상태 향상을 목표로 이 솔루션을 최선의 방법으로 어떻게 사용할 수 있습니까? 먼저, 이 주제의 기본 개념을 이해하는 것이 필요합니다.
스레드는 운영 체제에서 프로세스를 실행하는 기본 단위입니다. 이를 통해 프로그램은 동일한 프로세스 내에서 동시에 여러 작업을 수행할 수 있습니다. 각 스레드는 기본 프로세스와 동일한 메모리 공간을 공유하지만 독립적으로 실행할 수 있습니다. 이는 입출력(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는 어떻습니까? 더 많은 스레드가 시작될수록 성능이 향상되는 이유는 무엇입니까?
간단합니다. 통합이므로 기계에는 처리가 없으며 기본적으로 응답을 기다리고 응답이 도착할 때까지 "휴면" 상태를 유지하므로 예, 여기서 스레드 수가 더 클 수 있습니다. 하지만 주의하세요. 스레드가 가능한 한 많을 수 있다는 의미는 아닙니다. 스레드는 리소스 고갈을 일으키고, 스레드를 무분별하게 사용하면 서비스의 전반적인 상태에 영향을 미치는 역효과가 있습니다.
따라서 풀에 포함될 스레드 수를 정의하는 가장 쉽고 안전한 방법은 수행할 작업의 성격을 분리하는 것입니다. 두 가지로 구분됩니다:
처리가 필요하지 않은 작업:
작업 유형에 처리가 필요하지 않은 경우 시스템에 있는 프로세서 코어보다 더 많은 스레드가 생성될 수 있습니다. 이는 스레드를 완료하기 위해 정보를 처리할 필요가 없기 때문에 발생합니다. 기본적으로 이러한 성격의 스레드는 대부분 DB에 쓰기 또는 API의 응답과 같은 통합의 응답을 기대합니다.
처리가 필요한 작업:
솔루션에 처리가 있는 경우, 즉 머신이 실제로 작업을 수행하는 경우 최대 스레드 수는 머신 프로세서의 코어 수와 같아야 합니다. 이는 프로세서 코어가 동시에 두 가지 이상의 작업을 수행할 수 없기 때문입니다. 예를 들어, 솔루션이 실행되는 프로세서에 4개의 코어가 있는 경우 스레드 풀은 프로세서 코어의 크기인 4스레드 풀이어야 합니다.
스레드 풀을 생각할 때 가장 먼저 정의해야 할 점은 스레드 풀의 크기를 제한하는 숫자가 아니라 수행되는 작업의 성격입니다. 스레드는 서비스 성능에 많은 도움이 되지만 반대 효과를 가져오거나 성능을 저하시키거나 더 나쁜 경우 전체 서비스 상태에 영향을 미치지 않도록 최선의 방법으로 사용해야 합니다. 작은 풀은 처리 사용량이 많은 작업, 즉 CPU 제한 작업을 선호하게 된다는 것은 분명합니다. 스레드가 사용될 솔루션이 프로세싱을 대량으로 사용하는 동작을 가지고 있는지 확실하지 않다면 주의를 기울이십시오. 풀을 머신의 프로세서 수로 제한하십시오. 믿으십시오. 너 머리가 많이 아프구나.
위 내용은 스레드: 성능을 목표로 실행을 정의하고 제한하는 방법은 무엇입니까?의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!