Input/Output៖ ការទាក់ទងជាមួយពិភពខាងក្រៅ

មាតិកា

  1. តើ I/O ជាអ្វី?
  2. I/O យឺត
  3. របៀបដែលឧបករណ៍ទាក់ទាញការយកចិត្តទុកដាក់របស់ CPU
  4. Blocking I/O
  5. Non-Blocking I/O និង Async Code
  6. Event Loop ដោយសង្ខេប
  7. Standard Input និង Output
  8. បណ្តាញគ្រាន់តែជា I/O មួយប្រភេទទៀត
  9. សាកល្បងដោយខ្លួនឯង

1. តើ I/O ជាអ្វី?

I/O មកពីពាក្យ Input/Output៖ គ្រប់ពេលដែលទិន្នន័យផ្លាស់ទីរវាង CPU និង RAM នៅម្ខាង និង អ្វីៗផ្សេងទៀត នៅម្ខាងទៀត។

  • Input៖ ក្តារចុច កណ្តុរ អេក្រង់ប៉ះ មីក្រូហ្វូន កាមេរ៉ា ការអាន file ការទទួលទិន្នន័យពីបណ្តាញ
  • Output៖ ការគូរលើអេក្រង់ ការចាក់សំឡេង ការសរសេរ file ការផ្ញើទិន្នន័យតាមបណ្តាញ

កម្មវិធីដែលមិនធ្វើ I/O សោះ នឹងគ្មានប្រយោជន៍ទេ — វាអាចគណនាបាន ប៉ុន្តែវាមិនអាចទទួលសំណួរ ឬបង្ហាញចម្លើយដល់អ្នកបានទេ។ គ្រប់កម្មវិធីដែលមានប្រយោជន៍ គឺជា loop នៃ៖ ទទួល input → ដំណើរការ → បង្កើត output ដូចគំរូពីជំពូកទី 1 ដែរ។

2. I/O យឺត

ត្រឡប់ទៅតារាងពីជំពូកទី 4 វិញ។ បើការអាន register ចំណាយពេល 1 វិនាទី ការអានពី SSD ចំណាយពេល ច្រើនថ្ងៃ ហើយ network request ឆ្លងពិភពលោកចំណាយពេល ច្រើនឆ្នាំ។

ឥឡូវបន្ថែមឧបករណ៍ដែលយឺតបំផុតក្នុងចំណោមទាំងអស់៖ មនុស្ស។ អ្នកប្រហែលជាចំណាយពេលមួយវិនាទីដើម្បីចុចប៊ូតុងមួយ។ សម្រាប់ CPU ដែលដំណើរការ instruction រាប់ពាន់លានក្នុងមួយវិនាទី នោះគឺជាភាពអស់កល្បជានិច្ច។

ដូច្នេះ ពេលវេលាភាគច្រើនរបស់កុំព្យូទ័រ មិនមែនចំណាយលើការគណនាទេ។ វាចំណាយលើការ រង់ចាំ — disk បណ្តាញ អ្នកប្រើប្រាស់។ នោះហើយជាមូលហេតុដែលអ្នកបានឃើញ process ភាគច្រើននៅ 0% CPU ក្នុងជំពូកទី 7។

រឿងនេះនាំទៅដល់សំណួរកណ្តាលនៃជំពូកនេះ៖ តើកម្មវិធីគួរធ្វើអ្វីខណៈពេលវារង់ចាំ?

3. របៀបដែលឧបករណ៍ទាក់ទាញការយកចិត្តទុកដាក់របស់ CPU

វិធីមួយដើម្បីរង់ចាំ គឺឲ្យ CPU ពិនិត្យជាបន្តបន្ទាប់៖ «មានគេចុចគ្រាប់ចុចទេ? ទេ។ មានគេចុចគ្រាប់ចុចទេ? ទេ…» — រាប់លានដងក្នុងមួយវិនាទី។ នេះត្រូវបានហៅថា polling ហើយវាខ្ជះខ្ជាយពេលវេលារបស់ CPU ស្ទើរតែទាំងអស់។

ផ្ទុយទៅវិញ ឧបករណ៍ប្រើ interrupt។ នៅពេលមានអ្វីមួយកើតឡើង — គ្រាប់ចុចត្រូវបានចុច packet បណ្តាញមកដល់ disk អានរួច — ឧបករណ៍ផ្ញើសញ្ញាអគ្គិសនីទៅ CPU។ CPU ផ្អាកអ្វីដែលវាកំពុងធ្វើមួយភ្លែត ដំណើរការកូដ kernel តូចមួយ (ជាធម្មតាពី driver របស់ឧបករណ៍) ដើម្បីដោះស្រាយព្រឹត្តិការណ៍នោះ ហើយបន្ទាប់មកត្រឡប់ទៅការងាររបស់វាវិញ។

វាជាភាពខុសគ្នារវាងការសម្លឹងមើលទូរស័ព្ទរបស់អ្នករង់ចាំសារ និងការទុកឲ្យវាញ័រនៅពេលមានសារមកដល់។

Interrupt ក៏ជារបៀបដែល scheduler (ជំពូកទី 7) ដំណើរការផងដែរ៖ hardware timer មួយ interrupt CPU រៀងរាល់ពីរបី millisecond ដោយផ្តល់ឱកាសឲ្យ kernel ប្តូរទៅ thread មួយផ្សេងទៀត។

4. Blocking I/O

វិធីសាមញ្ញបំផុតក្នុងការសរសេរកូដ I/O គឺ blocking៖ នៅពេលកម្មវិធីរបស់អ្នកសុំអាន file មួយ វាគ្រាន់តែឈប់ ហើយរង់ចាំរហូតដល់ទិន្នន័យរួចរាល់។

// Blocking: nothing else happens until the whole file is read
const data = fs.readFileSync("big-video.mp4");
console.log("Done!");

វិធីនេះងាយយល់ — កូដដំណើរការពីលើចុះក្រោមតាមលំដាប់។ ខណៈពេលដែលកម្មវិធីរង់ចាំ OS ដាក់ thread របស់វាក្នុងស្ថានភាព waiting ហើយផ្តល់ CPU ទៅអ្នកផ្សេង ដូច្នេះកុំព្យូទ័រទាំងមូលមិនខ្ជះខ្ជាយទេ។

បញ្ហាគឺពេលវេលារបស់ កម្មវិធីអ្នក។ Thread នោះមិនអាចធ្វើអ្វីផ្សេងបានទេខណៈពេលវារង់ចាំ។ បើវាជា UI thread កម្មវិធីគាំង (ជំពូកទី 8)។ បើវាជា web server ដែលដោះស្រាយ request មួយក្នុងមួយ thread នោះ request នីមួយៗដែលកំពុងរង់ចាំ ចាប់យក thread មួយទាំងមូល។ Server ដែលមាន 100 thread ដែលនីមួយៗរង់ចាំ database កន្លះវិនាទី អាចដោះស្រាយបានតែប្រហែល 200 request ក្នុងមួយវិនាទីប៉ុណ្ណោះ — ទោះបី CPU របស់វាភាគច្រើននៅទំនេរក៏ដោយ។

5. Non-Blocking I/O និង Async Code

ជម្រើសមួយទៀតគឺ non-blocking (ឬ asynchronous) I/O។ ជំនួសឲ្យការរង់ចាំ កម្មវិធីរបស់អ្នកនិយាយទៅកាន់ OS ថា៖ «ចាប់ផ្តើមអាន file នេះ ហើយប្រាប់ខ្ញុំនៅពេលវារួចរាល់»។ បន្ទាប់មក វាបន្តការងារផ្សេងទៀតភ្លាមៗ។

// Non-blocking: start the read, then keep going
fs.readFile("big-video.mp4", (err, data) => {
  console.log("Done!"); // runs later, when the data is ready
});
console.log("Started reading…"); // runs immediately

នៅទីនេះ "Started reading…" ត្រូវបានបង្ហាញ មុនគេ ទោះបីវាត្រូវបានសរសេរទីពីរក៏ដោយ។ Callback function ដំណើរការនៅពេលក្រោយ នៅពេល OS រាយការណ៍ថាការអានបានបញ្ចប់។

នៅក្នុង JavaScript សម័យថ្មី ជាធម្មតាអ្នកនឹងសរសេរគំនិតដូចគ្នានេះដោយប្រើ async/await ដែល មើលទៅ ដូចជា blocking code ប៉ុន្តែមិន block ទេ៖

const data = await fs.promises.readFile("big-video.mp4");
console.log("Done!");

await មានន័យថា «ផ្អាក function នេះ រហូតដល់ទិន្នន័យរួចរាល់ — ប៉ុន្តែឲ្យអ្វីៗផ្សេងទៀតបន្តដំណើរការក្នុងពេលនេះ»។

អត្ថប្រយោជន៍គឺធំធេងណាស់៖ thread តែមួយអាចមានប្រតិបត្តិការ I/O រាប់ពាន់ កំពុងដំណើរការក្នុងពេលតែមួយ ព្រោះវាមិនដែលអង្គុយស្ងៀមរង់ចាំប្រតិបត្តិការណាមួយទេ។

6. Event Loop ដោយសង្ខេប

តើ thread តែមួយតាមដានប្រតិបត្តិការដែលមិនទាន់បញ្ចប់ទាំងអស់នោះដោយរបៀបណា? JavaScript (ក្នុង browser និងក្នុង Node.js) ប្រើ event loop។ ក្នុងទម្រង់សាមញ្ញ វាជា loop ដែលដំណើរការជារៀងរហូត៖

  1. ដំណើរការកូដដែលរួចរាល់សម្រាប់ដំណើរការ។
  2. នៅពេលកូដចាប់ផ្តើមប្រតិបត្តិការ I/O មួយ ប្រគល់វាទៅ OS ហើយបន្តទៅមុខ។
  3. នៅពេល OS រាយការណ៍ថាប្រតិបត្តិការមួយបានបញ្ចប់ បន្ថែម callback របស់វាទៅក្នុងជួរ (queue)។
  4. នៅពេលកូដបច្ចុប្បន្នធ្វើរួច យក callback បន្ទាប់ពីជួរ ហើយដំណើរការវា។
  5. ធ្វើម្តងទៀត។

នោះហើយជារបៀបដែល Node.js server ដែលមាន thread តែមួយ អាចបម្រើអ្នកប្រើប្រាស់ជាច្រើនក្នុងពេលតែមួយ៖ ខណៈពេលវារង់ចាំ database query របស់អ្នកប្រើប្រាស់ម្នាក់ វាដោះស្រាយ request របស់អ្នកប្រើប្រាស់ម្នាក់ទៀត។ វាក៏ជាមូលហេតុដែល ក្នុង JavaScript ការគណនាយឺតមួយ block អ្វីៗទាំងអស់ — loop មិនអាចបន្តទៅមុខបានទេ រហូតដល់កូដបច្ចុប្បន្នបញ្ចប់។

អ្នកនឹងរៀន event loop យ៉ាងស៊ីជម្រៅនៅក្នុង មូលដ្ឋានគ្រឹះ JavaScript និង TypeScript។ សម្រាប់ពេលនេះ គំនិតធំគឺគ្រប់គ្រាន់ហើយ៖ async code មាន ព្រោះ I/O យឺត ហើយការរង់ចាំគឺខ្ជះខ្ជាយ។

7. Standard Input និង Output

Process នីមួយៗចាប់ផ្តើមជាមួយឆានែល I/O ដែលរៀបចំរួចស្រាប់ចំនួនបី ហៅថា standard stream៖

Streamលេខលំនាំដើម
stdin (standard input)0ក្តារចុច
stdout (standard output)1អេក្រង់ terminal
stderr (standard error)2អេក្រង់ terminal សម្រាប់សារ error

នៅពេលកម្មវិធីរបស់អ្នកហៅ console.log() (JavaScript) ឬ print() (Python) វាកំពុងសរសេរទៅ stdout។ សារ error ទៅ stderr ដូច្នេះពួកវាអាចត្រូវបានរក្សាទុកដាច់ដោយឡែកពី output ធម្មតា។

ផ្នែកដែលមានអានុភាពគឺ stream ទាំងនេះអាចត្រូវបាន redirect។ ជំនួសឲ្យអេក្រង់ stdout អាចចូលទៅក្នុង file ឬចូលទៅក្នុងកម្មវិធីមួយទៀត។ នេះគឺជាមូលដ្ឋាននៃ command line ភាគច្រើន — ហើយវាជារបៀបដែល Docker ប្រមូល log របស់ container អ្នក៖ docker logs គ្រាន់តែបង្ហាញអ្វីដែល process បានសរសេរទៅ stdout និង stderr។ អ្នកនឹងប្រើរឿងនេះជានិច្ចនៅក្នុង មូលដ្ឋានគ្រឹះ Command Line។

8. បណ្តាញគ្រាន់តែជា I/O មួយប្រភេទទៀត

សម្រាប់កម្មវិធីមួយ ការនិយាយតាមបណ្តាញមើលទៅស្រដៀងនឹងការអាន និងសរសេរ file ច្រើនណាស់៖ បើកការតភ្ជាប់ សរសេរ byte មួយចំនួន អាន byte មួយចំនួន បិទវា។ ប្រព័ន្ធប្រតិបត្តិការផ្តល់ socket — ចំណុចបញ្ចប់សម្រាប់ការតភ្ជាប់បណ្តាញ — ហើយ network card និង driver របស់វាដោះស្រាយការផ្ញើពិតប្រាកដ។

ភាពខុសគ្នាគឺល្បឿន និងភាពអាចទុកចិត្តបាន។ បណ្តាញគឺជាកម្រិតយឺតបំផុតនៃឋានានុក្រម memory ហើយមិនដូច disk ក្នុងម៉ាស៊ីនទេ ម្ខាងទៀតអាចយឺត រវល់ ឬមិនមានទាល់តែសោះ។ នោះហើយជាមូលហេតុដែលកូដបណ្តាញត្រូវការ timeout និង ការដោះស្រាយ error ហើយហេតុអ្វីការធ្វើ network request ឲ្យតិច គឺជាវិធីមានប្រសិទ្ធភាពបំផុតមួយក្នុងការធ្វើឲ្យកម្មវិធីលឿនជាងមុន។

របៀបដែល request ពិតជាធ្វើដំណើរឆ្លងកាត់អ៊ីនធឺណិត — IP address, DNS, port និង HTTP — នឹងត្រូវបានរៀននៅក្នុង របៀបដែលគេហទំព័រដំណើរការ និង បណ្តាញសម្រាប់អ្នកអភិវឌ្ឍន៍។

9. សាកល្បងដោយខ្លួនឯង

  1. មើល I/O កើតឡើង។

    • Windows៖ Task Manager → Performance → Disk និង Ethernet/Wi-Fi។
    • macOS៖ Activity Monitor → tab Disk និង Network។
    • Linux៖ រត់ iotop (ប្រហែលជាត្រូវដំឡើង) សម្រាប់ disk ឬពិនិត្យក្រាហ្វបណ្តាញរបស់ system monitor អ្នក។

    ឥឡូវនេះ ចម្លង file ធំមួយ ឬទាញយកអ្វីមួយធំ។ មើលសកម្មភាព disk ឬបណ្តាញលោតឡើង ខណៈដែល ការប្រើប្រាស់ CPU នៅទាប — កុំព្យូទ័រភាគច្រើនកំពុងរង់ចាំ។

  2. មានអារម្មណ៍ពីភាពខុសគ្នានៃល្បឿន។ បើកកម្មវិធីមួយដែលអ្នកប្រើញឹកញាប់ បិទវា បន្ទាប់មកបើកវាម្តងទៀត។ ការបើកលើកទីពីរជាធម្មតាលឿនជាង — ផ្នែកខ្លះរបស់វានៅតែ cache ក្នុង RAM ដូច្នេះត្រូវអានពី storage តិចជាង។

  3. សម្គាល់ async ក្នុងជីវិតពិត។ សម្គាល់ពីរបៀបដែលគេហទំព័រមួយបង្ហាញប្លង់របស់វាមុន ហើយបន្ទាប់មករូបភាពលេចឡើងម្តងមួយៗ។ ទំព័រនោះមិនបាន block ខណៈពេលរង់ចាំរូបភាពនីមួយៗទេ — វាបន្តទៅមុខ ហើយបំពេញពួកវានៅពេលពួកវាមកដល់។

ឥឡូវអ្នកមានគ្រប់បំណែកហើយ៖ byte, CPU, memory, file, OS, process, thread និង I/O។ នៅជំពូកចុងក្រោយ យើងនឹងដាក់ពួកវាទាំងអស់បញ្ចូលគ្នា។

ទិញកាហ្វេជូនខ្ញុំ

ចូលចិត្តមេរៀននេះមែនទេ? សូមពិចារណាទិញកាហ្វេជូនខ្ញុំមួយកែវ ដើម្បីគាំទ្រការបង្កើតមាតិកាឥតគិតថ្លៃបន្ថែមទៀត!