std :: สัญญาคืออะไร?


384

ผมค่อนข้างคุ้นเคยกับ C ++ 11 std::thread, std::asyncและstd::futureส่วนประกอบ (เช่นดูคำตอบนี้ ) ซึ่งจะตรงไปข้างหน้า

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

ใครช่วยกรุณาstd::promiseเล่าตัวอย่างสั้น ๆ สังเขปของสถานการณ์ที่จำเป็นและเป็นวิธีแก้ปัญหาที่สำนวนที่สุด?


2
นี่คือโค้ดบางส่วนใน: en.cppreference.com/w/cpp/thread/future
chris

58
รุ่นมันสั้นจริงๆคือ std::promiseเป็นที่ที่std::futures มาจาก std::futureเป็นสิ่งที่ช่วยให้คุณสามารถเรียกคืนค่าที่สัญญากับคุณ เมื่อคุณโทรหาget()ในอนาคตมันจะรอจนกว่าเจ้าของstd::promiseที่จะกำหนดค่า (โดยการโทรหาset_valueสัญญา) หากสัญญาถูกทำลายก่อนที่จะมีการตั้งค่าและจากนั้นคุณโทรหาget()อนาคตที่เกี่ยวข้องกับสัญญานั้นคุณจะได้รับการstd::broken_promiseยกเว้นเนื่องจากคุณได้รับสัญญามูลค่า แต่เป็นไปไม่ได้ที่คุณจะได้รับ
James McNellis

14
ฉันแนะนำว่าถ้าคุณสามารถ / ต้องการดูที่C ++ Concurrency ใน ActionโดยAnthony Williams
David Rodríguez - dribeas

32
@KerrekSB std::broken_promiseเป็นตัวระบุชื่อที่ดีที่สุดในไลบรารีมาตรฐาน std::atomic_futureและไม่มี
Cubbi

3
Downvoter สนใจอธิบายคำคัดค้านของคุณไหม?
Kerrek SB

คำตอบ:


182

ในคำพูดของ [futures.state] a std::futureเป็นวัตถุส่งคืนแบบอะซิงโครนัส ("วัตถุที่อ่านผลลัพธ์จากสถานะที่ใช้ร่วมกัน") และ a std::promiseเป็นผู้ให้บริการแบบอะซิงโครนัส ("วัตถุที่ให้ผลลัพธ์ สัญญาเป็นสิ่งที่คุณตั้งค่าผลลัพธ์เพื่อให้คุณสามารถได้รับจากอนาคตที่เกี่ยวข้อง

ตัวให้บริการแบบอะซิงโครนัสคือสิ่งที่เริ่มต้นสร้างสถานะที่ใช้ร่วมกันซึ่งหมายถึงอนาคต std::promiseเป็นผู้ให้บริการแบบอะซิงโครนัสชนิดหนึ่งเป็นอีกประเภทหนึ่งstd::packaged_taskและรายละเอียดภายในของstd::asyncเป็นอีกประเภทหนึ่ง แต่ละคนสามารถสร้างสถานะที่ใช้ร่วมกันและให้คุณมีสถานะที่ใช้ร่วมกันstd::futureและสามารถทำให้รัฐพร้อม

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

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


10
อาจทำให้สิ้นเปลืองทรัพยากร อาจไม่ถูกต้องหากรหัสนั้นไม่สามารถขนานกันได้
ลูกสุนัข

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

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

496

ตอนนี้ฉันเข้าใจสถานการณ์ดีขึ้นเล็กน้อย (ในจำนวนเล็กน้อยเนื่องจากคำตอบที่นี่!) ดังนั้นฉันจึงคิดว่าฉันจะเพิ่มการเขียนของตัวเองเล็กน้อย


มีแนวคิดที่แตกต่างกันสองประการแม้ว่าเกี่ยวข้องกันใน C ++ 11: การคำนวณแบบอะซิงโครนัส (ฟังก์ชันที่เรียกว่าที่อื่น) และการดำเนินการพร้อมกัน ( เธรดสิ่งที่ทำงานพร้อมกัน) ทั้งสองเป็นแนวความคิดมุมฉาก การคำนวณแบบอะซิงโครนัสเป็นเพียงรสชาติที่แตกต่างของการเรียกใช้ฟังก์ชันในขณะที่เธรดเป็นบริบทการดำเนินการ หัวข้อมีประโยชน์ในสิทธิของตนเอง แต่สำหรับวัตถุประสงค์ของการสนทนานี้ฉันจะถือว่าพวกเขาเป็นรายละเอียดการดำเนินการ


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

int foo(double, char, bool);

ปิดครั้งแรกที่เรามีแม่แบบซึ่งหมายถึงมูลค่าในอนาคตประเภทstd::future<T> Tค่าสามารถดึงผ่านฟังก์ชั่นสมาชิกget()ซึ่งประสานโปรแกรมได้อย่างมีประสิทธิภาพโดยรอผล อีกวิธีหนึ่งคือการรองรับในอนาคตwait_for()ซึ่งสามารถใช้ในการตรวจสอบว่ามีผลลัพธ์อยู่หรือไม่ ฟิวเจอร์สควรถูกมองว่าเป็นแบบดรอปดาวน์แบบอะซิงโครนัสแทนประเภทผลตอบแทนปกติ std::future<int>สำหรับฟังก์ชั่นตัวอย่างของเราเราคาดหวัง

จากนี้ไปสู่ลำดับชั้นจากระดับสูงสุดถึงระดับต่ำสุด:

  1. std::async: วิธีที่สะดวกและตรงไปตรงมาที่สุดในการคำนวณแบบอะซิงโครนัสคือผ่านasyncเทมเพลตฟังก์ชั่นซึ่งจะคืนค่าการจับคู่ในอนาคตทันที:

    auto fut = std::async(foo, 1.5, 'x', false);  // is a std::future<int>

    เรามีการควบคุมรายละเอียดน้อยมาก โดยเฉพาะอย่างยิ่งเราไม่รู้ด้วยซ้ำว่าฟังก์ชั่นนั้นทำงานพร้อมกันตามลำดับget()หรือตามด้วยเวทมนตร์ดำอื่น ๆ อย่างไรก็ตามผลที่ได้รับง่ายเมื่อต้องการ:

    auto res = fut.get();  // is an int
  2. ตอนนี้เราสามารถพิจารณาวิธีการใช้งานบางอย่างเช่นasyncแต่ในแบบที่เราควบคุม ตัวอย่างเช่นเราอาจยืนยันว่าฟังก์ชั่นจะดำเนินการในหัวข้อที่แยกต่างหาก เรารู้อยู่แล้วว่าเราสามารถจัดเตรียมเธรดแยกต่างหากโดยใช้std::threadคลาส

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

    std::packaged_task<int(double, char, bool)> tsk(foo);
    
    auto fut = tsk.get_future();    // is a std::future<int>

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

    std::thread thr(std::move(tsk), 1.5, 'x', false);

    เธรดเริ่มทำงานทันที เราสามารถทำได้detachหรือมีjoinไว้ที่ส่วนท้ายของขอบเขตหรือเมื่อใดก็ตาม (เช่นใช้scoped_threadเสื้อคลุมของ Anthony Williams ซึ่งควรอยู่ในไลบรารีมาตรฐานจริง ๆ ) แม้ว่ารายละเอียดการใช้งานstd::threadจะไม่เกี่ยวข้องกับเราที่นี่ เพียงให้แน่ใจว่าได้เข้าร่วมหรือแยกออกthrในที่สุด สิ่งสำคัญคือเมื่อใดก็ตามที่การเรียกใช้ฟังก์ชันเสร็จสิ้นผลลัพธ์ของเราก็พร้อม:

    auto res = fut.get();  // as before
  3. ตอนนี้เรากำลังลงสู่ระดับต่ำสุด: เราจะดำเนินการตามภารกิจของแพคเกจได้อย่างไร ที่นี่คือที่std::promiseมาสัญญาเป็นหน่วยการสร้างสำหรับการสื่อสารกับอนาคต ขั้นตอนหลัก ได้แก่ :

    • ด้ายเรียกทำให้สัญญา

    • หัวข้อการโทรได้รับอนาคตจากสัญญา

    • คำสัญญาพร้อมกับข้อโต้แย้งฟังก์ชันถูกย้ายไปยังเธรดแยกต่างหาก

    • เธรดใหม่ดำเนินการฟังก์ชันและปฏิบัติตามสัญญา

    • เธรดดั้งเดิมดึงผลลัพธ์

    ตัวอย่างเช่นต่อไปนี้เป็น "ภารกิจบรรจุ" ของเรา:

    template <typename> class my_task;
    
    template <typename R, typename ...Args>
    class my_task<R(Args...)>
    {
        std::function<R(Args...)> fn;
        std::promise<R> pr;             // the promise of the result
    public:
        template <typename ...Ts>
        explicit my_task(Ts &&... ts) : fn(std::forward<Ts>(ts)...) { }
    
        template <typename ...Ts>
        void operator()(Ts &&... ts)
        {
            pr.set_value(fn(std::forward<Ts>(ts)...));  // fulfill the promise
        }
    
        std::future<R> get_future() { return pr.get_future(); }
    
        // disable copy, default move
    };

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


ทำให้มีข้อยกเว้น

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

  • สัญญาที่สร้างขึ้นเป็นค่าเริ่มต้นจะไม่ทำงาน สัญญาที่ไม่ใช้งานสามารถตายได้โดยไม่มีผล

  • get_future()สัญญาจะใช้งานเมื่ออนาคตที่จะได้รับผ่านทาง อย่างไรก็ตามอาจมีเพียงหนึ่งอนาคตที่จะได้รับ!

  • สัญญาจะต้องเป็นที่พึงพอใจผ่านset_value()หรือมีการตั้งค่าข้อยกเว้นผ่านset_exception()ก่อนที่มันจะจบชีวิตถ้ามันจะถูกใช้ในอนาคต คำสัญญาที่น่าพอใจสามารถตายได้โดยปราศจากผลและget()จะเกิดขึ้นในอนาคต สัญญาที่มีข้อยกเว้นจะเพิ่มข้อยกเว้นที่เก็บไว้เมื่อมีการโทรget()ในอนาคต หากสัญญานั้นตายไปโดยไม่มีค่าและไม่มีข้อยกเว้นการเรียกget()ในอนาคตจะเพิ่มข้อยกเว้น "สัญญาที่เสียหาย"

นี่คือชุดทดสอบเล็ก ๆ น้อย ๆ ที่แสดงให้เห็นถึงพฤติกรรมที่ยอดเยี่ยมเหล่านี้ ก่อนเทียม:

#include <iostream>
#include <future>
#include <exception>
#include <stdexcept>

int test();

int main()
{
    try
    {
        return test();
    }
    catch (std::future_error const & e)
    {
        std::cout << "Future error: " << e.what() << " / " << e.code() << std::endl;
    }
    catch (std::exception const & e)
    {
        std::cout << "Standard exception: " << e.what() << std::endl;
    }
    catch (...)
    {
        std::cout << "Unknown exception." << std::endl;
    }
}

ตอนนี้การทดสอบ

กรณีที่ 1: คำสัญญาที่ไม่ใช้งาน

int test()
{
    std::promise<int> pr;
    return 0;
}
// fine, no problems

กรณีที่ 2: สัญญาที่ยังไม่ได้ใช้

int test()
{
    std::promise<int> pr;
    auto fut = pr.get_future();
    return 0;
}
// fine, no problems; fut.get() would block indefinitely

กรณีที่ 3: ฟิวเจอร์มากเกินไป

int test()
{
    std::promise<int> pr;
    auto fut1 = pr.get_future();
    auto fut2 = pr.get_future();  //   Error: "Future already retrieved"
    return 0;
}

กรณีที่ 4: สัญญาที่น่าพอใจ

int test()
{
    std::promise<int> pr;
    auto fut = pr.get_future();

    {
        std::promise<int> pr2(std::move(pr));
        pr2.set_value(10);
    }

    return fut.get();
}
// Fine, returns "10".

กรณีที่ 5: ความพึงพอใจมากเกินไป

int test()
{
    std::promise<int> pr;
    auto fut = pr.get_future();

    {
        std::promise<int> pr2(std::move(pr));
        pr2.set_value(10);
        pr2.set_value(10);  // Error: "Promise already satisfied"
    }

    return fut.get();
}

ยกเว้นอย่างเดียวจะโยนถ้ามีมากกว่าหนึ่งอย่างใดอย่างหนึ่งของหรือset_valueset_exception

กรณีที่ 6: ข้อยกเว้น

int test()
{
    std::promise<int> pr;
    auto fut = pr.get_future();

    {
        std::promise<int> pr2(std::move(pr));
        pr2.set_exception(std::make_exception_ptr(std::runtime_error("Booboo")));
    }

    return fut.get();
}
// throws the runtime_error exception

กรณีที่ 7: สัญญาแตก

int test()
{
    std::promise<int> pr;
    auto fut = pr.get_future();

    {
        std::promise<int> pr2(std::move(pr));
    }   // Error: "broken promise"

    return fut.get();
}

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

9
คำตอบที่ดีขอบคุณสำหรับความช่วยเหลือของคุณเกี่ยวกับ std :: async ฉันจำได้ว่าเราสามารถตรวจสอบว่ามันจะวางไข่เธรดอื่นหรือซิงโครนัสทำงานกับแฟล็ก (std :: launch :: async, std :: launch :: deferred)
StereoMatching

1
@ FelixDombek: การส่งต่อที่สมบูรณ์แบบ ฯลฯstd::functionมีตัวสร้างจำนวนมาก my_taskไม่มีเหตุผลที่จะไม่เปิดเผยเหล่านั้นไปยังผู้บริโภคของ
Kerrek SB

1
@DaveedV: ขอบคุณสำหรับคำติชม! ใช่นั่นคือกรณีทดสอบ 7: ถ้าคุณทำลายสัญญาโดยไม่ตั้งค่าหรือข้อยกเว้นการเรียกget()ใช้ในอนาคตจะทำให้เกิดข้อยกเว้น ฉันจะชี้แจงเรื่องนี้โดยการเพิ่ม "ก่อนที่มันจะถูกทำลาย"; โปรดแจ้งให้เราทราบหากมีความชัดเจนเพียงพอ
Kerrek SB

3
ในที่สุดgot()ฉันก็อ่านfutureไลบรารีสนับสนุนเธรดโดยใช้promiseคำอธิบายที่น่าทึ่งของคุณ!
ดวงจันทร์ที่แดดส่องถึง

33

Bartosz Milewskiให้การเขียนที่ดี

C ++ แยกการใช้งานของฟิวเจอร์สเป็นชุดของบล็อกเล็ก ๆ

std :: สัญญาเป็นหนึ่งในส่วนเหล่านี้

สัญญาคือยานพาหนะสำหรับส่งค่าส่งคืน (หรือข้อยกเว้น) จากเธรดที่เรียกใช้ฟังก์ชันไปยังเธรดที่รับเงินในฟังก์ชันในอนาคต

...

อนาคตคือวัตถุการซิงโครไนซ์ที่สร้างขึ้นรอบ ๆ จุดสิ้นสุดของการรับสัญญา

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

ตัวอย่างจากหน้าคือ:

promise<int> intPromise;
future<int> intFuture = intPromise.get_future();
std::thread t(asyncFun, std::move(intPromise));
// do some other stuff
int result = intFuture.get(); // may throw MyException

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

28

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


12
@KerrekSB: std::asyncสามารถมีแนวคิด (นี่ไม่ได้เป็นข้อบังคับโดยมาตรฐาน) ที่เข้าใจกันว่าเป็นฟังก์ชั่นที่สร้างstd::promise, ผลักเข้าไปในเธรดพูล (เรียงลำดับ, อาจเป็นเธรดพูล, อาจเป็นเธรดใหม่, ... ) และส่งคืน ที่เกี่ยวข้องstd::futureกับการโทร บนฝั่งไคลเอ็นต์ที่คุณจะรอบนstd::futureและด้ายในส่วนอื่น ๆ std::promiseจะคำนวณผลและเก็บไว้ใน หมายเหตุ: มาตรฐานต้องมีสถานะที่ใช้ร่วมกันและstd::futureไม่ได้มีอยู่ของ a std::promiseในกรณีการใช้งานเฉพาะนี้
David Rodríguez - dribeas

6
@KerrekSB: std::futureจะไม่เรียกjoinใช้เธรด แต่จะมีตัวชี้ไปยังสถานะที่ใช้ร่วมกันซึ่งเป็นบัฟเฟอร์การสื่อสารจริง สาธารณะรัฐมีกลไกการประสาน (อาจจะstd::function+ std::condition_variableจะล็อคโทรจนstd::promiseเป็นจริง. การดำเนินการของด้ายเป็นมุมฉากทั้งหมดนี้และในการใช้งานมากมายที่คุณอาจพบว่าstd::asyncยังไม่ได้ดำเนินการโดยหัวข้อใหม่ที่จะเข้าร่วมแล้ว แต่ ค่อนข้างสระว่ายน้ำด้ายที่มีชีวิตขยายไปถึงจุดสิ้นสุดของโปรแกรม
เดวิดRodríguez - dribeas

1
@ DavidRodríguez-dribeas: โปรดแก้ไขข้อมูลจากความคิดเห็นในคำตอบของคุณ
Marc Mutz - mmutz

2
@JonathanWakely: นั่นไม่ได้หมายความว่าจะต้องดำเนินการในเธรดใหม่เท่านั้นที่จะต้องดำเนินการแบบอะซิงโครนัสราวกับว่ามันถูกเรียกใช้ในเธรดที่สร้างขึ้นใหม่ ข้อได้เปรียบหลักของstd::asyncคือไลบรารีรันไทม์สามารถทำการตัดสินใจที่ถูกต้องสำหรับคุณเกี่ยวกับจำนวนเธรดที่จะสร้างและในกรณีส่วนใหญ่ฉันคาดว่ารันไทม์ที่ใช้เธรดพูล ปัจจุบัน VS2012 ใช้เธรดพูลภายใต้ประทุนและจะไม่ละเมิดกฎas-if ทราบว่ามีการค้ำประกันน้อยมากที่จะต้องปฏิบัติตามสำหรับการนี้โดยเฉพาะอย่างยิ่งเป็นถ้า
David Rodríguez - dribeas

1
ชาวกระทู้จะต้องเริ่มต้นใหม่ แต่กฎ as-if อนุญาตให้มีสิ่งใด (ซึ่งเป็นเหตุผลที่ฉันใส่ "ราวกับว่า" ในตัวเอียง :)
โจนาธาน Wakely

11

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


8

มีสามหน่วยงานหลักในการประมวลผลแบบอะซิงโครนัส ปัจจุบัน C ++ 11 เน้นที่ 2 รายการ

สิ่งสำคัญที่คุณต้องใช้ตรรกะบางอย่างแบบอะซิงโครนัสคือ:

  1. งาน (ตรรกะบรรจุเป็นวัตถุ functor บางคน) ที่จะทำงาน 'ที่ใดที่หนึ่ง'
  2. โหนดประมวลผลที่เกิดขึ้นจริง - ด้ายกระบวนการอื่น ๆ ที่ทำงาน functors ดังกล่าวเมื่อพวกเขามีให้กับมัน ดูที่รูปแบบการออกแบบ "คำสั่ง" สำหรับความคิดที่ดีว่ากลุ่มเธรดของผู้ปฏิบัติงานขั้นพื้นฐานทำได้อย่างไร
  3. จับผล : ใครบางคนต้องการผลที่และความต้องการวัตถุที่จะได้รับมันสำหรับพวกเขา สำหรับ OOP และเหตุผลอื่น ๆ การรอหรือการซิงโครไนซ์ใด ๆ ควรทำใน API ของหมายเลขอ้างอิงนี้

C ++ 11 เรียกร้องในสิ่งที่ผมพูดถึงใน (1) std::promiseและผู้ที่อยู่ใน std::future(3) std::threadเป็นสิ่งเดียวที่เปิดเผยต่อสาธารณะสำหรับ (2) นี่เป็นเรื่องที่โชคร้ายเพราะโปรแกรมจริงต้องจัดการทรัพยากรของเธรดและหน่วยความจำและส่วนใหญ่จะต้องการให้ทำงานบนเธรดพูลแทนที่จะสร้างและทำลายเธรดสำหรับงานเล็ก ๆ น้อย ๆ ทุกครั้ง (ซึ่งมักจะทำให้ประสิทธิภาพการทำงานที่ไม่จำเป็น ความอดอยากที่ยิ่งเลวร้ายลง)

จากข้อมูลของ Herb Sutter และคนอื่น ๆ ใน C ++ 11 เชื่อใจในสมองมีแผนเบื้องต้นที่จะเพิ่มสิ่งstd::executorที่คล้ายใน Java จะเป็นพื้นฐานสำหรับเธรดพูลและการตั้งค่าที่คล้ายคลึงกันทางตรรกะสำหรับ (2) บางทีเราจะเห็นมันใน C ++ 2014 แต่การเดิมพันของฉันจะเหมือน C ++ 17 (และพระเจ้าช่วยเราถ้าพวกมันบอทมาตรฐานสำหรับสิ่งเหล่านี้)


7

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

คุณสามารถคิดว่ามันเป็นหนึ่งเธรดสร้างสัญญาเพื่อให้ข้อมูลและหัวข้ออื่น ๆ รวบรวมสัญญาในอนาคต กลไกนี้สามารถใช้ได้เพียงครั้งเดียว

กลไกสัญญา / อนาคตเป็นเพียงทิศทางเดียวจากเธรดที่ใช้set_value()วิธีการของ a std::promiseไปยังเธรดที่ใช้get()a ของstd::futureเพื่อรับข้อมูล ข้อยกเว้นจะถูกสร้างขึ้นหากget()วิธีการในอนาคตถูกเรียกมากกว่าหนึ่งครั้ง

หากเธรดที่std::promiseไม่ได้ใช้set_value()เพื่อเติมเต็มสัญญาของมันแล้วเมื่อเธรดที่สองเรียกget()ใช้std::futureเพื่อรวบรวมสัญญาเธรดที่สองจะเข้าสู่สถานะรอจนกว่าสัญญาจะเสร็จสมบูรณ์โดยเธรดแรกด้วยstd::promiseเมื่อใช้set_value()เมธอด เพื่อส่งข้อมูล

ด้วย coroutines ที่เสนอของข้อมูลจำเพาะทางเทคนิค N4663 ภาษาโปรแกรม - ส่วนขยาย C ++ สำหรับ Coroutinesและการสนับสนุนคอมไพเลอร์ Visual Studio 2017 C ++ ของco_awaitยังเป็นไปได้ที่จะใช้std::futureและstd::asyncเขียนฟังก์ชันการทำงานของ coroutine ดูการอภิปรายและเป็นตัวอย่างในhttps://stackoverflow.com/a/50753040/1466970ซึ่งมีเป็นส่วนหนึ่งที่กล่าวถึงการใช้ของที่มีstd::futureco_await

ตัวอย่างรหัสต่อไปนี้เป็นแอปพลิเคชันคอนโซล Windows Visual Studio 2013 แบบง่าย ๆ แสดงโดยใช้คลาส / แม่แบบการทำงานพร้อมกันของ C ++ 11 และฟังก์ชันอื่น ๆ มันแสดงให้เห็นถึงการใช้งานสำหรับสัญญา / อนาคตซึ่งทำงานได้ดีกระทู้อิสระที่จะทำงานบางอย่างและหยุดและการใช้งานที่จำเป็นต้องมีพฤติกรรมซิงโครนัสมากขึ้นและเนื่องจากความต้องการการแจ้งเตือนหลายคู่สัญญา / อนาคตไม่ทำงาน

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

ส่วนแรกของการmain()สร้างสามกระทู้เพิ่มเติมและการใช้std::promiseและstd::futureการส่งข้อมูลระหว่างหัวข้อ จุดที่น่าสนใจคือที่ที่เธรดหลักเริ่มเธรด T2 ซึ่งจะรอข้อมูลจากเธรดหลักทำอะไรแล้วส่งข้อมูลไปยังเธรดที่สาม T3 ซึ่งจะทำบางสิ่งบางอย่างและส่งข้อมูลกลับไปที่ หัวข้อหลัก

ส่วนที่สองของการmain()สร้างสองเธรดและชุดของคิวเพื่ออนุญาตให้หลายข้อความจากเธรดหลักไปยังแต่ละเธรดที่สร้างขึ้นสองรายการ เราไม่สามารถใช้งานได้std::promiseและstd::futureเพราะสิ่งนี้เพราะคำสัญญา / คู่หูในอนาคตเป็นนัดเดียวและไม่สามารถใช้ซ้ำได้

แหล่งที่มาของคลาสSync_queueมาจากภาษา C ++ Programming Stroustrup: 4th Edition

// cpp_threads.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"
#include <iostream>
#include <thread>  // std::thread is defined here
#include <future>  // std::future and std::promise defined here

#include <list>    // std::list which we use to build a message queue on.

static std::atomic<int> kount(1);       // this variable is used to provide an identifier for each thread started.

//------------------------------------------------
// create a simple queue to let us send notifications to some of our threads.
// a future and promise are one shot type of notifications.
// we use Sync_queue<> to have a queue between a producer thread and a consumer thread.
// this code taken from chapter 42 section 42.3.4
//   The C++ Programming Language, 4th Edition by Bjarne Stroustrup
//   copyright 2014 by Pearson Education, Inc.
template<typename Ttype>
class Sync_queue {
public:
    void  put(const Ttype &val);
    void  get(Ttype &val);

private:
    std::mutex mtx;                   // mutex used to synchronize queue access
    std::condition_variable cond;     // used for notifications when things are added to queue
    std::list <Ttype> q;              // list that is used as a message queue
};

template<typename Ttype>
void Sync_queue<Ttype>::put(const Ttype &val) {
    std::lock_guard <std::mutex> lck(mtx);
    q.push_back(val);
    cond.notify_one();
}

template<typename Ttype>
void Sync_queue<Ttype>::get(Ttype &val) {
    std::unique_lock<std::mutex> lck(mtx);
    cond.wait(lck, [this]{return  !q.empty(); });
    val = q.front();
    q.pop_front();
}
//------------------------------------------------


// thread function that starts up and gets its identifier and then
// waits for a promise to be filled by some other thread.
void func(std::promise<int> &jj) {
    int myId = std::atomic_fetch_add(&kount, 1);   // get my identifier
    std::future<int> intFuture(jj.get_future());
    auto ll = intFuture.get();   // wait for the promise attached to the future
    std::cout << "  func " << myId << " future " << ll << std::endl;
}

// function takes a promise from one thread and creates a value to provide as a promise to another thread.
void func2(std::promise<int> &jj, std::promise<int>&pp) {
    int myId = std::atomic_fetch_add(&kount, 1);   // get my identifier
    std::future<int> intFuture(jj.get_future());
    auto ll = intFuture.get();     // wait for the promise attached to the future

    auto promiseValue = ll * 100;   // create the value to provide as promised to the next thread in the chain
    pp.set_value(promiseValue);
    std::cout << "  func2 " << myId << " promised " << promiseValue << " ll was " << ll << std::endl;
}

// thread function that starts up and waits for a series of notifications for work to do.
void func3(Sync_queue<int> &q, int iBegin, int iEnd, int *pInts) {
    int myId = std::atomic_fetch_add(&kount, 1);

    int ll;
    q.get(ll);    // wait on a notification and when we get it, processes it.
    while (ll > 0) {
        std::cout << "  func3 " << myId << " start loop base " << ll << " " << iBegin << " to " << iEnd << std::endl;
        for (int i = iBegin; i < iEnd; i++) {
            pInts[i] = ll + i;
        }
        q.get(ll);  // we finished this job so now wait for the next one.
    }
}

int _tmain(int argc, _TCHAR* argv[])
{
    std::chrono::milliseconds myDur(1000);

    // create our various promise and future objects which we are going to use to synchronise our threads
    // create our three threads which are going to do some simple things.
    std::cout << "MAIN #1 - create our threads." << std::endl;

    // thread T1 is going to wait on a promised int
    std::promise<int> intPromiseT1;
    std::thread t1(func, std::ref(intPromiseT1));

    // thread T2 is going to wait on a promised int and then provide a promised int to thread T3
    std::promise<int> intPromiseT2;
    std::promise<int> intPromiseT3;

    std::thread t2(func2, std::ref(intPromiseT2), std::ref(intPromiseT3));

    // thread T3 is going to wait on a promised int and then provide a promised int to thread Main
    std::promise<int> intPromiseMain;
    std::thread t3(func2, std::ref(intPromiseT3), std::ref(intPromiseMain));

    std::this_thread::sleep_for(myDur);
    std::cout << "MAIN #2 - provide the value for promise #1" << std::endl;
    intPromiseT1.set_value(22);

    std::this_thread::sleep_for(myDur);
    std::cout << "MAIN #2.2 - provide the value for promise #2" << std::endl;
    std::this_thread::sleep_for(myDur);
    intPromiseT2.set_value(1001);
    std::this_thread::sleep_for(myDur);
    std::cout << "MAIN #2.4 - set_value 1001 completed." << std::endl;

    std::future<int> intFutureMain(intPromiseMain.get_future());
    auto t3Promised = intFutureMain.get();
    std::cout << "MAIN #2.3 - intFutureMain.get() from T3. " << t3Promised << std::endl;

    t1.join();
    t2.join();
    t3.join();

    int iArray[100];

    Sync_queue<int> q1;    // notification queue for messages to thread t11
    Sync_queue<int> q2;    // notification queue for messages to thread t12

    std::thread t11(func3, std::ref(q1), 0, 5, iArray);     // start thread t11 with its queue and section of the array
    std::this_thread::sleep_for(myDur);
    std::thread t12(func3, std::ref(q2), 10, 15, iArray);   // start thread t12 with its queue and section of the array
    std::this_thread::sleep_for(myDur);

    // send a series of jobs to our threads by sending notification to each thread's queue.
    for (int i = 0; i < 5; i++) {
        std::cout << "MAIN #11 Loop to do array " << i << std::endl;
        std::this_thread::sleep_for(myDur);  // sleep a moment for I/O to complete
        q1.put(i + 100);
        std::this_thread::sleep_for(myDur);  // sleep a moment for I/O to complete
        q2.put(i + 1000);
        std::this_thread::sleep_for(myDur);  // sleep a moment for I/O to complete
    }

    // close down the job threads so that we can quit.
    q1.put(-1);    // indicate we are done with agreed upon out of range data value
    q2.put(-1);    // indicate we are done with agreed upon out of range data value

    t11.join();
    t12.join();
    return 0;
}

แอปพลิเคชั่นที่เรียบง่ายนี้สร้างผลลัพธ์ต่อไปนี้

MAIN #1 - create our threads.
MAIN #2 - provide the value for promise #1
  func 1 future 22
MAIN #2.2 - provide the value for promise #2
  func2 2 promised 100100 ll was 1001
  func2 3 promised 10010000 ll was 100100
MAIN #2.4 - set_value 1001 completed.
MAIN #2.3 - intFutureMain.get() from T3. 10010000
MAIN #11 Loop to do array 0
  func3 4 start loop base 100 0 to 5
  func3 5 start loop base 1000 10 to 15
MAIN #11 Loop to do array 1
  func3 4 start loop base 101 0 to 5
  func3 5 start loop base 1001 10 to 15
MAIN #11 Loop to do array 2
  func3 4 start loop base 102 0 to 5
  func3 5 start loop base 1002 10 to 15
MAIN #11 Loop to do array 3
  func3 4 start loop base 103 0 to 5
  func3 5 start loop base 1003 10 to 15
MAIN #11 Loop to do array 4
  func3 4 start loop base 104 0 to 5
  func3 5 start loop base 1004 10 to 15

1

สัญญาคือปลายอีกด้านของเส้นลวด

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

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

promiseนี่คือ มันเป็นที่จับfutureเชื่อมต่อกับคุณ ถ้าfutureเป็นลำโพงและมีget()คุณเริ่มต้นการฟังจนบางเสียงออกมาที่promiseเป็นไมโครโฟน ; แต่ไม่เพียงไมโครโฟนใด ๆ ก็เป็นไมโครโฟนที่เชื่อมต่อกับสายเดียวกับลำโพงที่คุณถือ คุณอาจรู้ว่าใครอยู่ที่ปลายอีกด้าน แต่คุณไม่จำเป็นต้องรู้ - คุณเพียงแค่ให้มันและรอจนกว่าอีกฝ่ายจะพูดอะไรบางอย่าง


0

http://www.cplusplus.com/reference/future/promise/

คำอธิบายประโยคหนึ่งคำ: furture :: get () รอ promse :: set_value () ตลอดไป

void print_int(std::future<int>& fut) {
    int x = fut.get(); // future would wait prom.set_value forever
    std::cout << "value: " << x << '\n';
}

int main()
{
    std::promise<int> prom;                      // create promise

    std::future<int> fut = prom.get_future();    // engagement with future

    std::thread th1(print_int, std::ref(fut));  // send future to new thread

    prom.set_value(10);                         // fulfill promise
                                                 // (synchronizes with getting the future)
    th1.join();
    return 0;
}
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.