#Header
#include "co/co.h"
The API is in the co namespace.
go has been introduced into the global namespace via using co::go; and can be used directly.
#Initialization
Just include co/co.h; no explicit initialization is needed.
You must call flag::parse(argc, argv); at the beginning of main to start the scheduling threads.
#include "co/co.h"
#include "co/flag.h"
#include "co/print.h"
int main(int argc, char** argv) {
flag::parse(argc, argv);
go([] { co::println("coroutine test"); });
co::sleep(100);
return 0;
}
#Getting Scheduler and Coroutine Information
int co::sched_num();
sched_t* co::sched();
coro_t* co::coroutine();
int co::sched_id();
int co::coroutine_id();
sched_num(): number of schedulers, defaults toos::cpunum().sched(): current scheduler; returnsNULLfor non-scheduler threads.coroutine(): current coroutine.sched_id(): current scheduler id; returns-1for non-scheduler threads.coroutine_id(): current coroutine id; returns-1for non-coroutine contexts.
#Starting a Coroutine: go
void co::go(void (*f)());
template<typename F, typename ...A>
void co::go(F&& f, A&& ...args);
- Can be called from any thread; does not block.
- In
go(f, args...), passingargsby reference is safe; iffis a lambda, objects on the coroutine stack must not be captured by reference. - A coroutine may be stolen by another scheduler before it starts; once started, it cannot be stolen.
coost coroutines use a shared stack design; you cannot access the contents of another coroutine’s stack through pointers or references.
Example:
go(f); // void f();
go([]() {co::println("hello");}); // lambda
go(&T::f, (T*)o); // void T::f()
#sleep and timeout
void co::sleep(uint32 ms);
bool co::timeout();
sleep: yields in a coroutine; equivalent totime::sleepin a non-coroutine context;sleep(0)also yields.timeout: checks whether the current coroutine has timed out; used together withadd_timer.
#Low-Level Scheduling Interfaces
#Coroutine Scheduling
void co::yield();
void co::resume(coro_t* c);
yield: suspends the current coroutine; must be called inside a coroutine.resume: wakes up a coroutine;cmust come fromco::coroutine(); can be called from any thread.
#Timers and IO Events
void co::add_timer(uint32 ms);
void co::add_io_event(sock_t fd, ev_t ev);
void co::del_io_event(sock_t fd, ev_t ev);
void co::del_io_event(sock_t fd);
add_timer: adds a timeout to the current coroutine; you need to callyieldmanually; after waking up, usetimeout()to check.add_io_event/del_io_event: register/remove socket read/write events; usually called internally by socket APIs.
#Stack Check
bool co::on_stack(const void* p);
- Checks whether the memory pointed to by
pis on the current coroutine’s stack.
#Synchronization Mechanisms
#co::mutex
struct mutex {
mutex();
~mutex();
mutex(mutex&& c) noexcept;
mutex(const mutex& c);
void operator=(const mutex&) = delete;
void operator=(mutex&&) = delete;
void lock() const;
void unlock() const;
bool try_lock() const;
void* _p;
};
- Mutex; supports coroutines and non-coroutines.
- When blocking in a coroutine, it yields; when blocking in a non-coroutine context, it blocks the thread.
- Copy construction only increases the reference count.
- Whoever locks should unlock; unlocking across threads is not a good design.
#co::mutex_guard
RAII lock; locks on construction, unlocks on destruction; non-copyable and non-movable.
#co::event
struct event {
explicit event(bool manual_reset=false, bool signaled=false);
~event();
event(event&& e) noexcept;
event(const event& e);
void operator=(const event&) = delete;
void operator=(event&&) = delete;
void wait() const;
bool wait(uint32 ms) const;
void notify_one() const;
void notify_all() const;
void reset() const;
void* _p;
};
- Synchronization event; supports coroutines and non-coroutines.
manual_reset=true: requires manualreset().manual_reset=false:notify_one()wakes one and automatically resets.waitblocks until it is woken up or times out; it does not block the scheduling thread and is not subject to spurious wakeups; returnsfalseon timeout.- Copy construction only increases the reference count.
#co::wait_group
struct wait_group {
explicit wait_group(uint32 n);
wait_group();
~wait_group();
wait_group(wait_group&& wg) noexcept;
wait_group(const wait_group& wg);
void operator=(const wait_group&) = delete;
void operator=(wait_group&&) = delete;
void add(uint32 n=1) const;
void done() const;
void wait() const;
void* _p;
};
- Similar to
WaitGroupin golang. - The constructor can specify an initial count.
add(n)increments the count byn;done()decrements the count by 1; when it reaches 0, allwait()calls are woken up.- Copy construction only increases the reference count.
#co::pool
struct pool {
using create_cb_t = void* (*)();
using destroy_cb_t = void (*)(void*);
pool();
~pool();
pool(create_cb_t c, destroy_cb_t d, uint32 cap=(uint32)-1);
pool(pool&& p);
pool(const pool& p);
void operator=(const pool&) = delete;
void operator=(pool&&) = delete;
void* pop() const;
void push(void* e) const;
void* _p;
};
- Used only in coroutines; each scheduler has one pool; elements in the pool are not shared across threads, so no locking is needed.
ccreates elements,ddestroys elements,capis the maximum capacity per pool.pop()takes an element; if the pool is empty, callscto create one.push(e)puts it back; if it exceedscap, callsdto destroy it; ifeis NULL, it is ignored.- Elements in the pool are destroyed at destruction time.
- Copy construction only increases the reference count.
co::pool can be used to manage TCP connections; a large number of coroutines can reuse a small number of connections in the pool.
#co::pool_guard
RAII guard; calls pop on construction and push on destruction; non-copyable and non-movable.
#Examples
#Starting Coroutines
#include "co/co.h"
#include "co/print.h"
#include "co/time.h"
int main(int argc, char** argv) {
flag::parse(argc, argv);
go([] {
co::println("coroutine id: ", co::coroutine_id());
co::sleep(1000);
co::println("after sleep");
});
go([](int a, int b) {
co::println("sum = ", a + b);
}, 1, 2);
time::sleep(2000);
return 0;
}
#yield / resume
co::coro_t* gco = 0;
co::wait_group wg;
void f() {
co::println("coroutine starts: ", co::coroutine_id());
gco = co::coroutine();
co::yield();
co::println("coroutine ends: ", co::coroutine_id());
wg.done();
}
int main(int argc, char** argv) {
flag::parse(argc, argv);
wg.add(1);
go(f);
time::sleep(1000);
if (gco) co::resume(gco);
wg.wait();
return 0;
}
#mutex
co::mutex m;
go([m] {
co::mutex_guard g(m);
co::print("locked in coroutine\n");
co::sleep(100);
});
Because of the shared stack, lambdas must capture by value.
#wait_group
co::wait_group wg(3);
for (int i = 0; i < 3; ++i) {
go([wg, i] {
co::print("task ", i, '\n');
wg.done();
});
}
wg.wait();
#pool
co::pool p(
[]() -> void* { return new co::string("hello"); },
[](void* e) { delete (co::string*)e; }
);
go([p] {
co::pool_guard<co::string> g(p);
co::print("pool string: ", *g, '\n');
});
#Coroutine-Related Flags
| flag | Default | Meaning |
|---|---|---|
co_sched_num |
Number of system CPU cores | Number of schedulers |
co_stack_num |
8 |
Number of stacks per scheduler; must be a power of 2 |
co_stack_size |
1024 * 1024 |
Coroutine stack size |
Command line:
./app -co_sched_num=4 -co_stack_num=16 -co_stack_size=2m
Configuration file:
co_sched_num = 4
co_stack_num = 16
co_stack_size = 2m
#Notes
gocan be called from any thread.- The main thread is not a scheduler thread; keep it simple.
- The default number of schedulers equals
os::cpunum(); it can be adjusted with flags. - A coroutine may be stolen before it starts; once started, it cannot be stolen.
yieldmust be called inside a coroutine.resumeis thread-safe and can wake up a coroutine from another thread.co::sleepis equivalent totime::sleepin a non-coroutine context.co::sleep(0)also yields.- After
add_timer, you need toyield, and usetimeout()to check for timeout. - Shared stack: you cannot access the contents of another coroutine’s stack through pointers or references.
- Capturing coroutine-stack
mutex,event,wait_group, etc. in lambdas requires capturing by value. - Copy construction of
mutex/event/wait_group/poolonly increases the reference count. poolis used only in coroutines.- No explicit shutdown of schedulers is needed.