Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
josuttis_nm_c20_the_complete_guide.pdf
Скачиваний:
44
Добавлен:
27.03.2023
Размер:
5.85 Mб
Скачать

542

Chapter 15: Coroutines in Detail

15.9Concurrent Use of Coroutines

In principle, passing operands to co_await can execute any code. We can jump to a very different context or perform some operation and continue with the result of that operation as soon as we have it. Even when the other operation runs in a different thread, no synchronization mechanism is necessary.

This section provides a basic example to demonstrate this technique. Part of it is a simple thread pool to deal with coroutines, which uses std::jthread and a couple of new concurrency features of C++20.

15.9.1 co_await Coroutines

Assume we have the following coroutines calling each other:

coro/coroasync.hpp

 

 

 

 

 

 

#include "coropool.hpp"

 

 

 

 

 

 

 

#include <iostream>

 

 

 

 

 

 

#include <syncstream>

// for std::osyncstream

 

 

 

inline auto syncOut(std::ostream& strm = std::cout) {

 

 

return std::osyncstream{strm};

 

 

 

 

}

 

 

 

 

 

 

CoroPoolTask print(std::string id, std::string msg)

 

 

{

 

 

 

 

 

 

syncOut() << "

>

" << id <<

" print: " << msg

 

 

<< " on thread: " << std::this_thread::get_id() << std::endl;

 

 

co_return; // make it a coroutine

 

 

 

 

}

 

 

 

 

 

 

CoroPoolTask runAsync(std::string id)

 

 

 

{

 

 

 

 

 

 

syncOut() << "=====

" << id << " start

"

 

 

<< "

on

thread: " << std::this_thread::get_id() << std::endl;

 

 

co_await print(id +

"a", "start");

 

 

 

syncOut() << "=====

" << id << " resume

"

 

 

<< "

on

thread " << std::this_thread::get_id() << std::endl;

 

 

co_await print(id +

"b", "end

");

 

 

 

syncOut() << "=====

" << id << " resume

"

 

 

<< "

on

thread " << std::this_thread::get_id() << std::endl;

 

 

} syncOut() << "=====

" << id << " done" << std::endl;

 

 

 

 

 

15.9 Concurrent Use of Coroutines

543

Both coroutines use CoroPoolTask, a coroutine interface for tasks running in a thread pool. The implementation of the interface and the pool will be discussed later.

The important part is that the coroutine runAsync() uses co_await to call another coroutine print():

CoroPoolTask runAsync(std::string id)

{

...

co_await print( ... );

...

}

As we will see, this has the effect that the coroutine print() will be scheduled in a thread pool to run in different threads. In addition, co_await blocks until print() is done.

Note that print() needs a co_return to ensure that it is treated as a coroutine by the compiler. Without this, we would not have any co_ keyword at all and the compiler (assuming this is an ordinary function) would complain that we have a return type but no return statement.

Note also that we use the helper syncOut(), which yields a std::osyncstream, to ensure that the concurrent output from different threads is synchronized line by line.

Assume we call the coroutine runAsync() as follows: coro/coroasync1.cpp

#include "coroasync.hpp" #include <iostream>

int main()

{

// init pool of coroutine threads:

syncOut() << "**** main() on thread " << std::this_thread::get_id() << std::endl;

CoroPool pool{4};

//start main coroutine and run it in coroutine pool: syncOut() << "runTask(runAsync(1))" << std::endl; CoroPoolTask t1 = runAsync("1"); pool.runTask(std::move(t1));

//wait until all coroutines are done:

syncOut() << "\n**** waitUntilNoCoros()" << std::endl; pool.waitUntilNoCoros();

} syncOut() << "\n**** main() done" << std::endl;

544

Chapter 15: Coroutines in Detail

Here, we first create a thread pool of the type CoroPool for all coroutines using the interface CoroPoolTask:

CoroPool pool{4};

Then, we call the coroutine, which starts the coroutine lazily and returns the interface, and hand the interface over to the pool to take control of the coroutine:

CoroPoolTask t1 = runAsync("1"); pool.runTask(std::move(t1));

We use (and have to use) move semantics for the coroutine interface, because runTask() requires to pass an rvalue to be able to take over the ownership of the coroutine (after the call t1 no longer has it).

We could also do that in one statement:

pool.runTask(runAsync("1"));

Before we end the program. the pool blocks until all scheduled coroutines have been processed:

pool.waitUntilNoCoros();

When we run this program, we get something like the following output (the thread IDs vary):

****main() on thread 0x80000008 runTask(runAsync(1))

****waitUntilNoCoros()

=====

1

start

on thread: 0x8002cd90

>

1a print: start

on thread: 0x8002ce68

=====

1

resume

on thread 0x8002ce68

>

1b print: end

on thread: 0x8004d090

=====

1

resume

on thread 0x8004d090

=====

1

done

 

**** main() done

As you can see, different threads are used:

The coroutine runAsync() is started on a different thread to main()

The coroutine print() called from there is started on a third thread

The second call of coroutine print() called from there is started on a fourth thread

However, runAsync() changes threads with each call of print(). By using co_await, these calls suspend runAsync() and call print() on a different thread (as we will see, the suspension schedules the called coroutine in the pool). At the end of print(), the calling coroutine runAsync() resumes on the same thread in which print() was running.

As a variation, we could also start and schedule coroutine runAsync() four times: coro/coroasync2.cpp

#include "coroasync.hpp" #include <iostream>

int main()

{

15.9 Concurrent Use of Coroutines

545

 

 

 

// init pool of coroutine threads:

 

 

 

 

 

 

 

 

syncOut() << "**** main() on thread " << std::this_thread::get_id()

 

 

 

 

<< std::endl;

 

 

 

 

CoroPool pool{4};

 

 

 

 

// start multiple coroutines and run them in coroutine pool:

 

 

 

 

for (int i = 1; i <= 4; ++i) {

 

 

 

 

syncOut() << "runTask(runAsync(" << i << "))" << std::endl;

 

 

 

 

pool.runTask(runAsync(std::to_string(i)));

 

 

 

 

}

 

 

 

 

// wait until all coroutines are done:

 

 

 

 

syncOut() << "\n**** waitUntilNoCoros()" << std::endl;

 

 

 

 

pool.waitUntilNoCoros();

 

 

 

 

syncOut() << "\n**** main() done" << std::endl;

 

 

 

 

}

 

 

 

This program might have the following output (using different thread IDs due to a different platform):

 

 

 

 

****main() on thread 17308 runTask(runAsync(1)) runTask(runAsync(2)) runTask(runAsync(3)) runTask(runAsync(4))

****waitUntilNoCoros()

=====

1

start

on thread: 18016

=====

2

start

on thread: 9004

=====

3

start

on thread: 17008

=====

4

start

on thread: 2816

>

2a print: start

on thread: 2816

>

1a print: start

on thread: 17008

=====

1

resume

on thread 17008

>

4a print: start

on thread: 18016

=====

4

resume

on thread 18016

>

3a print: start

on thread: 9004

=====

3

resume

on thread 9004

=====

2

resume

on thread 2816

>

4b print: end

on thread: 9004

>

1b print: end

on thread: 2816

=====

1

resume

on thread 2816

=====

1

done

 

=====

4

resume

on thread 9004

=====

4

done