Thread និង Concurrency
មាតិកា
- ការធ្វើរឿងច្រើនក្នុងពេលតែមួយ
- តើ Thread ជាអ្វី?
- Thread ធៀបនឹង Process
- Concurrency ធៀបនឹង Parallelism
- ហេតុអ្វីកម្មវិធីគាំង
- គ្រោះថ្នាក់នៃការចែករំលែក៖ Race Condition
- Lock និងបញ្ហាផ្ទាល់ខ្លួនរបស់វា
- របៀបដែលភាសានីមួយៗដោះស្រាយរឿងនេះ
- សាកល្បងដោយខ្លួនឯង
1. ការធ្វើរឿងច្រើនក្នុងពេលតែមួយ
សូមគិតអំពីកម្មវិធីចាក់តន្ត្រីមួយ។ ក្នុងពេលតែមួយ វាត្រូវ៖
- ចាក់បទចម្រៀងបច្ចុប្បន្នដោយមិនដាច់
- ឆ្លើយតបនៅពេលអ្នកចុចប៊ូតុង
- ទាញយកបទបន្ទាប់នៅផ្ទៃខាងក្រោយ
- update របារវឌ្ឍនភាពរៀងរាល់វិនាទី
បើកម្មវិធីអាចធ្វើបានតែរឿងមួយក្នុងពេលតែមួយ តន្ត្រីនឹងឈប់រាល់ពេលដែលអ្នករំកិលបញ្ជីបទចម្រៀង។ កម្មវិធីត្រូវការវិធីមួយដើម្បីធ្វើរឿងច្រើន ក្នុងពេលតែមួយ នៅខាងក្នុង process តែមួយ។ នោះហើយជាអ្វីដែល thread មានសម្រាប់។
2. តើ Thread ជាអ្វី?
Thread គឺជាលំដាប់ instruction តែមួយដែលកំពុងដំណើរការ — «ខ្សែការងារ» មួយនៅខាងក្នុង process មួយ។
Process នីមួយៗចាប់ផ្តើមជាមួយ thread មួយ ដែលហៅថា main thread។ បន្ទាប់មក វាអាចបង្កើត thread បន្ថែមទៀត ដែលនីមួយៗដំណើរការផ្នែកផ្ទាល់ខ្លួនរបស់កម្មវិធី។ Thread នីមួយៗមាន៖
- program counter ផ្ទាល់ខ្លួន — bookmark ផ្ទាល់ខ្លួនក្នុងកូដ
- register ផ្ទាល់ខ្លួន — ស្ថានភាព CPU ផ្ទាល់ខ្លួន
- stack ផ្ទាល់ខ្លួន — កំណត់ត្រាផ្ទាល់ខ្លួនថាវានៅក្នុង function ណា និង local variable របស់ពួកវា
ប៉ុន្តែ thread ទាំងអស់ក្នុង process មួយ ចែករំលែក អ្វីៗផ្សេងទៀតទាំងអស់៖ code ដូចគ្នា heap memory ដូចគ្នា និង file ដែលបើកដូចគ្នា។
រូបភាពដ៏មានប្រយោជន៍មួយ៖ process មួយគឺជា ផ្ទះបាយភោជនីយដ្ឋាន ដែលមានឧបករណ៍ និងគ្រឿងផ្សំរបស់វា។ Thread គឺជា ចុងភៅ។ ចុងភៅម្នាក់ៗធ្វើម្ហូបរៀងៗខ្លួន ប៉ុន្តែពួកគេទាំងអស់ចែករំលែកទូទឹកកក ចង្ក្រាន និងតុដូចគ្នា។
3. Thread ធៀបនឹង Process
| Process | Thread | |
|---|---|---|
| Memory | ផ្ទាល់ខ្លួន ដាច់ដោយឡែក | ចែករំលែកជាមួយ thread ផ្សេងទៀតក្នុង process ដូចគ្នា |
| ការបង្កើត | យឺតជាង ប្រើ memory ច្រើនជាង | លឿនជាង ស្រាលជាង |
| បើវា crash | Process ផ្សេងទៀតមានសុវត្ថិភាព | ជាធម្មតាធ្វើឲ្យ process ទាំងមូលគាំង |
| ការទាក់ទងគ្នា | ត្រូវការ IPC ដោយចេតនា (file, បណ្តាញ, pipe) | អាចអាន និងសរសេរ variable ដែលចែករំលែកបានយ៉ាងងាយ |
Thread មានតម្លៃថោកជាង ហើយអាចចែករំលែកទិន្នន័យបានយ៉ាងងាយ — នោះជាចំណុចខ្លាំងរបស់វា។ Process ដាច់ដោយឡែក និងមានសុវត្ថិភាពជាង — នោះជាចំណុចខ្លាំងរបស់វា។ ការដោះដូរនេះហើយជាមូលហេតុដែល Chrome ប្រើ process ដាច់ដោយឡែកសម្រាប់ tab (សុវត្ថិភាព) ខណៈដែល tab នីមួយៗប្រើ thread ជាច្រើននៅខាងក្នុង (ល្បឿន)។
Scheduler របស់ OS ពីជំពូកទី 7 តាមពិតរៀបចំវេនឲ្យ thread មិនមែន process ទេ។ Thread នីមួយៗយកវេនរបស់វាលើ CPU core មួយ។
4. Concurrency ធៀបនឹង Parallelism
ពាក្យទាំងពីរនេះជាញឹកញាប់ត្រូវបានច្រឡំ៖
- Concurrency មានន័យថា ដោះស្រាយ ការងារច្រើនក្នុងរយៈពេលតែមួយ — ដោយប្តូរទៅមករវាងពួកវា។ នៅខណៈពិតប្រាកដណាមួយ ប្រហែលជាមានតែមួយប៉ុណ្ណោះដែលកំពុងដំណើរការ។
- Parallelism មានន័យថា ធ្វើ ការងារច្រើនក្នុងខណៈតែមួយពិតប្រាកដ លើ CPU core ផ្សេងៗគ្នា។
ចុងភៅម្នាក់ដែលកូរស៊ុប បន្ទាប់មកហាន់ខ្ទឹមបារាំង បន្ទាប់មកពិនិត្យបាយ បន្ទាប់មកកូរស៊ុបម្តងទៀត គឺកំពុងធ្វើការបែប concurrency។ ចុងភៅបីនាក់ដែលម្នាក់ៗធ្វើការងារមួយក្នុងចំណោមការងារទាំងនោះក្នុងពេលតែមួយ គឺកំពុងធ្វើការបែប parallelism។
លើកុំព្យូទ័រ៖
- Core មួយអាចផ្តល់ឲ្យអ្នកនូវ concurrency (ដោយប្តូរ thread យ៉ាងលឿន) ប៉ុន្តែមិនមែន parallelism ទេ។
- Core ច្រើនផ្តល់ឲ្យអ្នកនូវ parallelism ពិតប្រាកដ — នេះហើយជាកន្លែងដែល core បន្ថែមពីជំពូកទី 3 ចុងក្រោយផ្តល់ផលប្រយោជន៍។
ភាគច្រើន concurrency គឺជាអ្វីដែលអ្នកត្រូវការ — ជាពិសេសនៅពេលការងារចំណាយពេលរបស់វាក្នុងការ រង់ចាំ (disk, បណ្តាញ, អ្នកប្រើប្រាស់)។ Parallelism សំខាន់នៅពេលមានការគណនាសុទ្ធច្រើនដែលត្រូវបែងចែក ដូចជាការ encode វីដេអូ ឬការដំណើរការរូបភាពធំមួយ។
5. ហេតុអ្វីកម្មវិធីគាំង
ក្នុងកម្មវិធីស្ទើរតែទាំងអស់ដែលមាន user interface thread មួយ — main thread ដែលជាញឹកញាប់ហៅថា UI thread — ទទួលខុសត្រូវក្នុងការគូរអេក្រង់ និងឆ្លើយតបនឹងការចុច និងការវាយគ្រាប់ចុច។
បើ thread នោះជាប់គាំងធ្វើអ្វីមួយដែលយឺត — អាន file ដ៏ធំ រង់ចាំ network request យឺតៗ ឬគណនាអ្វីមួយធ្ងន់ — វាមិនអាចគូរអេក្រង់ឡើងវិញ ឬឆ្លើយតបនឹងអ្នកបានទេ។ កម្មវិធីមើលទៅដូចជាគាំង។ លើ Windows អ្នកឃើញ «(Not Responding)» នៅរបារចំណងជើង លើ macOS គឺ cursor ពណ៌ឥន្ទធនូដែលវិល។
ដំណោះស្រាយតែងតែជាគំនិតដូចគ្នា៖ កុំដាក់ការងារយឺតលើ main thread។ ប្រគល់វាទៅ background thread (ឬប្រើ asynchronous code ដែលនឹងរៀននៅជំពូកបន្ទាប់) ហើយឲ្យ main thread រក្សា interface ឲ្យរលូន។
រឿងនេះក៏អនុវត្តចំពោះ server ផងដែរ។ Server ដែលដោះស្រាយ request នីមួយៗលើ thread មួយ ហើយ block ខណៈពេលរង់ចាំ database អាចបម្រើអ្នកប្រើប្រាស់បានតែពីរបីនាក់ក្នុងពេលតែមួយ។
6. គ្រោះថ្នាក់នៃការចែករំលែក៖ Race Condition
ការចែករំលែក memory រវាង thread គឺមានអានុភាព — និងគ្រោះថ្នាក់។
ស្រមៃថាសមតុល្យគណនីធនាគារ $100 ត្រូវបានរក្សាទុកក្នុង variable ដែលចែករំលែក។ Thread ពីរព្យាយាមដាក់ប្រាក់ $50 ម្នាក់ៗក្នុងពេលតែមួយ។ ការដាក់ប្រាក់នីមួយៗមានបីជំហាន៖
- អាន សមតុល្យបច្ចុប្បន្ន
- បូក 50
- សរសេរ សមតុល្យថ្មីត្រឡប់ទៅវិញ
បើជំហានទាំងនោះឆ្លាស់គ្នាមិនល្អ៖
| ពេលវេលា | Thread A | Thread B | សមតុល្យក្នុង memory |
|---|---|---|---|
| 1 | អាន 100 | 100 | |
| 2 | អាន 100 | 100 | |
| 3 | បូក 50 → 150 | 100 | |
| 4 | បូក 50 → 150 | 100 | |
| 5 | សរសេរ 150 | 150 | |
| 6 | សរសេរ 150 | 150 |
ការដាក់ប្រាក់ $50 ពីរដង ប៉ុន្តែសមតុល្យកើនឡើងតែ $50 ប៉ុណ្ណោះ។ ការដាក់ប្រាក់មួយបានបាត់ទៅដោយស្ងាត់ៗ។
នេះគឺជា race condition៖ លទ្ធផលអាស្រ័យលើពេលវេលាពិតប្រាកដនៃ thread ដែលអ្នកមិនអាចគ្រប់គ្រងបាន។ Scheduler អាចឆ្លាស់ពួកវាខុសគ្នារាល់ពេលដំណើរការ។ ដូច្នេះ bug នេះលេចឡើង តែម្តងម្កាល — ប្រហែលជាម្តងក្នុងមួយម៉ឺនដង ឬតែនៅពេលមានបន្ទុកធ្ងន់ក្នុង production — ដែលធ្វើឲ្យ race condition ក្លាយជា bug ពិបាករកបំផុតមួយ។
បញ្ហាដូចគ្នានេះក៏កើតឡើងក្រៅពី thread ដែរ — ឧទាហរណ៍ request ពីរទៅកាន់ API របស់អ្នក update ជួរដេកដូចគ្នាក្នុង database ក្នុងពេលតែមួយ។
7. Lock និងបញ្ហាផ្ទាល់ខ្លួនរបស់វា
ដំណោះស្រាយបុរាណគឺ lock (ហៅផងដែរថា mutex មកពី mutual exclusion)។ មុនពេលប៉ះទិន្នន័យដែលចែករំលែក thread មួយយក lock នៅពេលវាធ្វើរួច វាដោះលែងវា។ មានតែ thread មួយប៉ុណ្ណោះដែលអាចកាន់ lock ក្នុងពេលតែមួយ ដូច្នេះជំហាន អាន–បូក–សរសេរ មិនអាចឆ្លាស់គ្នាបានទេ។
វាដូចជាកូនសោបន្ទប់ទឹកតែមួយ៖ អ្នកណាកាន់កូនសោ អ្នកនោះចូល អ្នកផ្សេងទៀតរង់ចាំ។
Lock នាំមកនូវហានិភ័យផ្ទាល់ខ្លួនរបស់វា៖
- ភាពយឺត — thread ចំណាយពេលរង់ចាំគ្នាទៅវិញទៅមក។
- Deadlock — Thread A កាន់ lock 1 ហើយរង់ចាំ lock 2 ខណៈដែល Thread B កាន់ lock 2 ហើយរង់ចាំ lock 1។ ទាំងពីររង់ចាំជារៀងរហូត ហើយកម្មវិធីគាំង។
ដោយសាររឿងនេះងាយនឹងធ្វើខុសណាស់ ច្បាប់ទូទៅដ៏ល្អមួយគឺ៖ ចែករំលែករវាង thread ឲ្យតិចបំផុតតាមដែលអាចធ្វើបាន។
8. របៀបដែលភាសានីមួយៗដោះស្រាយរឿងនេះ
ភាសាផ្សេងៗប្រើវិធីផ្សេងៗគ្នា ហើយអ្នកនឹងជួបពួកវាទាំងអស់៖
- Java, C#, C++ — អ្នកបង្កើត និងគ្រប់គ្រង thread ដោយផ្ទាល់ ហើយប្រើ lock ដើម្បីការពារទិន្នន័យដែលចែករំលែក។
- Go — ធ្វើឲ្យការដំណើរការការងារស្រាលៗរាប់ពាន់ ហៅថា goroutine មានតម្លៃថោកខ្លាំង ហើយលើកទឹកចិត្តឲ្យបញ្ជូនទិន្នន័យរវាងពួកវាតាមរយៈ channel ជំនួសឲ្យការចែករំលែក memory។
- JavaScript (browser និង Node.js) — កូដរបស់អ្នកដំណើរការលើ main thread តែមួយ ដូច្នេះកូដរបស់អ្នកពីរផ្នែកមិនដែលដំណើរការក្នុងខណៈតែមួយពិតប្រាកដទេ ហើយ data race បុរាណមិនអាចកើតឡើងក្នុងកូដធម្មតាបានទេ។ ផ្ទុយទៅវិញ JavaScript ដោះស្រាយការរង់ចាំច្រើនតាមរយៈ asynchronous code និង event loop។ (Node.js ប្រើ thread បន្ថែមនៅខាងក្នុង ហើយអ្នកអាចបង្កើត worker thread សម្រាប់ការគណនាធ្ងន់ៗ។)
- Python — មាន thread ប៉ុន្តែ implementation សំខាន់របស់វាតាមប្រវត្តិសាស្ត្រ អនុញ្ញាតឲ្យមានតែ thread មួយប៉ុណ្ណោះដំណើរការកូដ Python ក្នុងពេលតែមួយ (ហៅថា «GIL») ដូច្នេះការគណនាស្របគ្នាជាធម្មតាប្រើ process ច្រើនជំនួសវិញ។
ហេតុអ្វី JavaScript ដំណើរការបានល្អជាមួយ thread តែមួយ គឺអាស្រ័យលើរបៀបដែលកុំព្យូទ័រដោះស្រាយ input និង output — ប្រធានបទនៃជំពូកបន្ទាប់។
9. សាកល្បងដោយខ្លួនឯង
-
រាប់ thread។
- Windows៖ Task Manager → Performance → CPU។ នៅក្រោមក្រាហ្វ អ្នកនឹងឃើញចំនួន Processes និង Threads សរុប។
- macOS៖ Activity Monitor → tab CPU។ នៅខាងក្រោម អ្នកនឹងឃើញ Threads និង Processes សរុប អ្នកក៏អាចបន្ថែមជួរឈរ Threads ដោយចុចស្តាំលើក្បាលជួរឈរ។
- Linux៖ រត់
top -Hដើម្បីរាយ thread ជំនួសឲ្យ process។
សម្គាល់ថាជាធម្មតាមាន thread ច្រើនជាង process ឆ្ងាយណាស់។
-
មើល parallelism។ លើ Windows ចុចស្តាំលើក្រាហ្វ CPU → Change graph to → Logical processors។ លើ macOS បើក Window → CPU History។ ចាប់ផ្តើមអ្វីមួយធ្ងន់ (ការ export វីដេអូ ហ្គេមធំ ឬការ compile យូរ) ហើយមើល core ជាច្រើនភ្លឺឡើងក្នុងពេលតែមួយ។
-
មើលការគាំង។ លើកក្រោយដែលកម្មវិធីមួយបង្ហាញ «Not Responding» ឬ cursor ដែលវិល អ្នកដឹងហើយថាមានអ្វីកើតឡើង៖ main thread របស់វាកំពុងជាប់គាំង។
Thread អនុញ្ញាតឲ្យកម្មវិធីមួយធ្វើរឿងច្រើនក្នុងពេលតែមួយ ហើយរឿងទាំងនោះភាគច្រើនប្រែជា ការរង់ចាំ។ បន្ទាប់ សូមមើលថាពួកវាកំពុងរង់ចាំអ្វី។
ទិញកាហ្វេជូនខ្ញុំ
ចូលចិត្តមេរៀននេះមែនទេ? សូមពិចារណាទិញកាហ្វេជូនខ្ញុំមួយកែវ ដើម្បីគាំទ្រការបង្កើតមាតិកាឥតគិតថ្លៃបន្ថែមទៀត!