Coroutines ใน C ++ 20 คืออะไร?


104

Coroutines คืออะไรใน เหรอ?

แตกต่างจาก "Parallelism2" หรือ / และ "Concurrency2" อย่างไร (ดูภาพด้านล่าง)

ภาพด้านล่างมาจาก ISOCPP

https://isocpp.org/files/img/wg21-timeline-2017-03.png

ใส่คำอธิบายภาพที่นี่


3
เพื่อที่จะตอบ "ในสิ่งที่เป็นแนวคิดของcoroutinesแตกต่างจากความเท่าเทียมและเห็นพ้องด้วย ?" - th.wikipedia.org/wiki/Coroutine
Ben Voigt

ที่เกี่ยวข้อง: stackoverflow.com/q/35121078/103167
Ben Voigt

3
บทนำสู่โครูทีนที่ดีและง่ายต่อการติดตามคือการนำเสนอของ James McNellis“ Introduction to C ++ Coroutines” (Cppcon2016)
philsumuru

2
ในที่สุดมันก็ยังจะดีที่จะครอบคลุม "How are coroutinesใน C ++ ที่แตกต่างจากการใช้งานภาษาอื่น ๆ ของ coroutines และฟังก์ชั่นกลับมาทำงานต่อ?" (ซึ่งบทความวิกิพีเดียที่เชื่อมโยงข้างต้นเป็นภาษาที่ไม่เชื่อเรื่องพระเจ้าไม่ได้กล่าวถึง)
Ben Voigt

1
มีใครอ่าน "การกักกันใน C ++ 20" นี้บ้าง
Sahib Yar

คำตอบ:


201

ในระดับนามธรรม Coroutines แยกแนวคิดในการมีสถานะการดำเนินการออกจากแนวคิดที่จะมีเธรดการดำเนินการ

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

เธรดมี "เธรดการดำเนินการ" หลายรายการและสถานะการดำเนินการหลายรายการ คุณมีโปรแกรมมากกว่าหนึ่งโปรแกรมและมีเธรดการดำเนินการมากกว่าหนึ่งชุด

Coroutines มีสถานะการดำเนินการหลายสถานะ แต่ไม่มีเธรดการดำเนินการ คุณมีโปรแกรมและโปรแกรมมีสถานะ แต่ไม่มีเธรดการดำเนินการ


ตัวอย่างที่ง่ายที่สุดของโครูทีนคือเครื่องกำเนิดไฟฟ้าหรือตัวนับจากภาษาอื่น

ในรหัสหลอก:

function Generator() {
  for (i = 0 to 100)
    produce i
}

เรียกว่าและครั้งแรกที่ถูกเรียกมันว่าผลตอบแทนGenerator 0สถานะของมันเป็นที่จดจำ (สถานะที่แตกต่างกันไปตามการใช้งานโครูทีน) และในครั้งต่อไปที่คุณเรียกมันว่าจะดำเนินต่อจากจุดที่ค้างไว้ ดังนั้นจะคืนค่า 1 ในครั้งต่อไป จากนั้น 2.

ในที่สุดก็มาถึงจุดสิ้นสุดของลูปและหลุดออกจากจุดสิ้นสุดของฟังก์ชัน โครูทีนเสร็จสิ้น (สิ่งที่เกิดขึ้นที่นี่จะแตกต่างกันไปตามภาษาที่เรากำลังพูดถึงใน python จะแสดงข้อยกเว้น)

Coroutines นำความสามารถนี้มาสู่ C ++

โครูทีนมีสองชนิด ซ้อนกันและไม่ซ้อนกัน

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

โครูทีนแบบเรียงซ้อนจะจัดเก็บสแต็กทั้งหมด (เช่นเธรด)

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

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

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


มีอะไรมากกว่าเครื่องกำเนิดไฟฟ้าธรรมดา คุณสามารถรอโครูทีนในโครูทีนซึ่งช่วยให้คุณสามารถเขียนโครูทีนได้อย่างมีประโยชน์

โครูทีนเช่น if ลูปและการเรียกใช้ฟังก์ชันเป็น "goto ที่มีโครงสร้าง" อีกประเภทหนึ่งที่ช่วยให้คุณสามารถแสดงรูปแบบที่เป็นประโยชน์บางอย่าง (เช่นเครื่องสเตต) ได้อย่างเป็นธรรมชาติมากขึ้น


การนำ Coroutines ไปใช้งานเฉพาะใน C ++ นั้นน่าสนใจเล็กน้อย

ในระดับพื้นฐานที่สุดจะเพิ่มคำหลักสองสามคำลงใน C ++: co_return co_await co_yieldพร้อมกับไลบรารีบางประเภทที่ใช้งานได้

ฟังก์ชันจะกลายเป็นโครูทีนโดยมีหนึ่งในฟังก์ชันนั้นอยู่ในตัว ดังนั้นจากการประกาศของพวกเขาพวกเขาจึงแยกไม่ออกจากฟังก์ชัน

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

โครูทีนที่ง่ายที่สุดคือเครื่องกำเนิดไฟฟ้า:

generator<int> get_integers( int start=0, int step=1 ) {
  for (int current=start; true; current+= step)
    co_yield current;
}

co_yieldระงับการดำเนินการฟังก์ชั่นร้านค้าที่ของรัฐในgenerator<int>แล้วส่งกลับค่าของผ่านcurrentgenerator<int>

คุณสามารถวนซ้ำจำนวนเต็มที่ส่งคืนได้

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

std::future<std::expected<std::string>> load_data( std::string resource )
{
  auto handle = co_await open_resouce(resource);
  while( auto line = co_await read_line(handle)) {
    if (std::optional<std::string> r = parse_data_from_line( line ))
       co_return *r;
  }
  co_return std::unexpected( resource_lacks_data(resource) );
}

load_dataเป็นโครูทีนที่สร้างstd::futureเมื่อทรัพยากรที่ระบุชื่อถูกเปิดและเราจัดการเพื่อแยกวิเคราะห์ไปยังจุดที่เราพบข้อมูลที่ร้องขอ

open_resourceและread_lineอาจเป็น async coroutines ที่เปิดไฟล์และอ่านบรรทัดจากไฟล์ co_awaitเชื่อมต่อพักและรัฐพร้อมload_dataที่จะมีความคืบหน้าของพวกเขา

โครูทีน C ++ มีความยืดหยุ่นมากกว่านี้เนื่องจากมีการใช้งานเป็นชุดคุณสมบัติภาษาขั้นต่ำที่ด้านบนของประเภทพื้นที่ผู้ใช้ ประเภทพื้นที่ผู้ใช้กำหนดความหมายco_return co_awaitและความco_yield หมายได้อย่างมีประสิทธิภาพ- ฉันเคยเห็นผู้คนใช้มันเพื่อใช้นิพจน์ทางเลือกแบบ monadic เช่นco_awaitบนตัวเลือกที่ว่างเปล่าจะขับเคลื่อนสถานะว่างให้เป็นตัวเลือกภายนอกโดยอัตโนมัติ:

modified_optional<int> add( modified_optional<int> a, modified_optional<int> b ) {
  return (co_await a) + (co_await b);
}

แทน

std::optional<int> add( std::optional<int> a, std::optional<int> b ) {
  if (!a) return std::nullopt;
  if (!b) return std::nullopt;
  return *a + *b;
}

26
นี่เป็นหนึ่งในคำอธิบายที่ชัดเจนที่สุดว่าโครูทีนคืออะไรที่ฉันเคยอ่าน การเปรียบเทียบและแยกความแตกต่างจาก SIMD และเธรดแบบคลาสสิกเป็นความคิดที่ยอดเยี่ยม
Omnifarious

2
ฉันไม่เข้าใจตัวอย่าง add-optionals std :: ตัวเลือก <int> ไม่ใช่วัตถุที่รอได้
Jive Dadson

1
@mord ใช่มันควรจะคืน 1 องค์ประกอบ อาจต้องขัด ถ้าเราต้องการมากกว่าหนึ่งบรรทัดต้องการโฟลว์การควบคุมที่แตกต่างกัน
Yakk - Adam Nevraumont

1
@lf ;;ขอโทษที่ควรจะเป็น
Yakk - Adam Nevraumont

1
@LF สำหรับฟังก์ชันง่ายๆเช่นนี้อาจไม่มีความแตกต่าง แต่ความแตกต่างที่ฉันเห็นโดยทั่วไปคือโครูทีนจะจดจำจุดเข้า / ออก (การดำเนินการ) ในร่างกายในขณะที่ฟังก์ชันคงที่จะเริ่มการดำเนินการตั้งแต่เริ่มต้นทุกครั้ง ตำแหน่งของข้อมูล "ในเครื่อง" ไม่เกี่ยวข้องฉันเดาว่า
AVP

21

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

การใช้งานโครูทีนแบบทดลองก่อนหน้านี้จาก Microsoft ใช้สแต็กที่คัดลอกมาเพื่อให้คุณสามารถย้อนกลับจากฟังก์ชันที่ซ้อนกันอยู่ลึก ๆ แต่เวอร์ชันนี้ถูกปฏิเสธโดยคณะกรรมการ C ++ คุณสามารถรับการนำไปใช้งานนี้ได้เช่นกับไลบรารี Boosts fiber


1

โครูทีนควรจะเป็นฟังก์ชัน (ใน C ++) ที่สามารถ "รอ" ให้รูทีนอื่น ๆ ดำเนินการให้เสร็จสิ้นและจัดเตรียมสิ่งที่จำเป็นสำหรับกิจวัตรที่ถูกระงับหยุดชั่วคราวรอและดำเนินการต่อไป คุณลักษณะที่น่าสนใจที่สุดสำหรับชาว C ++ คือโครูทีนไม่ควรใช้พื้นที่สแต็ก ... C # สามารถทำสิ่งนี้ได้แล้วด้วยการรอและให้ผล แต่ C ++ อาจต้องสร้างขึ้นใหม่เพื่อให้ได้มา

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

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


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