สับสนเมื่อ boost :: asio :: io_service เรียกใช้เมธอดบล็อก / ปลดบล็อก


91

ในฐานะที่เป็นผู้เริ่มต้น Boost ทั้งหมด Asio ฉันสับสนกับio_service::run(). ฉันจะขอบคุณถ้ามีใครสามารถอธิบายให้ฉันฟังได้เมื่อวิธีนี้บล็อก / ปลดบล็อก เอกสารระบุ:

run()บล็อกฟังก์ชั่จนกว่าการทำงานทั้งหมดได้เสร็จสิ้นและมีรถขนไม่มากที่จะส่งหรือจนกว่าจะio_serviceได้รับการหยุด

เธรดหลายเธรดอาจเรียกใช้run()ฟังก์ชันเพื่อตั้งค่ากลุ่มเธรดซึ่งio_serviceอาจเรียกใช้งานตัวจัดการ เธรดทั้งหมดที่รออยู่ในพูลจะเทียบเท่ากันและio_serviceอาจเลือกเธรดใดก็ได้เพื่อเรียกใช้ตัวจัดการ

การออกจากrun()ฟังก์ชันปกติหมายความว่าio_serviceวัตถุหยุดทำงาน ( stopped()ฟังก์ชันจะคืนค่าจริง) โทรตามมาrun(), run_one(), poll()หรือจะกลับทันทีจนกว่าจะมีการเรียกร้องก่อนที่จะpoll_one()reset()

ข้อความต่อไปนี้หมายถึงอะไร?

[... ] ไม่มีตัวจัดการอีกต่อไปที่จะถูกส่ง [... ]


ในขณะที่พยายามทำความเข้าใจพฤติกรรมของio_service::run()ฉันก็เจอตัวอย่างนี้(ตัวอย่างที่ 3a) ภายในนั้นฉันสังเกตว่าio_service->run()บล็อกนั้นและรอคำสั่งงาน

// WorkerThread invines io_service->run()
void WorkerThread(boost::shared_ptr<boost::asio::io_service> io_service);
void CalculateFib(size_t);

boost::shared_ptr<boost::asio::io_service> io_service(
    new boost::asio::io_service);
boost::shared_ptr<boost::asio::io_service::work> work(
   new boost::asio::io_service::work(*io_service));

// ...

boost::thread_group worker_threads;
for(int x = 0; x < 2; ++x)
{
  worker_threads.create_thread(boost::bind(&WorkerThread, io_service));
}

io_service->post( boost::bind(CalculateFib, 3));
io_service->post( boost::bind(CalculateFib, 4));
io_service->post( boost::bind(CalculateFib, 5));

work.reset();
worker_threads.join_all();

อย่างไรก็ตามในรหัสต่อไปนี้ที่ฉันกำลังทำงานอยู่ไคลเอนต์จะเชื่อมต่อโดยใช้ TCP / IP และบล็อกเมธอดการรันจนกว่าข้อมูลจะได้รับแบบอะซิงโครนัส

typedef boost::asio::ip::tcp tcp;
boost::shared_ptr<boost::asio::io_service> io_service(
    new boost::asio::io_service);
boost::shared_ptr<tcp::socket> socket(new tcp::socket(*io_service));

// Connect to 127.0.0.1:9100.
tcp::resolver resolver(*io_service);
tcp::resolver::query query("127.0.0.1", 
                           boost::lexical_cast< std::string >(9100));
tcp::resolver::iterator endpoint_iterator = resolver.resolve(query);
socket->connect(endpoint_iterator->endpoint());

// Just blocks here until a message is received.
socket->async_receive(boost::asio::buffer(buf_client, 3000), 0,
                      ClientReceiveEvent);
io_service->run();

// Write response.
boost::system::error_code ignored_error;
std::cout << "Sending message \n";
boost::asio::write(*socket, boost::asio::buffer("some data"), ignored_error);

คำอธิบายใด ๆrun()ที่อธิบายพฤติกรรมของมันในสองตัวอย่างด้านล่างจะได้รับการชื่นชม

คำตอบ:


238

มูลนิธิ

เริ่มต้นด้วยตัวอย่างที่เรียบง่ายและตรวจสอบชิ้นส่วน 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);                             // 1
socket.connect(endpoint);                            // 2
socket.async_receive(buffer, &handle_async_receive); // 3
io_service.post(&print);                             // 4
io_service.run();                                    // 5

คืออะไร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) อย่างชัดเจน
  • อัญเชิญReadHandlerhandle_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_serviceio_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_serviceio_service.run()

โปรดทราบว่าเมื่อการio_serviceทำงานหมดลงแอปพลิเคชันจะต้องreset()มีio_serviceก่อนที่จะรันอีกครั้ง


ตัวอย่างคำถามและตัวอย่างรหัส 3a

ตอนนี้ให้ตรวจสอบรหัสสองส่วนที่อ้างถึงในคำถาม

รหัสคำถาม

socket->async_receiveเพิ่มงานให้กับไฟล์io_service. ดังนั้นio_service->run()จะปิดกั้นจนกว่าการดำเนินการอ่านจะเสร็จสมบูรณ์ด้วยความสำเร็จหรือข้อผิดพลาดและClientReceiveEventรันเสร็จหรือแสดงข้อยกเว้น

ตัวอย่าง 3aรหัส

ด้วยความหวังว่าจะทำให้เข้าใจง่ายขึ้นนี่คือตัวอย่างคำอธิบายประกอบที่มีขนาดเล็กกว่า 3a:

void CalculateFib(std::size_t n);

int main()
{
  boost::asio::io_service io_service;
  boost::optional<boost::asio::io_service::work> work =       // '. 1
      boost::in_place(boost::ref(io_service));                // .'

  boost::thread_group worker_threads;                         // -.
  for(int x = 0; x < 2; ++x)                                  //   :
  {                                                           //   '.
    worker_threads.create_thread(                             //     :- 2
      boost::bind(&boost::asio::io_service::run, &io_service) //   .'
    );                                                        //   :
  }                                                           // -'

  io_service.post(boost::bind(CalculateFib, 3));              // '.
  io_service.post(boost::bind(CalculateFib, 4));              //   :- 3
  io_service.post(boost::bind(CalculateFib, 5));              // .'

  work = boost::none;                                         // 4
  worker_threads.join_all();                                  // 5
}

ในระดับสูงโปรแกรมจะสร้างเธรด 2 เธรดที่จะประมวลผลio_serviceลูปเหตุการณ์ของ (2) ส่งผลให้มีเธรดพูลอย่างง่ายที่จะคำนวณตัวเลขฟีโบนักชี (3)

ข้อแตกต่างที่สำคัญอย่างหนึ่งระหว่างรหัสคำถามและรหัสนี้คือรหัสนี้เรียกใช้io_service::run()(2) ก่อนที่จะเพิ่มงานจริงและตัวจัดการในio_service(3) เพื่อป้องกันไม่ให้io_service::run()กลับมาทันทีio_service::workวัตถุจะถูกสร้างขึ้น (1) วัตถุนี้ป้องกันไม่ให้io_serviceงานหมด ดังนั้นio_service::run()จะไม่กลับมาเนื่องจากไม่มีงานทำ

การไหลโดยรวมมีดังนี้:

  1. สร้างและเพิ่มio_service::workวัตถุที่เพิ่มลงในไฟล์io_service.
  2. io_service::run()สระว่ายน้ำของกระทู้ที่สร้างขึ้นจะเรียกว่า เธรดผู้ปฏิบัติงานเหล่านี้จะไม่กลับมาio_serviceเนื่องจากio_service::workอ็อบเจ็กต์
  3. เพิ่มตัวจัดการ 3 ตัวที่คำนวณตัวเลข Fibonacci ลงในio_serviceและส่งกลับทันที เธรดของผู้ปฏิบัติงานไม่ใช่เธรดหลักอาจเริ่มเรียกใช้ตัวจัดการเหล่านี้ทันที
  4. ลบio_service::workวัตถุ
  5. รอให้เธรดผู้ปฏิบัติงานทำงานเสร็จสิ้น สิ่งนี้จะเกิดขึ้นก็ต่อเมื่อตัวจัดการทั้ง 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));              //   :- 3
  io_service.post(boost::bind(CalculateFib, 5));              // .'

  boost::thread_group worker_threads;                         // -.
  for(int x = 0; x < 2; ++x)                                  //   :
  {                                                           //   '.
    worker_threads.create_thread(                             //     :- 2
      boost::bind(&boost::asio::io_service::run, &io_service) //   .'
    );                                                        //   :
  }                                                           // -'
  worker_threads.join_all();                                  // 5
}

ซิงโครนัสกับอะซิงโครนัส

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

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 เอกสาร


13
โพสต์ที่ยอดเยี่ยม ฉันต้องการเพิ่มเพียงสิ่งเดียวเพราะฉันรู้สึกว่ามันไม่ได้รับความสนใจมากพอ: หลังจากที่ run () กลับมาคุณต้องเรียก reset () บน io_service ของคุณก่อนจึงจะสามารถเรียกใช้ () ได้อีกครั้ง มิฉะนั้นอาจส่งคืนทันทีไม่ว่าจะมีการดำเนินการ async_ รออยู่หรือไม่
DeVadder

บัฟเฟอร์มาจากไหน? มันคืออะไร?
ruipacheco

ตอนนี้ยังงง ๆ ถ้าไม่แนะนำให้ผสมเป็นซิงก์และ async แล้วโหมด async บริสุทธิ์คืออะไร? คุณสามารถยกตัวอย่างการแสดงรหัสที่ไม่มี io_service.run ();?
Splash

@Splash One สามารถใช้io_service.poll()เพื่อประมวลผลลูปเหตุการณ์โดยไม่ปิดกั้นการทำงานที่โดดเด่น คำแนะนำหลักเพื่อหลีกเลี่ยงการผสมการดำเนินการแบบซิงโครนัสและอะซิงโครนัสคือการหลีกเลี่ยงการเพิ่มความซับซ้อนที่ไม่จำเป็นและเพื่อป้องกันการตอบสนองที่ไม่ดีเมื่อตัวจัดการใช้เวลานานในการดำเนินการให้เสร็จสมบูรณ์ มีบางกรณีที่ปลอดภัยเช่นเมื่อรู้ว่าการทำงานแบบซิงโครนัสจะไม่ปิดกั้น
Tanner Sansbury

คุณหมายถึงอะไรโดย "ปัจจุบัน" ใน"Boost Asio รับประกันว่าตัวจัดการจะทำงานภายในเธรดที่กำลังเรียกrun() .... " เท่านั้น ? ถ้ามี N เธรด (ที่มีการเรียกrun()) แล้วเธรดใดคือเธรด "ปัจจุบัน"? สามารถมีมากมาย? หรือคุณหมายถึงเธรดที่ดำเนินการasync_*()(พูดasync_read) เสร็จแล้วรับประกันว่าจะเรียกตัวจัดการได้เช่นกัน?
Nawaz

19

เพื่อลดความซับซ้อนของสิ่งที่runทำให้คิดว่าเป็นพนักงานที่ต้องประมวลผลกองกระดาษ ใช้เวลาหนึ่งแผ่นทำในสิ่งที่แผ่นงานบอกโยนแผ่นงานออกไปและนำแผ่นงานถัดไป เมื่อเขาหมดผ้าปูที่นอนก็จะออกจากสำนักงาน ในแต่ละแผ่นสามารถมีคำสั่งประเภทใดก็ได้แม้กระทั่งการเพิ่มแผ่นงานใหม่ลงในกอง กลับไป ASIO: คุณสามารถให้กับผู้io_serviceทำงานในสองวิธีหลัก: โดยใช้postที่มันเป็นในตัวอย่างคุณเชื่อมโยงหรือโดยการใช้วัตถุอื่น ๆ ที่ภายในโทรpostบนio_serviceเหมือนsocketและasync_*วิธีการ

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.