Coroutines trong C ++ 20 là gì?


104

Coroutines trong là gì ?

Nó khác với "Parallelism2" hoặc / và "Concurrency2" ở điểm nào (xem hình ảnh bên dưới)?

Hình ảnh dưới đây là của ISOCPP.

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

nhập mô tả hình ảnh ở đây


3
Để trả lời "Khái niệm về coroutines khác với song songđồng quy ở điểm nào?" - en.wikipedia.org/wiki/Coroutine
Ben Voigt


3
Một rất tốt và dễ tiếp giới thiệu để coroutine là trình bày James McNellis của “Giới thiệu về C ++ coroutines" (Cppcon2016).
philsumuru

2
Cuối cùng, cũng sẽ rất tốt nếu bạn đề cập đến " coroutines trong C ++ khác với cách triển khai coroutines và các hàm có thể nối lại của các ngôn ngữ khác như thế nào?" (mà bài viết wikipedia trên liên kết, là ngôn ngữ thuyết bất khả tri, không địa chỉ)
Ben Voigt

1
ai khác đọc "vùng cách ly trong C ++ 20" này?
Sahib Yar

Câu trả lời:


199

Ở cấp độ trừu tượng, Coroutines tách ý tưởng có một trạng thái thực thi ra khỏi ý tưởng có một chuỗi thực thi.

SIMD (một lệnh nhiều dữ liệu) có nhiều "luồng thực thi" nhưng chỉ có một trạng thái thực thi (nó chỉ hoạt động trên nhiều dữ liệu). Các thuật toán song song có thể cho là giống như thế này, trong đó bạn có một "chương trình" chạy trên các dữ liệu khác nhau.

Phân luồng có nhiều "luồng thực thi" và nhiều trạng thái thực thi. Bạn có nhiều chương trình và nhiều luồng thực thi.

Coroutines có nhiều trạng thái thực thi, nhưng không sở hữu một chuỗi thực thi. Bạn có một chương trình, và chương trình có trạng thái, nhưng nó không có luồng thực thi.


Ví dụ đơn giản nhất về coroutines là trình tạo hoặc liệt kê từ các ngôn ngữ khác.

Trong mã giả:

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

Các Generatorđược gọi, và lần đầu tiên nó được gọi là nó trả về 0. Trạng thái của nó được ghi nhớ (mức độ thay đổi trạng thái tùy theo việc triển khai các coroutines), và lần sau khi bạn gọi nó, nó sẽ tiếp tục ở vị trí đã dừng. Vì vậy, nó trả về 1 trong lần tiếp theo. Sau đó 2.

Cuối cùng nó đến cuối vòng lặp và rơi ra khỏi phần cuối của hàm; quy trình đăng quang đã kết thúc. (Những gì xảy ra ở đây thay đổi tùy theo ngôn ngữ mà chúng ta đang nói đến; trong python, nó đưa ra một ngoại lệ).

Coroutines mang lại khả năng này cho C ++.

Có hai loại coroutines; xếp chồng lên nhau và không xếp chồng lên nhau.

Một chương trình điều chỉnh không ngăn xếp chỉ lưu trữ các biến cục bộ ở trạng thái và vị trí thực thi của nó.

Một chương trình điều chỉnh nhiều ngăn xếp lưu trữ toàn bộ một ngăn xếp (giống như một chuỗi).

Các coroutines không chồng chất có thể có trọng lượng cực kỳ nhẹ. Đề xuất cuối cùng tôi đọc về cơ bản liên quan đến việc viết lại hàm của bạn thành một thứ giống như lambda; tất cả các biến cục bộ đều chuyển sang trạng thái của một đối tượng và các nhãn được sử dụng để chuyển đến / từ vị trí mà coroutine "tạo ra" các kết quả trung gian.

Quá trình tạo ra một giá trị được gọi là "sản lượng", vì các coroutines giống như đa luồng hợp tác; bạn đang nhường điểm thực thi lại cho người gọi.

Boost có một triển khai các coroutines xếp chồng lên nhau; nó cho phép bạn gọi một hàm để nhường cho bạn. Các coroutines xếp chồng lên nhau mạnh hơn nhưng cũng đắt hơn.


Có nhiều điều để điều tra hơn một máy phát điện đơn giản. Bạn có thể chờ một quy trình đăng ký trong một quy trình đăng ký, điều này cho phép bạn soạn các quy trình đăng ký một cách hữu ích.

Coroutines, như if, vòng lặp và lệnh gọi hàm, là một loại "goto có cấu trúc" khác cho phép bạn diễn đạt các mẫu hữu ích nhất định (như máy trạng thái) theo cách tự nhiên hơn.


Việc triển khai cụ thể của Coroutines trong C ++ có một chút thú vị.

Ở cấp độ cơ bản nhất, nó thêm một số từ khóa vào C ++:, co_return co_await co_yieldcùng với một số kiểu thư viện hoạt động với chúng.

Một hàm trở thành một quy trình bằng cách có một trong những hàm đó trong cơ thể của nó. Vì vậy, từ khai báo của họ, họ không thể phân biệt được với các hàm.

Khi một trong ba từ khóa đó được sử dụng trong thân hàm, một số kiểm tra bắt buộc tiêu chuẩn về kiểu trả về và các đối số sẽ xảy ra và hàm được chuyển thành một chương trình đăng quang. Việc kiểm tra này cho trình biên dịch biết nơi lưu trữ trạng thái chức năng khi chức năng bị tạm dừng.

Quy trình đăng quang đơn giản nhất là một trình tạo:

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

co_yieldtạm dừng việc thực thi các hàm, lưu trữ trạng thái đó trong generator<int>, sau đó trả về giá trị của currentqua generator<int>.

Bạn có thể lặp lại các số nguyên được trả về.

co_awaittrong khi đó cho phép bạn ghép một quy trình điều tra này vào một quy trình khác. Nếu bạn đang tham gia một chương trình đăng quang và bạn cần kết quả của một việc có thể chờ đợi được (thường là một quy trình đăng quang) trước khi tiến hành, bạn co_awaitphải làm như vậy. Nếu họ đã sẵn sàng, bạn tiến hành ngay lập tức; nếu không, bạn tạm ngưng cho đến khi sẵn sàng.

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_datalà một chương trình điều tra tạo ra std::futurekhi tài nguyên được đặt tên được mở và chúng tôi quản lý để phân tích cú pháp đến điểm mà chúng tôi tìm thấy dữ liệu được yêu cầu.

open_resourceread_lines có thể là các coroutines không đồng bộ để mở một tệp và đọc các dòng từ nó. Kết co_awaitnối trạng thái tạm ngừng và sẵn sàng load_datavới tiến trình của chúng.

C ++ coroutines linh hoạt hơn nhiều so với điều này, vì chúng được triển khai dưới dạng một tập hợp tối thiểu các tính năng ngôn ngữ trên các loại không gian người dùng. Các kiểu không gian người dùng xác định hiệu quả điều gì co_return co_awaitco_yield ý nghĩa - Tôi đã thấy mọi người sử dụng nó để triển khai các biểu thức tùy chọn đơn nguyên sao cho một co_awaittrên tùy chọn trống tự động đề xuất trạng thái trống cho tùy chọn bên ngoài:

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

thay vì

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
Đây là một trong những lời giải thích rõ ràng nhất về những gì mà tôi đã từng đọc. So sánh chúng và phân biệt chúng với SIMD và các luồng cổ điển là một ý tưởng tuyệt vời.
Omnifarious

2
Tôi không hiểu ví dụ về phần bổ sung tùy chọn. std :: option <int> không phải là một đối tượng có thể chờ được.
Jive Dadson

1
@mord vâng nó phải trả về 1 phần tử. Có thể cần đánh bóng; nếu chúng ta muốn nhiều hơn một dòng cần có một luồng điều khiển khác.
Yakk - Adam Nevraumont

1
@lf xin lỗi, đáng lẽ phải như vậy ;;.
Yakk - Adam Nevraumont

1
@LF cho chức năng đơn giản như vậy có thể không có sự khác biệt. Nhưng sự khác biệt mà tôi thấy nói chung là một coroutine ghi nhớ điểm vào / ra (thực thi) trong phần thân của nó, trong khi một hàm static bắt đầu thực thi từ đầu mỗi lần. Tôi đoán vị trí của dữ liệu "cục bộ" không liên quan.
avp

21

Một chương trình đăng quang giống như một hàm C có nhiều câu lệnh trả về và khi được gọi lần thứ hai, lệnh này không bắt đầu thực hiện ở đầu hàm mà ở lệnh đầu tiên sau lần trả về được thực thi trước đó. Vị trí thực thi này được lưu cùng với tất cả các biến tự động sẽ nằm trên ngăn xếp trong các hàm không phải hàm đăng ký.

Việc triển khai quy trình thử nghiệm trước đây của Microsoft đã sử dụng các ngăn xếp được sao chép để bạn thậm chí có thể quay lại từ các hàm lồng nhau sâu. Nhưng phiên bản này đã bị từ chối bởi ủy ban C ++. Bạn có thể lấy việc triển khai này chẳng hạn với thư viện sợi quang Boosts.


1

coroutines được cho là (trong C ++) các hàm có thể "đợi" một số quy trình khác hoàn thành và cung cấp bất cứ thứ gì cần thiết cho quy trình bị treo, tạm dừng, chờ đợi tiếp tục. đặc điểm thú vị nhất đối với những người làm C ++ là lý tưởng là coroutines sẽ không chiếm không gian ngăn xếp ... C # đã có thể làm điều gì đó như thế này với sự chờ đợi và lợi nhuận nhưng C ++ có thể phải được xây dựng lại để đưa nó vào.

đồng thời tập trung nhiều vào việc tách các mối quan tâm trong đó mối quan tâm là nhiệm vụ mà chương trình phải hoàn thành. sự tách biệt các mối quan tâm này có thể được thực hiện bằng một số phương tiện ... thường là sự ủy quyền của một số loại. ý tưởng về sự đồng thời là một số quy trình có thể chạy độc lập (tách biệt các mối quan tâm) và một 'người nghe' sẽ hướng bất cứ điều gì được tạo ra bởi các mối quan tâm riêng biệt đó đến bất cứ nơi nào nó được cho là đi đến. điều này phụ thuộc nhiều vào một số loại quản lý không đồng bộ. Có một số phương pháp tiếp cận đồng thời bao gồm lập trình hướng Aspect và các phương pháp khác. C # có toán tử 'ủy quyền' hoạt động khá tốt.

song song nghe có vẻ giống như đồng thời và có thể có liên quan nhưng thực chất là một cấu trúc vật lý liên quan đến nhiều bộ xử lý được sắp xếp theo kiểu song song ít nhiều với phần mềm có thể hướng các phần mã đến các bộ xử lý khác nhau nơi nó sẽ được chạy và kết quả sẽ được nhận lại một cách đồng bộ.


9
Sự tương đồng và tách biệt các mối quan tâm hoàn toàn không liên quan đến nhau. Coroutines không phải để cung cấp thông tin cho thói quen bị đình chỉ, chúng những thói quen có thể phục hồi.
Ben Voigt
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.