มูลนิธิ
เริ่มต้นด้วยตัวอย่างที่เรียบง่ายและตรวจสอบชิ้นส่วน Boost ที่เกี่ยวข้อง Asio:
void handle_async_receive(...) { ... }
void print() { ... }
...
boost::asio::io_service io_service;
boost::asio::ip::tcp::socket socket(io_service);
...
io_service.post(&print);
socket.connect(endpoint);
socket.async_receive(buffer, &handle_async_receive);
io_service.post(&print);
io_service.run();
คืออะไรHandler ?
จัดการเป็นอะไรมากไปกว่าการติดต่อกลับ ในโค้ดตัวอย่างมีตัวจัดการ 3 ตัว:
print
จัดการ (1)
handle_async_receive
จัดการ (3)
print
จัดการ (4)
แม้ว่าจะใช้print()
ฟังก์ชันเดียวกันสองครั้ง แต่การใช้งานแต่ละครั้งถือเป็นการสร้างตัวจัดการที่ระบุตัวตนได้ Handlers สามารถมีหลายรูปทรงและขนาดตั้งแต่ฟังก์ชันพื้นฐานเช่นเดียวกับด้านบนไปจนถึงโครงสร้างที่ซับซ้อนมากขึ้นเช่น functors ที่สร้างจากboost::bind()
และ lambdas โดยไม่คำนึงถึงความซับซ้อนตัวจัดการยังคงไม่มีอะไรมากไปกว่าการโทรกลับ
งานคืออะไร?
งานคือการประมวลผลบางอย่างที่ Boost Asio ได้รับการร้องขอให้ทำในนามของรหัสแอปพลิเคชัน บางครั้ง Boost Asio อาจเริ่มงานบางอย่างทันทีที่ได้รับการบอกกล่าวเกี่ยวกับเรื่องนี้และในบางครั้งอาจต้องรอเพื่อทำงานในเวลาต่อมา เมื่อเสร็จสิ้นการทำงาน Boost Asio จะแจ้งแอปพลิเคชันโดยเรียกใช้ตัวจัดการที่ให้มา
Boost.Asio รับประกันว่ารถขนจะทำงานเฉพาะในหัวข้อที่กำลังเรียกrun()
, run_one()
, หรือpoll()
poll_one()
เหล่านี้เป็นหัวข้อที่จะทำงานและโทรไส ดังนั้นในตัวอย่างข้างต้นprint()
จะไม่ถูกเรียกใช้เมื่อโพสต์ลงในio_service
(1) แต่จะถูกเพิ่มลงในio_service
และจะถูกเรียกใช้ในภายหลัง ในกรณีนี้จะอยู่ภายในio_service.run()
(5)
การทำงานแบบอะซิงโครนัสคืออะไร?
การดำเนินการแบบอะซิงโครนัสจะสร้างงานและ Boost Asio จะเรียกตัวจัดการเพื่อแจ้งแอปพลิเคชันเมื่องานเสร็จสมบูรณ์ async_
การดำเนินงานที่ไม่ตรงกันจะถูกสร้างขึ้นโดยการเรียกฟังก์ชั่นที่มีชื่อที่มีคำนำหน้าเป็น ฟังก์ชั่นเหล่านี้ยังเป็นที่รู้จักกันเป็นฟังก์ชั่นการเริ่มต้น
การดำเนินการแบบอะซิงโครนัสสามารถแยกย่อยออกเป็นสามขั้นตอนที่ไม่ซ้ำกัน:
- การเริ่มต้นหรือแจ้งข้อมูลที่เกี่ยวข้อง
io_service
ที่ต้องทำ การasync_receive
ดำเนินการ (3) แจ้งให้ทราบio_service
ว่าจำเป็นต้องอ่านข้อมูลจากซ็อกเก็ตแบบอะซิงโครนัสจากนั้นasync_receive
ส่งกลับทันที
- ทำผลงานจริง. ในกรณีนี้เมื่อได้รับข้อมูลไบต์จะอ่านและคัดลอกลงใน
socket
buffer
งานจริงจะทำใน:
- ฟังก์ชันเริ่มต้น (3) หาก Boost Asio สามารถระบุได้ว่าจะไม่บล็อก
- เมื่อแอปพลิเคชันเรียกใช้
io_service
(5) อย่างชัดเจน
- อัญเชิญReadHandler
handle_async_receive
อีกครั้งตัวจัดการจะถูกเรียกใช้ภายในเธรดที่รันไฟล์io_service
. ดังนั้นไม่ว่างานจะเสร็จเมื่อใด (3 หรือ 5) ก็รับประกันได้ว่าhandle_async_receive()
จะถูกเรียกใช้ภายในio_service.run()
(5) เท่านั้น
การแยกเวลาและช่องว่างระหว่างสามขั้นตอนนี้เรียกว่าการผกผันการควบคุม เป็นหนึ่งในความซับซ้อนที่ทำให้การเขียนโปรแกรมแบบอะซิงโครนัสเป็นเรื่องยาก แต่มีเทคนิคที่จะช่วยให้สามารถลดนี้เช่นโดยใช้coroutines
สิ่งที่ไม่io_service.run()
ทำ?
เมื่อมีการเรียกเธรดio_service.run()
ผู้ทำงานและตัวจัดการจะถูกเรียกใช้จากภายในเธรดนี้ ในตัวอย่างข้างต้นio_service.run()
(5) จะบล็อกจนกว่าจะ:
- มีการเรียกและส่งคืนจาก
print
ตัวจัดการทั้งสองการดำเนินการรับจะเสร็จสมบูรณ์ด้วยความสำเร็จหรือล้มเหลวและhandle_async_receive
ตัวจัดการถูกเรียกและส่งคืน
- จะหยุดการทำงานอย่างชัดเจนผ่านทาง
io_service
io_service::stop()
- มีข้อยกเว้นเกิดขึ้นจากภายในตัวจัดการ
การไหลของ psuedo-ish ที่อาจเกิดขึ้นหนึ่งสามารถอธิบายได้ดังต่อไปนี้:
สร้าง io_service
สร้างซ็อกเก็ต
เพิ่มตัวจัดการการพิมพ์ไปที่ io_service (1)
รอให้ซ็อกเก็ตเชื่อมต่อ (2)
เพิ่มคำของานอ่านแบบอะซิงโครนัสไปยัง io_service (3)
เพิ่มตัวจัดการการพิมพ์ไปที่ io_service (4)
เรียกใช้ io_service (5)
มีงานหรือตัวจัดการ?
ใช่มี 1 งานและ 2 ตัวจัดการ
ซ็อกเก็ตมีข้อมูลหรือไม่ ไม่ทำอะไรเลย
เรียกใช้ตัวจัดการการพิมพ์ (1)
มีงานหรือตัวจัดการ?
ใช่มี 1 งานและ 1 ตัวจัดการ
ซ็อกเก็ตมีข้อมูลหรือไม่ ไม่ทำอะไรเลย
เรียกใช้ตัวจัดการการพิมพ์ (4)
มีงานหรือตัวจัดการ?
ใช่มี 1 งาน
ซ็อกเก็ตมีข้อมูลหรือไม่ ไม่รอต่อไป
- ซ็อกเก็ตรับข้อมูล -
ซ็อกเก็ตมีข้อมูลอ่านเป็นบัฟเฟอร์
เพิ่มตัวจัดการ handle_async_receive ใน io_service
มีงานหรือตัวจัดการ?
ใช่มีตัวจัดการ 1 ตัว
รัน handle_async_receive handler (3)
มีงานหรือตัวจัดการ?
ไม่ตั้งค่า io_service เป็นหยุดและย้อนกลับ
สังเกตว่าเมื่ออ่านเสร็จแล้วจะเพิ่มตัวจัดการอื่นในไฟล์io_service
. รายละเอียดที่ละเอียดอ่อนนี้เป็นคุณสมบัติสำคัญของการเขียนโปรแกรมแบบอะซิงโครนัส ช่วยให้ตัวจัดการสามารถผูกมัดกันได้ ตัวอย่างเช่นหากhandle_async_receive
ไม่ได้รับข้อมูลทั้งหมดที่คาดว่าแล้วการดำเนินงานสามารถโพสต์อื่นดำเนินการอ่านไม่ตรงกันมีผลในการที่มีการทำงานมากขึ้นและทำให้ไม่กลับมาจากio_service
io_service.run()
โปรดทราบว่าเมื่อการio_service
ทำงานหมดลงแอปพลิเคชันจะต้องreset()
มีio_service
ก่อนที่จะรันอีกครั้ง
ตัวอย่างคำถามและตัวอย่างรหัส 3a
ตอนนี้ให้ตรวจสอบรหัสสองส่วนที่อ้างถึงในคำถาม
รหัสคำถาม
socket->async_receive
เพิ่มงานให้กับไฟล์io_service
. ดังนั้นio_service->run()
จะปิดกั้นจนกว่าการดำเนินการอ่านจะเสร็จสมบูรณ์ด้วยความสำเร็จหรือข้อผิดพลาดและClientReceiveEvent
รันเสร็จหรือแสดงข้อยกเว้น
ด้วยความหวังว่าจะทำให้เข้าใจง่ายขึ้นนี่คือตัวอย่างคำอธิบายประกอบที่มีขนาดเล็กกว่า 3a:
void CalculateFib(std::size_t n);
int main()
{
boost::asio::io_service io_service;
boost::optional<boost::asio::io_service::work> work =
boost::in_place(boost::ref(io_service));
boost::thread_group worker_threads;
for(int x = 0; x < 2; ++x)
{
worker_threads.create_thread(
boost::bind(&boost::asio::io_service::run, &io_service)
);
}
io_service.post(boost::bind(CalculateFib, 3));
io_service.post(boost::bind(CalculateFib, 4));
io_service.post(boost::bind(CalculateFib, 5));
work = boost::none;
worker_threads.join_all();
}
ในระดับสูงโปรแกรมจะสร้างเธรด 2 เธรดที่จะประมวลผลio_service
ลูปเหตุการณ์ของ (2) ส่งผลให้มีเธรดพูลอย่างง่ายที่จะคำนวณตัวเลขฟีโบนักชี (3)
ข้อแตกต่างที่สำคัญอย่างหนึ่งระหว่างรหัสคำถามและรหัสนี้คือรหัสนี้เรียกใช้io_service::run()
(2) ก่อนที่จะเพิ่มงานจริงและตัวจัดการในio_service
(3) เพื่อป้องกันไม่ให้io_service::run()
กลับมาทันทีio_service::work
วัตถุจะถูกสร้างขึ้น (1) วัตถุนี้ป้องกันไม่ให้io_service
งานหมด ดังนั้นio_service::run()
จะไม่กลับมาเนื่องจากไม่มีงานทำ
การไหลโดยรวมมีดังนี้:
- สร้างและเพิ่ม
io_service::work
วัตถุที่เพิ่มลงในไฟล์io_service
.
io_service::run()
สระว่ายน้ำของกระทู้ที่สร้างขึ้นจะเรียกว่า เธรดผู้ปฏิบัติงานเหล่านี้จะไม่กลับมาio_service
เนื่องจากio_service::work
อ็อบเจ็กต์
- เพิ่มตัวจัดการ 3 ตัวที่คำนวณตัวเลข Fibonacci ลงใน
io_service
และส่งกลับทันที เธรดของผู้ปฏิบัติงานไม่ใช่เธรดหลักอาจเริ่มเรียกใช้ตัวจัดการเหล่านี้ทันที
- ลบ
io_service::work
วัตถุ
- รอให้เธรดผู้ปฏิบัติงานทำงานเสร็จสิ้น สิ่งนี้จะเกิดขึ้นก็ต่อเมื่อตัวจัดการทั้ง 3 ตัวดำเนินการเสร็จสิ้นเนื่องจาก
io_service
ไม่มีตัวจัดการหรืองาน
รหัสสามารถเขียนแตกต่างกันในลักษณะเดียวกับรหัสต้นฉบับโดยที่ตัวจัดการจะถูกเพิ่มเข้าไปในio_service
นั้นแล้วio_service
จึงประมวลผลลูปเหตุการณ์ สิ่งนี้ไม่จำเป็นต้องใช้io_service::work
และส่งผลให้เกิดรหัสต่อไปนี้:
int main()
{
boost::asio::io_service io_service;
io_service.post(boost::bind(CalculateFib, 3));
io_service.post(boost::bind(CalculateFib, 4));
io_service.post(boost::bind(CalculateFib, 5));
boost::thread_group worker_threads;
for(int x = 0; x < 2; ++x)
{
worker_threads.create_thread(
boost::bind(&boost::asio::io_service::run, &io_service)
);
}
worker_threads.join_all();
}
ซิงโครนัสกับอะซิงโครนัส
แม้ว่ารหัสในคำถามจะใช้การดำเนินการแบบอะซิงโครนัส แต่ก็ทำงานได้อย่างมีประสิทธิภาพพร้อมกันเนื่องจากกำลังรอให้การดำเนินการแบบอะซิงโครนัสเสร็จสมบูรณ์:
socket.async_receive(buffer, handler)
io_service.run();
เทียบเท่ากับ:
boost::asio::error_code error;
std::size_t bytes_transferred = socket.receive(buffer, 0, error);
handler(error, bytes_transferred);
ตามหลักการทั่วไปพยายามหลีกเลี่ยงการผสมการทำงานแบบซิงโครนัสและอะซิงโครนัส บ่อยครั้งอาจเปลี่ยนระบบที่ซับซ้อนให้กลายเป็นระบบที่ซับซ้อนได้ นี้คำตอบที่ไฮไลท์ข้อดีของการเขียนโปรแกรมไม่ตรงกันบางส่วนที่ได้รับความคุ้มครองยังอยู่ใน Boost.Asio เอกสาร