may Rust Stackful Coroutine Library

repository·master·Indexed 25 days ago

https://github.com/xudong-huang/may

A high-performance Rust library for stackful coroutines designed for massive concurrency. It provides an event loop, efficient I/O, and synchronization primitives for multi-core systems, featuring a `go!` macro for spawning coroutines and Coroutine Local Storage (CLS) via the `coroutine_local!` macro. The library includes the `may_queue` crate for inter-task communication with MPSC, SPMC, and SPSC queue implementations.

Tokens
12.7K
Snippets
34
Records
85
Agent score
81%

What's inside may

  1. Overview of I/O event loop implementations

    master

    The may project provides I/O event loop implementations designed for high-performance asynchronous operations. These implementations are cross-platform and currently support the following operating systems:

    • Linux
    • macOS
    • Windows
  2. Manage CPU-bound tasks with manual yielding

    master

    Because the may scheduler runs coroutines cooperatively, a coroutine that does not yield will occupy the running thread and prevent other coroutines from being scheduled on that thread. While may APIs automatically yield when necessary, long-running CPU-bound tasks do not.

    Solution: Manually call may::coroutine::yield_now() at appropriate points during long-running CPU-bound tasks to allow the scheduler to run other coroutines.

  3. Get the coroutine stack usage

    master

    To measure the actual memory footprint of a coroutine's stack, set the stack size to an odd number.

    When an odd stack size is provided, may initializes the entire stack with a special pattern. After the coroutine finishes execution, may will print the actual used size to the console. This is useful for tuning stack sizes to find the optimal balance between safety and memory usage.

    extern crate may;
    use std::io::{self, Read};
    
    fn main() {
        go!(
            may::coroutine::Builder::new()
                .name("test".to_owned())
                .stack_size(0x1000 - 1),
            || {
                println!("hello may");
            }
        ).unwrap();
    
        println!("Press any key to continue...");
        let _ = io::stdin().read(&mut [0u8]).unwrap();
    }
  4. Important caveats and restrictions when using May coroutines

    master

    When writing programs with May, follow these four rules to ensure performance and stability:

    1. Avoid thread-blocking APIs: Calling standard library functions that block the entire OS thread will degrade the performance of the entire scheduler.
    2. Handle Thread Local Storage (TLS) carefully: Accessing TLS in a coroutine can be unsafe if scheduling occurs between setting and using the TLS. It is considered unsafe to use the following pattern if the code is sensitive to state:
      set_tls();
      // Any coroutine API that causes scheduling (e.g., coroutine::yield_now())
      use_tls();
      It is safe if there is no coroutine scheduling between the set and use calls, or if your code is not sensitive to the previous state.
    3. Avoid long-running CPU-bound tasks: Long tasks can impact fairness unless you do not care about task scheduling fairness.
    4. Prevent stack overflow: Each coroutine has a fixed stack size with a guard page. Exceeding this stack will trigger a segmentation fault. Ensure your coroutine stack size is appropriately tuned for your application's needs.
  5. Use Coroutine Local Storage (CLS) instead of Thread Local Storage (TLS)

    master

    In a coroutine-based environment, using standard Thread Local Storage (TLS) via the thread_local! macro is unsafe for storing state that should be specific to a coroutine. Because a coroutine can be rescheduled onto different threads during its lifecycle, accessing TLS can lead to inconsistent or outdated values.

    To ensure each coroutine has its own unique local storage, use Coroutine Local Storage (CLS) provided by the coroutine_local! macro.

    Key behaviors of CLS:

    • Coroutine Context: Guarantees unique storage per coroutine.
    • Thread Context: If you access a CLS variable from a standard thread context, it safely falls back to its TLS storage based on the provided key.

    To migrate from TLS to CLS in Rust, replace the thread_local! macro with coroutine_local!.

    fn coroutine_local_many() {
        use std::sync::atomic::{AtomicUsize, Ordering};
        coroutine_local!(static FOO: AtomicUsize = AtomicUsize::new(0));
    
        coroutine::scope(|scope| {
            for i in 0..10 {
                go!(scope, move || {
                    FOO.with(|f| {
                        assert_eq!(f.load(Ordering::Relaxed), 0);
                        f.store(i, Ordering::Relaxed);
                        assert_eq!(f.load(Ordering::Relaxed), i);
                    });
                });
            }
        });
        // called in thread
        FOO.with(|f| {
            assert_eq!(f.load(Ordering::Relaxed), 0);
        });
    }
  6. Overview of the May coroutine library

    master

    May is a high-performance library for programming stackful coroutines in Rust, designed to enable the development of massive concurrent programs. It is conceptually similar to Goroutines in Go.

    Key features include:

    • Stackful Coroutines: Implementation based on generators.
    • Multi-core Support: Scheduling on a configurable number of threads.
    • Coroutine Local Storage (CLS): Support for local storage specific to a coroutine.
    • Efficient I/O and Timers: Optimized asynchronous network I/O and timer management.
    • Synchronization Primitives: Includes semaphores, MPMC channels, and more.
    • Robustness: Support for coroutine cancellation, graceful panic handling (panics do not affect other coroutines), and scoped coroutine creation.
    • Safety and Compatibility: APIs are compatible with standard library semantics and are safe to call in multi-threaded contexts. It supports stable, beta, and nightly Rust channels on x86_64 GNU/Linux, Windows, and macOS.
  7. Use Coroutine Local Storage (CLS)

    master
    May supports a coroutine-specific version of local storage. You can use LocalKey to manage data that is local to the current coroutine, similar to how thread_local! works for threads, but scoped to the coroutine lifecycle.
  8. Use Semphore for synchronization

    master

    A Semphore is a synchronization primitive that allows threads and coroutines to synchronize their actions using an integer value.

    • post(): Increments the semaphore value. If there are waiting threads or coroutines, one is woken up.
    • wait(): Decrements the semaphore value. If the value is zero, the caller blocks until a post() is called.
    • try_wait(): Attempts to decrement the value without blocking. Returns true if successful, false if the value is zero.
    • wait_timeout(dur): Similar to wait(), but returns false if the specified Duration elapses before the semaphore is acquired.
    • get_value(): Returns the current semaphore value (returns 0 if the value is negative, indicating waiters).
    use std::sync::Arc;
    use may::coroutine;
    use may::sync::Semphore;
    
    let sem = Arc::new(Semphore::new(0));
    let sem2 = sem.clone();
    
    // spawn a coroutine, and then wait for it to start
    unsafe {
        coroutine::spawn(move || {
            sem2.post();
        });
    }
    
    // wait for the coroutine to start up
    sem.wait();
  9. Use SyncFlag for thread and coroutine synchronization

    master

    A SyncFlag is a boolean synchronization primitive that acts like a barrier. It allows multiple threads or coroutines to synchronize their actions.

    • When the flag is false, any thread or coroutine calling wait() or wait_timeout() will block until the flag is set to true.
    • When the flag is true, any thread or coroutine calling wait() or wait_timeout() will return immediately.
    • Once the flag is set to true via fire(), it remains true and will never become false again.

    This primitive is useful for ensuring that a specific event has occurred (like a coroutine starting up) before proceeding with execution.

    use std::sync::Arc;
    use may::coroutine;
    use may::sync::SyncFlag;
    
    let flag = Arc::new(SyncFlag::new());
    let flag2 = flag.clone();
    
    // spawn a coroutine, and then wait for it to start
    unsafe {
        coroutine::spawn(move || {
            flag2.fire();
            flag2.wait();
        });
    }
    
    // wait for the coroutine to start up
    flag.wait();
  10. Use the MPSC Queue for multi-producer single-consumer communication

    master
    The Queue<T> in may_queue is a high-performance multi-producer single-consumer (MPSC) queue. It allows multiple threads to safely push data into the queue simultaneously, but it must be guaranteed that only one thread (the "popper") is calling pop() at any given time. The queue is Send and Sync as long as T is Send.
  11. Use WaitGroup for thread synchronization

    master

    A WaitGroup enables threads to synchronize the beginning or end of some computation. It allows a main thread (or any thread) to block until all registered tasks have completed.

    Key Differences from std::sync::Barrier:

    • Registration: Unlike a Barrier which requires a fixed number of threads at construction, a WaitGroup is registered by cloning it. Each clone represents a new participant.
    • Reusability: A Barrier can be reused after synchronization; a WaitGroup synchronizes threads only once.
    • Blocking Behavior: In a Barrier, all threads wait for each other. With a WaitGroup, individual threads can choose to either wait for others using .wait() or continue without blocking by simply dropping their reference.

    Basic Usage Pattern

    1. Create a WaitGroup using WaitGroup::new().
    2. Clone the WaitGroup for every spawned thread/task.
    3. Inside the task, perform work and then drop the clone (or let it go out of scope).
    4. Call .wait() on the original WaitGroup instance to block until all clones are dropped.
    use may::sync::WaitGroup;
    use std::thread;
    
    // Create a new wait group.
    let wg = WaitGroup::new();
    
    for _ in 0..4 {
        // Create another reference to the wait group.
        let wg = wg.clone();
    
        thread::spawn(move || {
            // Do some work.
    
            // Drop the reference to the wait group.
            drop(wg);
        });
    }
    
    // Block until all threads have finished their work.
    wg.wait();
  12. Change the default coroutine stack size

    master

    Because may does not support automatic stack increasing, you must ensure your coroutines have sufficient stack space. You can configure a global default stack size during the initialization stage. When a coroutine is created without an explicit stack size, it will use this configured value.

    Note: The unit for stack size is word. On a 64-bit system, 4k words equals 32k bytes.

    may::config().set_stack_size(0x400);
    // this coroutine would use 8K bytes stack
    go!(...);