ทำไม std :: future จึงแตกต่างจาก std :: packaged_task และ std :: async?

Sep 11 2020

ฉันได้รู้ว่าสาเหตุที่futureกลับมาstd::asyncมีสถานะพิเศษบางอย่างที่wait on returned futureเกิดขึ้นในผู้ทำลายอนาคต แต่เมื่อเราใช้std::pakaged_taskอนาคตของมันจะไม่แสดงพฤติกรรมเหมือนกัน เพื่อให้เป็นงานที่บรรจุคุณมีการโทรอย่างชัดเจนget()บนวัตถุfuturepackaged_task

ตอนนี้คำถามของฉันคือ:

  1. อะไรคือการดำเนินการภายในของอนาคต (การคิดstd::asyncกับstd::packaged_task)?
  2. เหตุใดจึงไม่นำพฤติกรรมเดียวกันนี้ไปใช้กับการfutureส่งกลับจากstd::packaged_task? หรือกล่าวอีกนัยหนึ่งพฤติกรรมเดียวกันนี้หยุดได้std::packaged_task futureอย่างไร?

หากต้องการดูบริบทโปรดดูรหัสด้านล่าง:

ไม่รอที่จะcountdownทำงานให้เสร็จ อย่างไรก็ตามหากฉันยกเลิกการแสดงความคิดเห็น// int value = ret.get();มันจะเสร็จสิ้นcountdownและชัดเจนเพราะเรากำลังปิดกั้นอนาคตที่จะกลับมาอย่างแท้จริง

    // packaged_task example
#include <iostream>     // std::cout
#include <future>       // std::packaged_task, std::future
#include <chrono>       // std::chrono::seconds
#include <thread>       // std::thread, std::this_thread::sleep_for

// count down taking a second for each value:
int countdown (int from, int to) {
  for (int i=from; i!=to; --i) {
    std::cout << i << std::endl;
    std::this_thread::sleep_for(std::chrono::seconds(1));
  }
  std::cout << "Lift off!" <<std::endl;
  return from-to;
}

int main ()
{
   std::cout << "Start " << std::endl;
  std::packaged_task<int(int,int)> tsk (countdown);   // set up packaged_task
  std::future<int> ret = tsk.get_future();            // get future

  std::thread th (std::move(tsk),10,0);   // spawn thread to count down from 10 to 0

//   int value = ret.get();                  // wait for the task to finish and get result

  std::cout << "The countdown lasted for " << std::endl;//<< value << " seconds.\n";

  th.detach();   

  return 0;
}

ถ้าฉันใช้std::asyncเพื่อรันงานcountdownบนเธรดอื่นไม่ว่าฉันจะใช้get()กับfutureอ็อบเจ็กต์ที่ส่งคืนหรือไม่ก็ตามมันก็จะทำงานให้เสร็จสิ้นเสมอ

// packaged_task example
#include <iostream>     // std::cout
#include <future>       // std::packaged_task, std::future
#include <chrono>       // std::chrono::seconds
#include <thread>       // std::thread, std::this_thread::sleep_for

    // count down taking a second for each value:
    int countdown (int from, int to) {
      for (int i=from; i!=to; --i) {
        std::cout << i << std::endl;
        std::this_thread::sleep_for(std::chrono::seconds(1));
      }
      std::cout << "Lift off!" <<std::endl;
      return from-to;
    }
    
    int main ()
    {
       std::cout << "Start " << std::endl;
      std::packaged_task<int(int,int)> tsk (countdown);   // set up packaged_task
      std::future<int> ret = tsk.get_future();            // get future
    
      auto fut = std::async(std::move(tsk), 10, 0);   

    
    //   int value = fut.get();                  // wait for the task to finish and get result
    
      std::cout << "The countdown lasted for " << std::endl;//<< value << " seconds.\n";

      return 0;
    }

คำตอบ

5 NicolBolas Oct 15 2020 at 08:43

std::asyncมีความรู้ที่ชัดเจนว่างานนั้นได้รับการดำเนินการอย่างไรและที่ไหน นั่นคือหน้าที่ของมัน: เพื่อดำเนินงาน การทำเช่นนั้นจะต้องวางไว้ที่ใดที่หนึ่งจริงๆ บางแห่งอาจเป็นเธรดพูลเธรดที่สร้างขึ้นใหม่หรือในสถานที่ที่จะดำเนินการโดยใครก็ตามที่ทำลายไฟล์future.

เนื่องจากasyncรู้ว่าฟังก์ชันจะดำเนินการอย่างไรจึงมีข้อมูล 100% ที่จำเป็นในการสร้างกลไกที่สามารถสื่อสารได้เมื่อการดำเนินการแบบอะซิงโครนัสที่อาจเกิดขึ้นได้ข้อสรุปรวมทั้งเพื่อให้แน่ใจว่าหากคุณทำลายfutureกลไกใด ๆ ที่จะเกิดขึ้น เรียกใช้ฟังก์ชันนั้นในที่สุดก็จะดำเนินการได้จริง ท้ายที่สุดมันรู้ว่ากลไกนั้นคืออะไร

แต่ไม่ได้packaged_task All packaged_taskdoes คือจัดเก็บอ็อบเจ็กต์ที่เรียกได้ซึ่งสามารถเรียกได้ด้วยอาร์กิวเมนต์ที่กำหนดสร้าง a promiseด้วยชนิดของค่าที่ส่งคืนของฟังก์ชันและระบุวิธีการรับfutureและดำเนินการฟังก์ชันที่สร้างค่า

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

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

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

เนื่องจากpackaged_taskไม่รู้ว่าคุณตั้งใจจะดำเนินการอย่างไรจึงไม่สามารถดำเนินการใด ๆ ได้

packaged_taskโปรดทราบว่านี้จะไม่ซ้ำกับ ทั้งหมด future s สร้างขึ้นจากที่ผู้ใช้สร้างpromiseวัตถุจะไม่ได้มีคุณสมบัติพิเศษasync's futures

ดังนั้นคำถามจริงๆควรจะเป็นสาเหตุที่asyncทำงานในลักษณะนี้ไม่ได้ว่าทำไมคนอื่นไม่ได้

หากคุณต้องการทราบว่าเป็นเพราะความต้องการที่แข่งขันกันสองอย่าง: asyncจำเป็นต้องเป็นวิธีง่ายๆในระดับสูงที่สมองตายในการรับการดำเนินการแบบอะซิงโครนัส (ซึ่งการซิงโครไนซ์กับการทำลายนั้นสมเหตุสมผล) และไม่มีใครต้องการสร้างสิ่งใหม่futureชนิดที่เหมือนกันกับที่มีอยู่บันทึกพฤติกรรมของตัวทำลาย ดังนั้นพวกเขาจึงตัดสินใจที่จะโอเวอร์โหลดวิธีการfutureทำงานทำให้การนำไปใช้งานและการใช้งานซับซ้อนมากเกินไป

5 Nawaz Nov 09 2020 at 01:21

@Nicol Bolas ตอบคำถามนี้ได้ค่อนข้างน่าพอใจแล้ว ดังนั้นฉันจะพยายามตอบคำถามเล็กน้อยจากมุมมองที่แตกต่างกันโดยอธิบายประเด็นที่กล่าวถึงแล้วโดย @Nicol Bolas

การออกแบบสิ่งที่เกี่ยวข้องและเป้าหมาย

พิจารณาฟังก์ชั่นง่ายๆที่เราต้องการเรียกใช้ในรูปแบบต่างๆ:

int add(int a, int b) {
    std::cout << "adding: " << a << ", "<< b << std::endl;
    return a + b;
}

ลืมstd::packaged_task, std ::futureและstd::asyncในขณะที่ขอใช้เวลาในขั้นตอนเดียวหลังและทบทวนวิธีการstd::functionทำงานและสิ่งที่ปัญหาจะทำให้

กรณีที่ 1 - std::functionไม่ดีพอสำหรับการดำเนินการในเธรดที่แตกต่างกัน

std::function<int(int,int)> f { add };

เมื่อเรามีfแล้วเราสามารถดำเนินการได้ในเธรดเดียวกันเช่น:

int result = f(1, 2); //note we can get the result here

หรือในเธรดอื่นเช่นนี้:

std::thread t { std::move(f), 3, 4 };
t.join(); 

หากเราพิจารณาอย่างรอบคอบเราจะรู้ว่าการดำเนินการfในเธรดอื่นทำให้เกิดปัญหาใหม่: เราจะได้ผลลัพธ์ของฟังก์ชันได้อย่างไร? การดำเนินการfในเธรดเดียวกันไม่มีปัญหา - เราได้ผลลัพธ์เป็นค่าที่ส่งคืน แต่เมื่อดำเนินการในเธรดอื่นเราไม่มีวิธีใดที่จะได้ผลลัพธ์ std::packaged_taskนั่นคือสิ่งที่จะแก้ไขได้โดย

กรณีที่ 2 - std::packaged_taskแก้ปัญหาที่std::functionไม่สามารถแก้ไขได้

โดยเฉพาะอย่างยิ่งจะสร้างช่องระหว่างเธรดเพื่อส่งผลลัพธ์ไปยังเธรดอื่น std::functionนอกเหนือจากนั้นก็จะมากขึ้นหรือน้อยลงเช่นเดียวกับ

std::packaged_task<int(int,int)> f { add }; // almost same as before

std::future<int> channel = f.get_future();  // get the channel
    
std::thread t{ std::move(f), 30, 40 }; // same as before
t.join();  // same as before
    
int result = channel.get(); // problem solved: get the result from the channel

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

std::packaged_task<int(int,int)> f { add }; // same as before
std::future<int> channel = f.get_future(); // same as before
    
f(10, 20); // execute it in the current thread !!

int result = channel.get(); // same as before

โดยพื้นฐานstd::functionแล้วและstd::packaged_taskเป็นสิ่งที่คล้ายกัน: พวกเขาเพียงแค่รวมเอนทิตีที่เรียกได้โดยมีความแตกต่างอย่างหนึ่ง: std::packaged_taskเป็นมิตรกับมัลติเธรดเนื่องจากเป็นช่องทางที่สามารถส่งผ่านผลลัพธ์ไปยังเธรดอื่นได้ ทั้งสองไม่ดำเนินการเอนทิตีที่เรียกได้ด้วยตัวเอง เราจำเป็นต้องเรียกใช้ไม่ว่าจะในเธรดเดียวกันหรือในเธรดอื่นเพื่อเรียกใช้เอนทิตีที่เรียกได้แบบห่อ โดยพื้นฐานแล้วมีสองสิ่งในพื้นที่นี้:

  • สิ่งที่จะดำเนินการเช่นฟังก์ชั่นปกติstd::function, std::packaged_taskฯลฯ
  • ดำเนินการอย่างไร / ที่ไหนเช่นเธรดพูลเธรดตัวดำเนินการ ฯลฯ

กรณีที่ 3: std::asyncเป็นสิ่งที่แตกต่างอย่างสิ้นเชิง

มันเป็นสิ่งที่แตกต่างกันเพราะมันรวมสิ่งที่จะดำเนินการด้วยวิธีการ / ที่จะถูกดำเนินการ

std::future<int> fut = std::async(add, 100, 200);
int result = fut.get();

โปรดทราบว่าในกรณีนี้อนาคตที่สร้างขึ้นมีตัวดำเนินการที่เกี่ยวข้องซึ่งหมายความว่าอนาคตจะเสร็จสมบูรณ์ในบางจุดเนื่องจากมีคนดำเนินการอยู่เบื้องหลังฉาก อย่างไรก็ตามในกรณีของอนาคตที่สร้างขึ้นโดยstd::packaged_taskไม่จำเป็นต้องเป็นผู้ดำเนินการและอนาคตนั้นอาจไม่มีวันเสร็จสมบูรณ์หากงานที่สร้างขึ้นนั้นไม่ได้ถูกมอบให้กับผู้ดำเนินการใด ๆ

หวังว่าจะช่วยให้คุณเข้าใจว่าสิ่งต่างๆทำงานอย่างไรเบื้องหลัง ดูการสาธิตออนไลน์

ความแตกต่างระหว่าง std::future

ณ จุดนี้มันค่อนข้างชัดเจนว่ามีสองประเภทstd::futureที่สามารถสร้างได้:

  • สามารถสร้างได้โดยstd::async. อนาคตดังกล่าวมีตัวดำเนินการที่เกี่ยวข้องจึงสามารถดำเนินการให้เสร็จสิ้น
  • ชนิดอื่น ๆ สามารถสร้างขึ้นโดยstd::packaged_taskหรือสิ่งที่คล้ายกัน อนาคตดังกล่าวไม่จำเป็นต้องมีผู้ดำเนินการที่เกี่ยวข้องดังนั้นจึงอาจสมบูรณ์หรือไม่ก็ได้

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

 {
   std::packaged_task<int(int,int)> f { add };
 
   std::future<int> fut = f.get_future(); 

 } // fut goes out of scope, but there is no point 
   // in waiting in its destructor, as it cannot complete 
   // because as `f` is not given to any executor.

หวังว่าคำตอบนี้จะช่วยให้คุณเข้าใจสิ่งต่างๆจากมุมมองที่แตกต่างออกไป

1 M.A Oct 11 2020 at 16:55

การเปลี่ยนแปลงในพฤติกรรมเป็นเพราะความแตกต่างระหว่างและstd::threadstd::async

ในตัวอย่างแรกคุณได้สร้างเธรด daemon โดยการแยกออก ที่คุณพิมพ์std::cout << "The countdown lasted for " << std::endl;ในเธรดหลักของคุณอาจเกิดขึ้นก่อนระหว่างหรือหลังคำสั่งการพิมพ์ภายในcountdownฟังก์ชันเธรด เนื่องจากเธรดหลักไม่รอการสร้างเธรดคุณจึงอาจไม่เห็นสิ่งที่พิมพ์ออกมาทั้งหมด

ในตัวอย่างที่สองคุณเปิดฟังก์ชันเธรดด้วยstd::launch::deferredนโยบาย ลักษณะการทำงานสำหรับ std :: asyncคือ:

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

ในตัวอย่างนี้คุณมีฟิวเจอร์สสองแบบสำหรับสถานะที่ใช้ร่วมกันเดียวกัน ก่อนที่ dtors จะถูกเรียกเมื่อออกจาก main งาน async จะต้องเสร็จสมบูรณ์ แม้ว่าคุณจะไม่ได้กำหนดฟิวเจอร์สไว้อย่างชัดเจนอนาคตชั่วคราวที่ถูกสร้างขึ้นและถูกทำลาย (ส่งคืนจากการเรียกไปที่std::async) จะหมายความว่างานจะเสร็จสมบูรณ์ก่อนที่เธรดหลักจะออก


นี่คือการโพสต์บล็อกที่ดีโดยสกอตต์เมเยอร์สชี้แจงการทำงานของเครื่องstd::futurestd::async

โพสต์ SO ที่เกี่ยวข้อง