Benang, alat yang membantu dan menjadi sangat diperlukan dalam pembangunan penyelesaian moden yang berprestasi tinggi. Tanpa mengira bahasa, keupayaan untuk melaksanakan tugas secara selari adalah sesuatu yang mempunyai daya tarikan yang hebat. Tetapi jelas terdapat petikan terkenal Uncle Ben: "Dengan kuasa besar datang tanggungjawab besar." Bagaimanakah penyelesaian ini boleh digunakan dengan cara terbaik, menyasarkan prestasi, penggunaan sumber yang lebih baik dan kesihatan aplikasi? Pertama, adalah perlu untuk memahami konsep asas topik ini.
Benang ialah unit asas pelaksanaan proses dalam sistem pengendalian. Mereka membenarkan program untuk melaksanakan berbilang operasi secara serentak dalam proses yang sama. Setiap utas berkongsi ruang memori yang sama seperti proses utama, tetapi boleh dilaksanakan secara bebas, yang berguna untuk tugasan yang boleh dilakukan secara selari, seperti operasi input/output (I/O), pengiraan kompleks atau antara muka pengguna data .
Pada banyak sistem, utas diurus oleh sistem pengendalian, yang memperuntukkan masa CPU untuk setiap utas dan mengurus penukaran konteks antara mereka. Dalam bahasa pengaturcaraan seperti Java, Python, dan C, terdapat perpustakaan dan rangka kerja yang memudahkan untuk membuat dan mengurus benang.
Benang digunakan terutamanya untuk meningkatkan kecekapan dan responsif program. Sebab untuk menggunakan benang, terutamanya memfokus pada bahagian belakang ialah:
Paralelisme: Benang membolehkan anda melakukan berbilang operasi serentak, menggunakan lebih baik sumber CPU yang tersedia, terutamanya pada sistem dengan berbilang teras.
Prestasi: Dalam operasi I/O, seperti membaca dan menulis fail atau komunikasi rangkaian, benang boleh membantu meningkatkan prestasi dengan membenarkan atur cara untuk terus melaksanakan tugasan lain sementara menunggu penyelesaian ini operasi.
Modulariti: Benang boleh digunakan untuk membahagikan atur cara kepada bahagian yang lebih kecil dan lebih mudah diurus, setiap satu menjalankan tugas tertentu.
Walau bagaimanapun, adalah penting untuk mengurus benang dengan berhati-hati, kerana penggunaan yang salah boleh membawa kepada masalah seperti keadaan perlumbaan, kebuntuan dan kesukaran menyahpepijat. Untuk pengurusan mereka yang lebih baik, penyelesaian kumpulan benang digunakan.
Kolam benang ialah corak reka bentuk perisian yang melibatkan mencipta dan mengurus kumpulan benang yang boleh digunakan semula untuk melaksanakan tugas. Daripada membuat dan memusnahkan benang berulang kali untuk setiap tugas, kumpulan benang mengekalkan bilangan benang tetap yang sedia untuk melaksanakan tugasan mengikut keperluan. Ini boleh meningkatkan prestasi aplikasi yang perlu mengendalikan banyak tugas serentak dengan ketara. Perkara positif menggunakan kumpulan benang ialah:
Peningkatan Prestasi: Mencipta dan memusnahkan benang ialah operasi yang mahal dari segi sumber. Kumpulan benang meminimumkan kos ini dengan menggunakan semula benang sedia ada.
Pengurusan Sumber: Mengawal bilangan utas berjalan, mengelakkan penciptaan benang berlebihan yang boleh membebankan sistem.
Kemudahan Penggunaan: Memudahkan pengurusan benang, membenarkan pembangun menumpukan pada logik aplikasi dan bukannya pengurusan benang.
Skalabiliti: Membantu menskalakan aplikasi untuk mengendalikan sejumlah besar tugas serentak dengan cekap.
Ok, sudah tentu saya perlu mencipta kumpulan benang untuk menggunakan ciri ini dengan lebih baik, tetapi soalan yang timbul dengan cepat ialah: "Berapa banyak utas yang harus ada dalam kumpulan itu?". Mengikut logik asas, lebih banyak lagi meriah, bukan? Jika semuanya boleh dilakukan secara selari, ia akan dilakukan tidak lama lagi, kerana ia akan menjadi lebih cepat. Oleh itu, adalah lebih baik untuk tidak mengehadkan bilangan benang, atau menetapkan nombor yang tinggi, supaya ini tidak menjadi kebimbangan. Betul?
Ia adalah satu kenyataan yang adil, jadi mari kita mengujinya. Kod untuk ujian ini ditulis dalam Kotlin hanya untuk kebiasaan dan kemudahan menulis contoh. Perkara ini adalah agnostik bahasa.
4 contoh telah dibuat untuk meneroka sifat sistem yang berbeza. Contoh 1 dan 2 dibuat untuk menggunakan CPU, melakukan banyak matematik, iaitu, mempunyai pemprosesan besar-besaran. Contoh 3 tertumpu pada I/O, contohnya ialah pembacaan fail dan akhirnya, dalam contoh 4 ia adalah situasi panggilan API secara selari, juga memfokuskan pada I/O. Mereka semua menggunakan kolam dengan saiz yang berbeza, masing-masing dengan 1, 2, 4, 8, 16, 32, 50, 100 dan 500 benang. Semua proses berlaku lebih daripada 500 kali.
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
Contoh 1 hingga 3 mempunyai tingkah laku yang sama, semuanya menjadi lebih berprestasi sehingga 8 utas, kemudian masa pemprosesan meningkat semula, tetapi bukan contoh 4, apakah yang ditunjukkan oleh ini? Bukankah menarik untuk sentiasa menggunakan benang sebanyak mungkin?
Jawapan ringkas dan cepat ialah tidak.
Pemproses mesin saya mempunyai 8 teras, iaitu, ia boleh melakukan 8 tugasan pada masa yang sama, lebih daripada itu masa meningkat apabila masa untuk menguruskan keadaan setiap utas akhirnya merendahkan prestasi.
Ok, ini menjawab contoh 1 hingga 3, tetapi bagaimana pula dengan contoh 4? Mengapakah prestasi bertambah baik apabila lebih banyak urutan dilancarkan?
Mudah, kerana ia adalah integrasi, mesin tidak mempunyai pemprosesan, ia pada dasarnya menunggu tindak balas, ia kekal "tidur" sehingga respons tiba, jadi ya, di sini bilangan benang boleh menjadi lebih besar. Tetapi berhati-hati, ini tidak bermakna terdapat sebanyak mungkin, benang menyebabkan keletihan sumber, menggunakannya secara sembarangan mempunyai kesan terbalik yang akan menjejaskan kesihatan keseluruhan perkhidmatan.
Oleh itu, untuk menentukan bilangan utas yang kolam anda akan ada, cara yang paling mudah dan selamat ialah memisahkan sifat tugas yang akan dilakukan. Mereka dipisahkan kepada dua:
Tugas yang tidak memerlukan pemprosesan:
Apabila jenis tugasan tidak memerlukan pemprosesan, lebih banyak benang boleh dibuat daripada teras pemproses pada mesin. Ini berlaku kerana tidak perlu memproses maklumat untuk melengkapkan utas, pada asasnya urutan seperti ini, sebahagian besarnya, mengharapkan respons daripada penyepaduan, seperti menulis kepada DB atau respons daripada API.
Tugas yang memerlukan pemprosesan:
Apabila penyelesaian mempunyai pemprosesan, iaitu, mesin sebenarnya melakukan kerja, bilangan maksimum benang mestilah bilangan teras dalam pemproses mesin. Ini kerana teras pemproses tidak dapat melakukan lebih daripada satu perkara pada masa yang sama. Contohnya, jika pemproses di mana penyelesaian dijalankan mempunyai 4 teras, maka kumpulan benang anda mestilah saiz teras pemproses anda, kumpulan 4-benang.
Perkara pertama yang perlu ditakrifkan apabila memikirkan tentang kumpulan benang tidak semestinya nombor yang akan mengehadkan saiznya, sebaliknya sifat tugas yang dilakukan. Benang sangat membantu dengan prestasi perkhidmatan, tetapi ia mesti digunakan dengan cara yang terbaik supaya ia tidak memberi kesan sebaliknya dan merendahkan prestasi, atau lebih teruk lagi, menyebabkan kesihatan keseluruhan perkhidmatan terjejas. Adalah jelas bahawa kumpulan yang lebih kecil akhirnya memihak kepada tugas dengan banyak penggunaan pemprosesan, tugas terhad CPU dengan kata lain. Jika anda tidak pasti sama ada penyelesaian di mana benang akan digunakan mempunyai tingkah laku di mana pemprosesan akan digunakan secara besar-besaran, silap di sisi berhati-hati, hadkan kumpulan anda kepada bilangan pemproses pada mesin, percayalah, ia akan menjimatkan awak banyak pening kepala.
Atas ialah kandungan terperinci Threads: Bagaimana untuk menentukan dan mengehadkan pelaksanaan yang bertujuan untuk prestasi?. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!