{"id":26093096,"url":"https://github.com/foldcat/oasync","last_synced_at":"2026-02-08T20:35:49.484Z","repository":{"id":279945525,"uuid":"939415924","full_name":"foldcat/oasync","owner":"foldcat","description":"lightweight threading for odin","archived":false,"fork":false,"pushed_at":"2026-01-22T01:52:35.000Z","size":392,"stargazers_count":53,"open_issues_count":0,"forks_count":2,"subscribers_count":3,"default_branch":"master","last_synced_at":"2026-01-22T15:34:01.249Z","etag":null,"topics":["async","fiber","multithreading","odin-lang"],"latest_commit_sha":null,"homepage":"","language":"Odin","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"bsd-2-clause","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/foldcat.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2025-02-26T14:04:16.000Z","updated_at":"2026-01-22T01:52:38.000Z","dependencies_parsed_at":"2025-02-28T17:27:13.621Z","dependency_job_id":"9c5d3c39-8a96-4adf-a25f-c0b4ab7c02a4","html_url":"https://github.com/foldcat/oasync","commit_stats":null,"previous_names":["foldcat/oasync"],"tags_count":4,"template":false,"template_full_name":null,"purl":"pkg:github/foldcat/oasync","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/foldcat%2Foasync","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/foldcat%2Foasync/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/foldcat%2Foasync/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/foldcat%2Foasync/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/foldcat","download_url":"https://codeload.github.com/foldcat/oasync/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/foldcat%2Foasync/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29242878,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-08T19:36:48.828Z","status":"ssl_error","status_checked_at":"2026-02-08T19:27:12.336Z","response_time":57,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["async","fiber","multithreading","odin-lang"],"created_at":"2025-03-09T11:22:28.144Z","updated_at":"2026-02-08T20:35:49.438Z","avatar_url":"https://github.com/foldcat.png","language":"Odin","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cpicture\u003e\n  \u003csource media=\"(prefers-color-scheme: dark)\" width=\"50%\" height=\"auto\" srcset=\"https://github.com/user-attachments/assets/5f3989b2-c89e-4961-a485-d05915c3829e\"\u003e\n  \u003cimg alt=\"logo\" width=\"50%\" height=\"auto\" src=\"https://github.com/user-attachments/assets/251c7ad8-25e4-4453-bc4a-bb487205eb4e\"\u003e\n\u003c/picture\u003e\n\n---\n\n![badge](https://img.shields.io/badge/documentation%20taken%20seriously-ff7eb6) ![Static Badge](https://img.shields.io/badge/odin_version-dev--2025--07-blue)\n\nNow officially in beta state!\n\nM:N multithreading for Odin. The end goal is to implement virtual threads that \nautomatically and quickly parallelize tasks across several os threads.\n\nFeel free to report bugs and request features by creating issues!\n\nAlso note that oasync is NOT compatable with `core:sync`. Please use \nthe synchronization primitives provided by oasync instead.\n\n## features\n- quickly and automatically parallelize tasks across a thread pool\n- supports blocking task pool and scheduling tasks to run in the future\n- depends on ONLY the Odin compiler (just like any Odin libraries)\n- 100% API documentation coverage\n- simple and easy to use API\n- small and commented codebase\n\n## walkthrough\nIt is **HEAVILY** recommend to execute `odin doc .` in the \nroot directory of oasync to read the API documentation. The following \nwalkthough does not cover every procedure and their options.\n\nIn the examples below, we will be importing oasync as so: \n```odin \nimport oa \"../oasync\"\n```\n\n### core functionalities\n#### initializing oasync runtime\nTo use oasync, we first have to initialize it. Note that the following examples \nwill all be executed with the following configuration.\n```odin\nmain :: proc() {\n    // create a coordinator struct for oasync to store \n    // its internal state\n    // it should NOT be modified by the user\n    coord: oa.Coordinator\n    oa.init_oa(\n        // coordinator\n        \u0026coord,\n        // what procedure to dispatch when oasync starts\n        init_proc = core,\n        // a rawptr that will be passed into the init_proc\n        init_proc_arg = nil,\n        // amount of worker threads oasync will run\n        // omit this field or set to 0 for oasync to use \n        // os.processor_core_count() as its value\n        max_workers = 4,\n        // how many blocking taskes should be allowed \n        // to execute at the same time\n        // set as 0 for oasync to use max_workers / 2 \n        // as its value\n        max_blocking = 2,\n        // whether to use the main thread as a worker or not, \n        // counts toward max_workers\n        use_main_thread = true,\n    )\n}\n\n// the task to run\ncore :: proc(_: rawptr) {\n    fmt.println(\"test\")\n}\n```\n\n#### logging \nWe provide additional information should a logger be supplied.\n\n```odin \nmain :: proc() {\n    context.logger = log.create_console_logger()\n    defer log.destroy_console_logger(context.logger)\n\n    // initialize oasync here...\n}\n```\n\nBy providing a logger, oasync will log main events. Should `-debug`\ncompiler flag be enabled, oasync will also detect worker starvation: \nA warning will be emitted should a task take more than 40ms \nto complete. It is recommending to split said task into smaller tasks \nor use the blocking feature documented below to dispatch in order \nto prevent hogging the scheduler.\n\n#### running new tasks\nIt is quite simple to spawn new tasks.\n\nNote that the order of task spawning is not guaranteed\nto be the same as the order of `oa.go` calls.\n```odin\nfoo :: proc(_: rawptr) {\n\tfmt.println(\"hi\")\n}\n\ncore :: proc(_: rawptr) {\n\tfmt.println(\"core\")\n\toa.go(foo) \n}\n```\n\nIn fact, it is far more likely for tasks to execute in reverse\ndue a queue algorithm.\n```odin\nfoo :: proc(a: rawptr) {\n\tfmt.print((cast(^int)a)^, \"\")\n}\n\ncore :: proc(_: rawptr) {\n\tfmt.println(\"started\")\n\n\tfor i in 1 ..= 20 {\n\t\toa.go(foo, new_clone(i))\n\t}\n\n}\n// 20 19 18 17 16...\n```\n\n#### passing in arguments\nIt is trival to pass arguments into tasks.\n```odin\nfoo :: proc(a: rawptr) {\n\targ := cast(^string)a\n\tfmt.println(arg^)\n}\n\ncore :: proc(_: rawptr) {\n\t// remember to free it\n\tnextarg := new_clone(\"hi\", context.temp_allocator)\n\toa.go(foo, nextarg)\n}\n```\n\nDue to items allocated on a stack being freed at the end of the scope,\nit is recommended to allocate the items you want to pass into \nthe next procedure on the heap to prevent accessing freed memories.\n\n#### blocking tasks\nSometimes you may want to run blocking tasks that takes a \nlong time to complete, this should be avoided as it hogs \nthe scheduler and leaves one of our threads out of commission.\nOne should use `block` in this situation.\n```odin\nblocking :: proc(_: rawptr) {\n\ttime.sleep(1 * time.Second)\n\tfmt.println(\"done\")\n}\n\ncore :: proc(_: rawptr) {\n\tfmt.println(\"test\")\n\tfor _ in 1 ..= 4 {\n\t\toa.go(blocking, block = true)\n\t}\n}\n```\nWe only allow `max_blocking` amount of blocking task to run \nsimultaneously, allowing non-blocking tasks to execute under load.\n\n#### timed schedule\nIt is possible to delay the execution of a task without needing\n`time.sleep()`, as `time.sleep()` hogs the scheduler.\n```odin\nstuff :: proc(a: rawptr) {\n\tfmt.println(\"done!\", (cast(^int)a)^)\n}\n\ncore :: proc(_: rawptr) {\n\tfmt.println(\"started\")\n\tfor i in 0 ..= 20 {\n\t\tdata := new_clone(i, context.temp_allocator)\n\t\toa.go(stuff, data, delay = 5 * time.Second)\n\t}\n}\n```\nNote that timed tasks will execute *during* or *after* the tick you supplied, \ni.e. tasks are not guaranteed to execute at percisely after 5 seconds.\n\n#### unsafe dispatching\nYou might want to spawn tasks outside of threads managed \nby oasync, we call this unsafe dispatching:\n```odin\ntask :: proc(_: rawptr) {\n\tfmt.println(\"hi\")\n}\n\nmain :: proc() {\n\tcoord: oa.Coordinator\n\t// some arguments has default options, see api docs\n\toa.init_oa(\u0026coord, init_proc = core, use_main_thread = false)\n\toa.go(\u0026coord, task, coord = \u0026coord)\n\t// hog the main thread to prevent exiting immediately\n\ttime.sleep(1 * time.Second)\n}\n```\nBy supplying `go` with a coordinator, it will be capable of \ndispatching tasks outside of threads managed not by oasync.\n\nThis imposes a heavy performance penality and should be \navoided.\n\n#### shutdown\nShutting down oasync can be done by executing the following \nin a task.\n\n```odin\noa.shutdown(graceful = true)\n```\n\nShutdown is `graceful` by default, where the scheduler will wait for \nthe current task to complete before destroying the worker. Should \n`graceful` be false, `thread.terminate()` will be called on worker \nthreads immediately. It is known that non-`graceful` termination may \nresult in memory leak and segmented fault.\n\nEven with non-`graceful` shutdown, should `use_main_thread` be true,\nthe main thread will be terminated gracefully instead of calling \n`thread.terminate`, causing additional wait time for the \nprocedure to yield.\n\n\n### context system\nTo spawn tasks, oasync injects data into `context.user_ptr`. \nThis means that you should NEVER change it. Should you still \nwish to use `context.user_ptr`, the following may be done.\n```odin \ncore :: proc(_: rawptr) {\n\t// cast it into a ref carrier\n\tptr := cast(^oa.Ref_Carrier)context.user_ptr\n\t// ONLY access the user_ptr field \n\t// do NOT access other fields in Ref_Carrier\n\tptr.user_ptr := ...\n}\n```\n\nHowever, please note that the context in a task will not be \ncarried over to another task spawned. See below for a \ndemonstration.\n```odin\ncore :: proc(_: rawptr) {\n\tcontext.user_index = 1\n\toa.go(stuff)\n}\n\nstuff :: proc(_: rawptr) {\n\tfmt.println(context.user_index) // 0\n}\n```\n\n### synchronization primitives\nWe provide oasync native synchronization primitives. These primitives \nwill not hog the scheduler unlike `core:sync`.\n\nEach destructor procedure have special behaviors, thus it \nis recommended to seek API documentations.\n\nNote that you should NEVER use the primitives after calling the\ndestructor procedures, since it may cause segmented fault.\n\nAlso note that the synchronization primitives are acquired at the moment \nof task dispatch. Should you spawn a delayed procedure, the aquiring of the \nprimitive begins immediately instead of beginning after the delay ends.\n\nThe following examples uses `time.sleep()` for convenience sake. Please do \nnot use `time.sleep()` for real world usage unless it is in a blocking task.\n\n#### resources \nResources are equivalent to mutexes, where only one task is allowed to access \neach resource, and said resource will be released upon task completion\nautomatically.\n\n`oa.destroy_resouce()` may be used to delete it.\n```odin\nacquire1 :: proc(_: rawptr) {\n\tfmt.println(\"first acquire\")\n\ttime.sleep(3 * time.Second)\n\tfmt.println(\"first release\")\n}\n\nacquire2 :: proc(_: rawptr) {\n\tfmt.println(\"second acquire\")\n\ttime.sleep(3 * time.Second)\n\tfmt.println(\"second release\")\n}\n\ncore :: proc(_: rawptr) {\n\tfmt.println(\"started\")\n\n\tres := oa.make_resource()\n\toa.go(acquire1, res = res)\n\toa.go(acquire2, res = res)\n}\n\n/*\nstarted\nfirst acquire\nfirst release\nsecond acquire\nsecond release\n*/\n```\n\nThe order of acquire might be different, but it should be impossible for \nanother task to acquire the same resource while it is acquired.\n\nNote that it is possible to acquire / release a resource in the middle of \na resources via a spinlock. This should be avoided, and should also be \nused in a blocking task.\n\n```odin\nres := oa.make_resource()\n\nstuff :: proc(a: rawptr) {\n\toa.res_spinlock_acquire(res)\n\ttime.sleep(1 * time.Second)\n\tfmt.println(\"acquiring task done\")\n\toa.res_spinlock_release(res)\n}\n\n\ncore :: proc(_: rawptr) {\n\tfmt.println(\"started\")\n\toa.go(stuff)\n\toa.go(stuff)\n}\n```\n\n#### backpressure\nBackpressure allows us to rate limit task spawns.\n\nThere are two strategies for backpressure:\n- Lossy: task will be ran in presence of backpressure\n- Loseless: task will not execute until backpressure is alleviated.\n\nUse `oa.destroy_bp()` to free it.\n\n```odin\nfoo :: proc(a: rawptr) {\n    time.sleep(3 * time.Second)\n    fmt.println((cast(^int)a)^)\n    free(a)\n}\n\ncore :: proc(_: rawptr) {\n    // allow only 3 tasks to run at the same time\n    bp := oa.make_bp(3, .Lossy)\n    for i in 1 ..= 5 {\n        inp := new_clone(i)\n        oa.go(foo, inp, bp = bp)\n    }\n}\n```\n\n#### count down latch \nCount down latches are one shot concurrency primitives that \nblocks any tasks waiting on it until `goal` tasks are waiting.\n\nUse `oa.destroy_cdl()` to free it.\n\n```odin\nstuff :: proc(a: rawptr) {\n  fmt.println(\"done!\")\n}\n\ncore :: proc(_: rawptr) {\n  fmt.println(\"started\")\n  cdl := oa.make_cdl(2)\n\n  oa.go(stuff, cdl = cdl)\n  time.sleep(4 * time.Second)\n  oa.go(stuff, cdl = cdl)\n  time.sleep(6 * time.Second)\n  // further acquires are allowed to execute immediately\n  oa.go(stuff, cdl = cdl)\n}\n```\n\n#### cyclic barrier\nCyclic barriers are re-usable synchronization primitives \nthat allows a set amount of tasks to wait until they've all reached the same point.\n\nUse `oa.destroy_cb()` to free it.\n\n```odin\nstuff :: proc(a: rawptr) {\n    fmt.println(\"done!\")\n}\n\ncore :: proc(_: rawptr) {\n    fmt.println(\"started\")\n    cb := oa.make_cb(2)\n\n    for i in 1 ..= 2 {\n        oa.go(stuff, cb = cb)\n        time.sleep(1 * time.Second)\n        oa.go(stuff, cb = cb)\n        time.sleep(1 * time.Second)\n    }\n}\n\n/* \n*nothing for 1 second*\ndone!\ndone!\n*nothing for 2 seconds*\ndone!\ndone!\n*/\n\n```\n\n#### semaphore \nSemaphore is internally a counter. When a task acquires the \nsemaphore, the counter incremenets. When a task releases the \nsemaphore, the counter decrements. Should the counter's value \nbe `max`, the task attempting to acquire will block until \nthe counter decrements.\n\nUse `oa.destroy_sem()` to free it.\n\n```odin\nacquire :: proc(a: rawptr) {\n\ttime.sleep(3 * time.Second)\n\tfmt.println((cast(^int)a)^)\n}\n\ncore :: proc(_: rawptr) {\n\tfmt.println(\"started\")\n\n\tres := oa.make_sem(3) // max amount of acquire before blocking\n\tfor i in 1 ..= 10 {\n\t\ta := new_clone(i)\n\t\toa.go(acquire, a, sem = res)\n\t}\n}\n```\n\n#### channels\nWe offer many to one channels.\n\nIt is known that the order of elements placed \ninto the channel may not be sequencially consistant.\n```odin\nconsumer :: proc(a: rawptr) {\n\tinput := (cast(^int)a)^\n\tfmt.println(input) \n}\n\ncore :: proc(_: rawptr) {\n\tchan := oa.make_chan(consumer)\n\toa.c_put(chan, 1)\n\toa.c_put(chan, 2)\n\toa.c_put(chan, 3)\n}\n```\n\nIn fact, `oa.c_put` is completely non-blocking and asynchronous.\n\nIt is possible to make buffered sliding channels. Buffered \nsliding channels may only hold `capacity` amount of data.\nWhen capacity is full, buffered sliding channels drops the \nlast item to make room for new items.\n```odin\nconsumer :: proc(a: rawptr) {\n\tinput := (cast(^int)a)^\n\ttime.sleep(1 * time.Second)\n\tfmt.println(input)\n}\n\ncore :: proc(_: rawptr) {\n\tchan := oa.make_chan(consumer, capacity = 2)\n\tfor i in 1 ..= 10 {\n\t\toa.c_put(chan, i)\n\t}\n}\n```\n\nIn order to shutdown the channel, `oa.c_stop()` may be used.\n\n#### task chaining \nIt is possible to make a sequencial task chain acquire one or \nmore primitives, releasing only after every single task completes.\n\n```odin\nstuff :: proc(a: rawptr) -\u003e rawptr {\n\ttime.sleep(1 * time.second)\n\tfmt.println(\"chained resource acquiring task done\")\n\treturn nil\n}\n\nstuff2 :: proc(a: rawptr) {\n\tfmt.println(\"single resource acquiring task done\")\n}\n\ncore :: proc(_: rawptr) {\n\tfmt.println(\"started\")\n\tres := oa.make_resource()\n\toa.go(stuff, stuff, stuff, res = res)\n\toa.go(stuff2, res = res)\n}\n```\n\nNote that chained executions require procedures returning a `rawptr`.\nThe return of the first procedure passed into `oa.go` will be passed \nto the next procedure, vice versa.\n\n## testing \nTesting oasync is as simple as executing `odin test .` in the root \ndirectory of the project.\n\nNote that the test uses 4 worker threads and are executed on-site on my \ncomputer with the CPU `13th Gen Intel(R) Core(TM) i5-13400F (16) @ 4.60 GHz`.\nTests may fail running on a lower end device.\n\nIt is also known that tests may report memory leaks. These leaks are likely false \npositives and should be largely ignored.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffoldcat%2Foasync","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ffoldcat%2Foasync","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffoldcat%2Foasync/lists"}