0Pricing
C Academy · บทเรียน

I/O แบบบล็อกเทียบกับไม่บล็อก

เหตุใดลูปเหตุการณ์จึงสำคัญ

I/O แบบบล็อกเทียบกับไม่บล็อก เป็นบทเรียน C Academy ฟรีบน CoddyKit นี่คือบทเรียนที่ 1 จากทั้งหมด 4 บทเรียน คุณสามารถอ่านบทเรียนทั้งหมดด้านล่างฟรี — จากนั้นลองปฏิบัติด้วยตัวคุณเองในเบราว์เซอร์พร้อมตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7 บทเรียนนี้เป็นส่วนหนึ่งของเส้นทางการเรียน C Academy และความก้าวหน้าของคุณจะซิงค์ข้ามเว็บและแอป CoddyKit คอร์ส C Academy มีบทเรียนทั้งหมด 4 บทเรียน

ความหมายของการบล็อก

การเรียกระบบแบบ บล็อก จะระงับเธรดที่เรียกจนกว่าการดำเนินการจะทำต่อได้ เมื่อคุณเรียก recv() บนซ็อกเก็ตที่ไม่มีข้อมูล เคอร์เนลจะพักเธรดไว้จนกว่าไบต์จะมาถึง

แนวทางนี้เข้าใจได้ง่าย คือหนึ่งการเชื่อมต่อใช้หนึ่งเธรดและโค้ดทำงานเป็นลำดับ ต้นทุนจะปรากฏเมื่อคุณต้องให้บริการไคลเอ็นต์หลายพันรายพร้อมกัน

ssize_t n = recv(fd, buf, sizeof buf, 0);
/* thread sleeps here until data or error */
if (n > 0) handle(buf, n);

ปัญหาด้านการขยายขนาด

เมื่อใช้การรับส่งข้อมูลแบบบล็อก ไคลเอ็นต์ที่ค้างอยู่หนึ่งรายจะบล็อกเธรดทั้งหมด วิธีแก้แบบคลาสสิกคือใช้หนึ่งเธรด (หรือหนึ่งกระบวนการ) ต่อหนึ่งการเชื่อมต่อ

วิธีนี้ใช้ได้ถึงระดับหนึ่ง แต่ 10,000 เธรดหมายถึงสแตก 10,000 ชุด การสลับบริบทจำนวนมาก และค่าใช้จ่ายของตัวจัดตารางเวลา นี่คือปัญหา C10k ที่มีชื่อเสียง ซึ่งผลักดันให้เซิร์ฟเวอร์หันไปใช้การออกแบบที่ขับเคลื่อนด้วยเหตุการณ์

โหมดไม่บล็อก

ซ็อกเก็ตแบบ ไม่บล็อก จะไม่หยุดรอ หากการเรียกไม่สามารถทำงานให้เสร็จได้ทันที การเรียกจะคืนค่า -1 ทันที และตั้งค่า errno เป็น EAGAIN หรือ EWOULDBLOCK

โค้ดของคุณต้องรับผิดชอบการลองใหม่ในภายหลัง วิธีนี้ทำให้เธรดเดียวจัดการซ็อกเก็ตจำนวนมากได้โดยไม่ติดค้างอยู่กับซ็อกเก็ตใดซ็อกเก็ตหนึ่ง

ssize_t n = recv(fd, buf, sizeof buf, 0);
if (n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) {
    /* no data right now, try again later */
}

การตั้งค่า O_NONBLOCK ด้วย fcntl

คุณเปลี่ยนตัวบอกคำอธิบายให้เป็นแบบไม่บล็อกได้โดยเพิ่มแฟล็ก O_NONBLOCK ด้วย fcntl() ควรอ่านแฟล็กปัจจุบันก่อนเสมอ แล้วจึง OR บิตที่ต้องการ เพื่อไม่เขียนทับการตั้งค่าอื่น

ตัวช่วยเดียวกันนี้ใช้ได้กับซ็อกเก็ตที่รอรับการเชื่อมต่อ ซ็อกเก็ตไคลเอ็นต์ที่รับการเชื่อมต่อแล้ว และไปป์เช่นกัน

int set_nonblocking(int fd) {
    int flags = fcntl(fd, F_GETFL, 0);
    if (flags == -1) return -1;
    return fcntl(fd, F_SETFL, flags | O_NONBLOCK);
}

การจัดการการอ่านบางส่วน

การรับส่งข้อมูลแบบไม่บล็อกทำให้การดำเนินการบางส่วนเป็นเรื่องปกติ recv() อาจคืนไบต์น้อยกว่าที่ร้องขอ และ send() อาจรับบัฟเฟอร์ของคุณไปเพียงบางส่วน

คุณต้องติดตามว่าส่งหรือรับไปแล้วเท่าใด และดำเนินการต่อจากจุดนั้น อย่าสมมติว่าการเรียกครั้งเดียวจะย้ายไบต์ทั้งหมด

size_t sent = 0;
while (sent < len) {
    ssize_t w = send(fd, buf + sent, len - sent, 0);
    if (w < 0) { if (errno == EAGAIN) break; else return -1; }
    sent += w;
}

การรอแบบวนตรวจไม่ใช่วิธีที่ถูกต้อง

วิธีใช้ซ็อกเก็ตแบบไม่บล็อกที่ง่ายแต่ไม่เหมาะสมคือวนผ่านซ็อกเก็ตทั้งหมดแล้วลองใหม่ตลอดเวลา การวนรอแบบใช้ทรัพยากรเต็มที่นี้ใช้ CPU 100% แม้ไม่มีอะไรเกิดขึ้น

สิ่งที่เราต้องการจริง ๆ คือถามเคอร์เนลว่า "บอกฉันว่าตัวบอกคำอธิบายใดพร้อมใช้งาน และให้ฉันพักจนกว่าจะถึงเวลานั้น" นั่นคือสิ่งที่การแจ้งเตือนความพร้อมใช้งานจัดเตรียมให้

การแจ้งเตือนความพร้อมใช้งาน

การทำมัลติเพล็กซ์การรับส่งข้อมูลช่วยให้เธรดเดียวรอด้วยตัวบอกคำอธิบายจำนวนมากพร้อมกัน และตื่นขึ้นเฉพาะเมื่อมีอย่างน้อยหนึ่งรายการพร้อมใช้งาน เคอร์เนลจะทำหน้าที่ตรวจสอบแทนคุณ

อินเทอร์เฟซแบบคลาสสิกคือ select() และ poll() ทั้งสองใช้งานได้ แต่จะสแกนตัวบอกคำอธิบายทุกตัวใหม่ทุกครั้งที่เรียกใช้ จึงมีค่าใช้จ่ายสูงเมื่อขยายขนาด

fd_set rfds;
FD_ZERO(&rfds);
FD_SET(fd, &rfds);
select(fd + 1, &rfds, NULL, NULL, NULL);

เหตุใด select และ poll จึงขยายขนาดไม่ได้

ทั้ง select() และ poll() มีความซับซ้อน O(n) โดยแต่ละการเรียกจะส่งชุดตัวบอกคำอธิบายทั้งหมดเข้าเคอร์เนล ซึ่งจะสแกนทั้งหมด จากนั้นคุณก็ต้องสแกนทั้งหมดอีกครั้งเพื่อค้นหารายการที่พร้อมใช้งาน

select() ยังมีขีดจำกัดอยู่ราว FD_SETSIZE (มักเป็น 1024) สำหรับการเชื่อมต่อหลายพันรายการ ค่าใช้จ่ายส่วนนี้จะกลายเป็นต้นทุนหลัก

ก้าวเข้าสู่ epoll

epoll คือคำตอบของ Linux สำหรับการขยายขนาด คุณลงทะเบียนความสนใจในตัวบอกคำอธิบายเพียงครั้งเดียว แล้วเคอร์เนลจะดูแลโครงสร้างข้อมูลภายในเพื่อติดตามความพร้อมใช้งาน

การรอแต่ละครั้งจะคืนเฉพาะตัวบอกคำอธิบายที่พร้อมใช้งานจริง ต้นทุนจึงเพิ่มตามจำนวนการเชื่อมต่อที่กำลังทำงาน ไม่ใช่จำนวนการเชื่อมต่อทั้งหมด ทำให้มีความซับซ้อนโดยประมาณ O(1) ต่อเหตุการณ์ที่พร้อมใช้งาน

int epfd = epoll_create1(0);
/* register fds once, then wait for ready events */

ไม่บล็อกร่วมกับ epoll

epoll และซ็อกเก็ตแบบไม่บล็อกทำงานร่วมกันเป็นทีม epoll บอกว่าตัวบอกคำอธิบายพร้อมใช้งาน ส่วนการเรียกแบบไม่บล็อกช่วยให้คุณอ่านข้อมูลออกจนหมดได้โดยไม่ต้องหยุดรอ

ควรตั้งค่า O_NONBLOCK ให้กับซ็อกเก็ตที่ส่งต่อให้ epoll เสมอ มิฉะนั้นการตื่นขึ้นโดยไม่มีเหตุผลหรือการอ่านบางส่วนอาจบล็อกเธรดลูปเหตุการณ์เพียงเธรดเดียวของคุณ

set_nonblocking(conn_fd);
struct epoll_event ev = { .events = EPOLLIN, .data.fd = conn_fd };
epoll_ctl(epfd, EPOLL_CTL_ADD, conn_fd, &ev);

แบบจำลองในใจ

ลองนึกภาพเซิร์ฟเวอร์เป็นลูป: บล็อกอยู่ใน epoll_wait() รับรายการตัวบอกคำอธิบายจำนวนน้อยที่พร้อมใช้งาน ทำงานแบบไม่บล็อกกับแต่ละรายการ แล้วทำซ้ำ

เธรดจะหลับเมื่อไม่มีงาน และตื่นขึ้นเฉพาะเมื่อมีงานจริง ตอนนี้เธรดเดียวสามารถให้บริการการเชื่อมต่อหลายหมื่นรายการได้อย่างมีประสิทธิภาพ

ตรวจสอบความเข้าใจ

ทดสอบความเข้าใจของคุณเกี่ยวกับซ็อกเก็ตแบบไม่บล็อก

สรุปทบทวน

การรับส่งข้อมูลแบบบล็อกเข้าใจง่าย แต่ทำให้เธรดหนึ่งต้องทุ่มให้กับการเชื่อมต่อหนึ่งรายการ จึงไม่เหมาะเมื่อขยายขนาด การรับส่งข้อมูลแบบไม่บล็อกจะคืนค่าทันทีพร้อม EAGAIN แทนการหยุดรอ

การวนตรวจซ็อกเก็ตในลูปถี่ ๆ ทำให้เสีย CPU เราจึงใช้การแจ้งเตือนความพร้อมใช้งาน select/poll มีความซับซ้อน O(n) ส่วน epoll รองรับการเชื่อมต่อหลายพันรายการได้ ขั้นต่อไปเราจะตั้งค่า epoll

คำถามที่พบบ่อย

บทเรียน “I/O แบบบล็อกเทียบกับไม่บล็อก” ฟรีหรือไม่

ใช่ — ข้อความเต็มของ “I/O แบบบล็อกเทียบกับไม่บล็อก” ฟรีให้อ่านที่นี่บนเว็บ เพื่อปฏิบัติแบบโต้ตอบ (ตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7) และปลดล็อคส่วนที่เหลือของคอร์ส C Academy ให้อัปเกรดเป็น CoddyKit PRO คอร์ส C Academy มีบทเรียนทั้งหมด 4 บทเรียน

คุณจะเรียนรู้อะไรในบทเรียน “I/O แบบบล็อกเทียบกับไม่บล็อก”

เหตุใดลูปเหตุการณ์จึงสำคัญ คุณปฏิบัติ C Academy ด้วยโค้ดที่ใช้งานได้จริงที่คุณเรียกใช้โดยตรงในเบราว์เซอร์ และติวเตอร์ AI ตลอด 24/7 ตอบคำถามของคุณขณะที่คุณไปผ่านบทเรียน

คุณต้องมีประสบการณ์ก่อนที่จะเริ่มเรียน C Academy หรือไม่

ไม่จำเป็นต้องมีประสบการณ์มาก่อน C Academy บน CoddyKit ออกแบบมาสำหรับผู้เริ่มต้นไปจนถึงผู้เรียนขั้นสูง คุณสามารถเริ่มต้นที่นี่หรือเริ่มจากตัวแรกและเรียนด้วยความเร็วของคุณเอง นี่คือบทเรียนที่ 1 จากทั้งหมด 4 บทเรียน

บทเรียน “I/O แบบบล็อกเทียบกับไม่บล็อก” ใช้เวลานานแค่ไหน

บทเรียน CoddyKit ส่วนใหญ่ใช้เวลาประมาณ 5–10 นาที แต่ละบทเรียนจึงสั้นและเป็นแบบโต้ตอบ คุณสามารถก้าวหน้าอย่างต่อเนื่องและกลับมาเรียนต่อจากตรงที่เพิ่งหยุดบนเว็บและแอปได้เลย

ฉันเขียนและรันโค้ดในบทเรียน C Academy นี้ได้ไหม

ได้ บทเรียน C Academy ทุกบทมีตัวแก้ไขโค้ดในตัว คุณจึงเขียนและรันโค้ดจริงได้เลยในเบราว์เซอร์ และได้รับข้อเสนอแนะจาก AI ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ

บทเรียนทั้งหมดในหลักสูตรนี้

  1. I/O แบบบล็อกเทียบกับไม่บล็อก
  2. ตั้งค่า epoll
  3. ลูปเหตุการณ์
  4. เซิร์ฟเวอร์สะท้อนกลับอย่างง่าย
← กลับไปที่ C Academy