├── .gitignore ├── LICENSE.md ├── README.md ├── example ├── nim.cfg ├── pingpong.nim ├── sort.nim ├── syncPingpong.nim └── waitOneTask.nim ├── nimroutine.nimble └── src └── nimroutine └── routine.nim /.gitignore: -------------------------------------------------------------------------------- 1 | nimcache/ 2 | *.exe 3 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Roger Shi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nim-routine 2 | A go routine like nim implementation 3 | 4 | ## Features 5 | + Light-weight task. 6 | + Go channel like message box (communication between tasks). 7 | + Support recursive task creating, that is to create new task in task itself. 8 | 9 | ## Routine (Task) 10 | + How to define a routine? 11 | 12 | It looks like normal proc, adding **routine** proc. For example: 13 | ```Nim 14 | proc foo(x: int) {.routine.} = 15 | echo "routine foo: ", x 16 | ``` 17 | 18 | + How to run routine? 19 | 20 | `pRun` is the proc run routine, followed by routine name and a tuple. The tuple contains the routine's parameters. For example: 21 | ```Nim 22 | pRun foo, (x: 1) 23 | ``` 24 | 25 | + What about generic type? 26 | ```Nim 27 | proc foo[T](x: T) {.routine.} = 28 | echo "routine foo: ", x 29 | pRun foo[int], (x: 1) 30 | ``` 31 | 32 | + If the parameter is void? 33 | ```Nim 34 | proc foo() {.routine.} = 35 | echo "routine void param" 36 | pRun foo 37 | ``` 38 | 39 | + How to wait a task? 40 | ```Nim 41 | var watcher = pRun(foo) 42 | wait(watcher) 43 | ``` 44 | 45 | + How to wait all tasks? 46 | ```Nim 47 | waitAllRoutine() 48 | ``` 49 | 50 | ## MsgBox (Channel) 51 | There're two kinds of MsgBox: sync and async. For sync msgbox, sender wait until there's a receiver get the message. For async msgbox, send continues running withoug waiting except msgbox is full (holding message count == capacity). Msgbox's capacity is determined while creating. 52 | 53 | + Create sync MsgBox 54 | ```Nim 55 | var msgBox = createSyncMsgBox[int]() 56 | ``` 57 | 58 | + Create async MsgBox 59 | ```Nim 60 | var msgBox = createMsgBox[int]() # msgBox's capacity is -1, which means unlimited. 61 | var sizedMsgBox = createMsgBox[int](10) # msgBox that can hold 10 message 62 | ``` 63 | 64 | + Send message 65 | ```Nim 66 | send(msgBox, 1) # 1 is the data to be sent 67 | ``` 68 | 69 | + Receive mssage 70 | ```Nim 71 | var data: int 72 | recv(msgBox, data) # data is assigned to msg value 73 | ``` 74 | 75 | ## Limitation 76 | + Not support var param in task. Walkaround: ptr. 77 | + No return value is support 78 | -------------------------------------------------------------------------------- /example/nim.cfg: -------------------------------------------------------------------------------- 1 | path = "../src/" 2 | threads:on 3 | -------------------------------------------------------------------------------- /example/pingpong.nim: -------------------------------------------------------------------------------- 1 | import nimroutine/routine, locks 2 | 3 | var msgBox1 = createMsgBox[int]() 4 | var msgBox2 = createMsgBox[int]() 5 | 6 | proc ping(a, b: MsgBox[int]) {.routine.} = 7 | var value: int 8 | for i in 1 .. 10: 9 | echo "ping: ", i 10 | send(a, i) 11 | recv(b, value) 12 | assert(value == i) 13 | echo "ping done" 14 | 15 | proc pong(a, b: MsgBox[int]) {.routine.} = 16 | var value: int 17 | for i in 1 .. 10: 18 | recv(a, value) 19 | assert(value == i) 20 | echo "pong: ", i 21 | send(b, i) 22 | echo "pong done" 23 | 24 | pRun ping, (msgBox1, msgBox2) 25 | pRun pong, (msgBox1, msgBox2) 26 | 27 | waitAllRoutine() 28 | 29 | msgBox1.deleteMsgBox() 30 | msgBox2.deleteMsgBox() 31 | -------------------------------------------------------------------------------- /example/sort.nim: -------------------------------------------------------------------------------- 1 | import nimroutine/routine, math, times, sequtils, os, random 2 | 3 | proc quickSort[T](a: ptr seq[T], lo, hi: int, deep: int) {.routine.}= 4 | if hi <= lo: return 5 | let pivot = a[int((lo+hi)/2)] 6 | var (i, j) = (lo, hi) 7 | 8 | while i <= j: 9 | if a[i] < pivot: 10 | inc i 11 | elif a[j] > pivot: 12 | dec j 13 | elif i <= j: 14 | swap a[i], a[j] 15 | inc i 16 | dec j 17 | discard pRun(quickSort[int], (a, lo, j, deep+1)) 18 | discard pRun(quickSort[int], (a, i, hi, deep+1)) 19 | 20 | proc quickSort*(a: ptr seq[int]) = 21 | pRun quickSort[int], (a, a[].low, a[].high, 0) 22 | 23 | randomize(int(epochTime())) 24 | var a = newSeqWith(100, random(100)) 25 | 26 | echo a 27 | quickSort a.addr 28 | waitAllRoutine() 29 | echo a 30 | -------------------------------------------------------------------------------- /example/syncPingpong.nim: -------------------------------------------------------------------------------- 1 | import nimroutine/routine, locks 2 | 3 | var msgBox = createSyncMsgBox[int]() 4 | 5 | proc ping(a: MsgBox[int]) {.routine.} = 6 | var value: int 7 | for i in 1 .. 10: 8 | echo "ping: ", i 9 | send(a, i) 10 | recv(a, value) 11 | assert(value == i) 12 | echo "ping done" 13 | 14 | proc pong(a: MsgBox[int]) {.routine.} = 15 | var value: int 16 | for i in 1 .. 10: 17 | recv(a, value) 18 | assert(value == i) 19 | echo "pong: ", i 20 | send(a, i) 21 | echo "pong done" 22 | 23 | pRun ping, (msgBox) 24 | pRun pong, (msgBox) 25 | 26 | waitAllRoutine() 27 | 28 | msgBox.deleteMsgBox() 29 | -------------------------------------------------------------------------------- /example/waitOneTask.nim: -------------------------------------------------------------------------------- 1 | import nimroutine/routine, os 2 | 3 | proc task() {.routine.} = 4 | sleep(1000) 5 | 6 | var watcher = pRun(task) 7 | 8 | wait(watcher) 9 | echo("done") -------------------------------------------------------------------------------- /nimroutine.nimble: -------------------------------------------------------------------------------- 1 | [Package] 2 | name = "nimroutine" 3 | version = "0.1.2" 4 | author = "Roger Shi" 5 | description = "A go routine like nim implementation" 6 | license = "MIT" 7 | srcDir = "src" 8 | 9 | [Deps] 10 | Requires: "nim >= 0.11.3" 11 | -------------------------------------------------------------------------------- /src/nimroutine/routine.nim: -------------------------------------------------------------------------------- 1 | import os, locks, lists, tables, macros, cpuinfo 2 | 3 | const debug = false 4 | proc print[T](data: T) = 5 | when debug: 6 | echo data 7 | 8 | # Thread 9 | type 10 | BreakState* = object 11 | isContinue: bool # tell whether this yield need to be continued later 12 | isSend: bool # this yield is caused by a send operation 13 | msgBoxPtr: pointer # this msgBox's pointer (void*) that makes this yield 14 | 15 | TaskBody* = (iterator(tl: TaskList, t: ptr Task, arg:pointer): BreakState{.closure.}) 16 | Task* = object 17 | isRunable: bool # if the task is runnable 18 | task: TaskBody 19 | arg: pointer 20 | watcher: TaskWatcher 21 | hasArg: bool 22 | 23 | TaskList* = ptr TaskListObj 24 | TaskListObj = object 25 | index: int # for debug 26 | lock: Lock # Protect list 27 | candiLock: Lock # Protect send and recv candidate 28 | list: DoublyLinkedRing[Task] 29 | size: int # list's size 30 | recvWaiter: Table[pointer, seq[ptr Task]] 31 | sendWaiter: Table[pointer, seq[ptr Task]] 32 | sendCandidate: seq[pointer] 33 | recvCandidate: seq[pointer] 34 | 35 | TaskWatcher* = ref TaskWatcherObj 36 | TaskWatcherObj = object 37 | isFinished: bool 38 | lock: Lock 39 | 40 | var threadPoolSize = countProcessors() 41 | if threadPoolSize == 0: 42 | threadPoolSize = 4.Natural 43 | var taskListPool = newSeq[TaskListObj](threadPoolSize) 44 | var threadPool = newSeq[Thread[TaskList]](threadPoolSize) 45 | 46 | proc isEmpty(tasks: TaskList): bool= 47 | result = tasks.list.head == nil 48 | 49 | proc isEmpty(tasks: TaskListObj): bool= 50 | result = tasks.list.head == nil 51 | 52 | proc run(taskNode: DoublyLinkedNode[Task], tasks: TaskList, t: ptr Task): BreakState {.inline.} = 53 | if t.hasArg: 54 | result = taskNode.value.task(tasks, t, t.arg) 55 | else: 56 | result = taskNode.value.task(tasks, t, nil) 57 | 58 | # Run a task, return false if no runnable task found 59 | proc runTask(tasks: TaskList, tracker: var DoublyLinkedNode[Task]): bool {.gcsafe.} = 60 | if tracker == nil: tracker = tasks.list.head 61 | let start = tracker 62 | 63 | while not tasks.isEmpty: 64 | if tracker.value.isRunable: 65 | tasks.lock.release() 66 | let ret = tracker.run(tasks, tracker.value.addr) 67 | tasks.lock.acquire() 68 | 69 | if not ret.isContinue: 70 | #print("one task finished") 71 | let temp = tracker.next 72 | if tracker.value.arg != nil: 73 | tracker.value.arg.deallocShared() # free task argument 74 | tracker.value.watcher.lock.acquire() 75 | tracker.value.watcher.isFinished = true 76 | tracker.value.watcher.lock.release() 77 | tasks.list.remove(tracker) 78 | tasks.size -= 1 79 | if tasks.isEmpty: 80 | #print("tasks is empty") 81 | tracker = nil 82 | else: 83 | tracker = temp 84 | return true 85 | else: # tracker.value.isRunable 86 | tracker = tracker.next 87 | if tracker == start: 88 | return false 89 | 90 | return false 91 | 92 | proc wakeUp(tasks: TaskList) = 93 | tasks.candiLock.acquire() 94 | if tasks.sendCandidate.len > 0: 95 | for scMsg in tasks.sendCandidate: 96 | if tasks.sendWaiter.hasKey(scMsg): 97 | for t in tasks.sendWaiter[scMsg]: 98 | t.isRunable = true 99 | tasks.sendWaiter[scMsg] = newSeq[ptr Task]() 100 | tasks.sendCandidate = newSeq[pointer]() 101 | 102 | if tasks.recvCandidate.len > 0: 103 | for rcMsg in tasks.recvCandidate: 104 | if tasks.recvWaiter.hasKey(rcMsg): 105 | for t in tasks.recvWaiter[rcMsg]: 106 | t.isRunable = true 107 | tasks.recvWaiter[rcMsg] = newSeq[ptr Task]() 108 | tasks.recvCandidate = newSeq[pointer]() 109 | tasks.candiLock.release() 110 | 111 | proc slave(tasks: TaskList) {.thread, gcsafe.} = 112 | var tracker:DoublyLinkedNode[Task] = nil 113 | #print($tasks.index & "init ac") 114 | tasks.lock.acquire() 115 | while true: 116 | if not runTask(tasks, tracker): 117 | #print($tasks.index & "sleep re") 118 | tasks.lock.release() 119 | #print("task list is empty:" & $(tasks.isEmpty)) 120 | sleep(0) 121 | #print($tasks.index & "sleep ac") 122 | tasks.lock.acquire() 123 | wakeUp(tasks) 124 | 125 | proc chooseTaskList: int = 126 | var minSize = taskListPool[0].size 127 | var minIndex = 0 128 | for i, tl in taskListPool: 129 | if tl.size < minSize: 130 | minSize = tl.size 131 | minIndex = i 132 | return minIndex 133 | 134 | proc pRun* (iter: TaskBody): TaskWatcher {.discardable.} = 135 | new(result) 136 | result.lock.initLock() 137 | result.isFinished = false 138 | let index = chooseTaskList() 139 | 140 | taskListPool[index].lock.acquire() 141 | taskListPool[index].list.append(Task(isRunable:true, task:iter, arg: nil, watcher: result, hasArg: false)) 142 | taskListPool[index].size += 1 143 | taskListPool[index].lock.release() 144 | 145 | proc pRun* [T](iter: TaskBody, arg: T): TaskWatcher {.discardable.} = 146 | new(result) 147 | result.lock.initLock() 148 | result.isFinished = false 149 | let index = chooseTaskList() 150 | 151 | var p = cast[ptr T](allocShared0(sizeof(T))) 152 | p[] = arg 153 | 154 | taskListPool[index].lock.acquire() 155 | taskListPool[index].list.append(Task(isRunable:true, task:iter, arg: cast[pointer](p), watcher: result, hasArg: true)) 156 | taskListPool[index].size += 1 157 | taskListPool[index].lock.release() 158 | 159 | proc initThread(index: int) = 160 | taskListPool[index].index = index 161 | taskListPool[index].list = initDoublyLinkedRing[Task]() 162 | taskListPool[index].lock.initLock() 163 | taskListPool[index].candiLock.initLock() 164 | taskListPool[index].sendWaiter = initTable[pointer, seq[ptr Task]]() 165 | taskListPool[index].recvWaiter = initTable[pointer, seq[ptr Task]]() 166 | taskListPool[index].sendCandidate = newSeq[pointer]() 167 | taskListPool[index].recvCandidate = newSeq[pointer]() 168 | createThread(threadPool[index], slave, taskListPool[index].addr) 169 | 170 | proc setup = 171 | for i in 0.. 0: 264 | print("template recv") 265 | msg = msgBox.data.head.value 266 | msgBox.data.remove(msgBox.data.head) # O(1) 267 | msgBox.size -= 1 268 | notifySend(msgBox) 269 | break 270 | else: 271 | #print("recv wait") 272 | registerRecv(tl, msgBox, t) 273 | t.isRunable = false 274 | msgBox.lock.release() 275 | yield BreakState(isContinue: true, isSend: false, msgBoxPtr: cast[pointer](msgBox)) 276 | msgBox.lock.acquire() 277 | msgBox.lock.release() 278 | 279 | ## Macro 280 | proc getName(node: NimNode): string {.compileTime.} = 281 | case node.kind 282 | of nnkPostfix: 283 | return $node[1].ident 284 | of nnkIdent: 285 | return $node.ident 286 | of nnkEmpty: 287 | return "anonymous" 288 | else: 289 | error("Unknown name.") 290 | 291 | proc routineSingleProc(prc: NimNode): NimNode {.compileTime.} = 292 | if prc.kind notin {nnkProcDef, nnkLambda}: 293 | error("Cannot transform this node kind into an nim routine." & 294 | " Proc definition or lambda node expected.") 295 | 296 | hint("Processing " & prc[0].getName & " as an nim routine") 297 | 298 | let returnType = prc[3][0] 299 | 300 | # Verify that the return type is a void or Empty 301 | if returnType.kind != nnkEmpty and not (returnType.kind == nnkIdent and returnType[0].ident == !"void"): 302 | error("Expected return type of void got '" & $returnType & "'") 303 | else: 304 | hint("return type is void or empty") 305 | 306 | result = newStmtList() 307 | 308 | var procBody = prc[6] 309 | 310 | # -> var rArg = (cast[ptr tuple[arg1: T1, arg2: T2, ...]](arg))[] 311 | if prc[3].len > 1: 312 | var rArgAssignment = newNimNode(nnkVarSection) 313 | var tupleList = newNimNode(nnkTupleTy) 314 | for i in 1 ..< prc[3].len: 315 | let param = prc[3][i] 316 | assert(param.kind == nnkIdentDefs) 317 | tupleList.add(param) 318 | rArgAssignment.add( 319 | newIdentDefs( 320 | ident("rArg"), 321 | newEmptyNode(), 322 | newNimNode(nnkBracketExpr).add( 323 | newNimNode(nnkPar).add( 324 | newNimNode(nnkCast).add( 325 | newNimNode(nnkPtrTy).add(tupleList), 326 | newIdentNode("arg")))))) 327 | 328 | # -> var arg1 = rArg.arg1 329 | # -> var arg2 = rArg.arg2 330 | # -> ... 331 | for i in 1 ..< prc[3].len: 332 | let param = prc[3][i] 333 | assert(param.kind == nnkIdentDefs) 334 | for j in 0 .. param.len - 3: 335 | rArgAssignment.add( 336 | newIdentDefs( 337 | param[j], 338 | newEmptyNode(), 339 | newNimNode(nnkDotExpr).add( 340 | ident("rArg"), 341 | param[j]))) 342 | 343 | procBody.insert(0, rArgAssignment) 344 | 345 | var closureIterator = newProc( 346 | newIdentNode($prc[0].getName), 347 | [ 348 | newIdentNode("BreakState"), 349 | newIdentDefs(ident("tl"), ident("TaskList")), 350 | newIdentDefs(ident("t"), newNimNode(nnkPtrTy).add(newIdentNode("Task"))), 351 | newIdentDefs(ident("arg"), ident("pointer")) 352 | ], 353 | procBody, 354 | nnkIteratorDef) 355 | closureIterator[4] = newNimNode(nnkPragma).add(newIdentNode("closure")) 356 | 357 | # Add generic 358 | closureIterator[2] = prc[2] 359 | result.add(closureIterator) 360 | 361 | macro routine*(prc: untyped): untyped = 362 | ## Macro which processes async procedures into the appropriate 363 | ## iterators and yield statements. 364 | if prc.kind == nnkStmtList: 365 | result = newStmtList() 366 | for oneProc in prc: 367 | result.add routineSingleProc(oneProc) 368 | else: 369 | result = routineSingleProc(prc) 370 | 371 | proc waitAllRoutine* = 372 | var allFinished = true 373 | while true: 374 | for i in 0 ..< threadPoolSize: 375 | if not taskListPool[i].isEmpty: 376 | allFinished = false 377 | break 378 | if allFinished: 379 | return 380 | allFinished = true 381 | sleep(0) 382 | 383 | proc isDone(watcher: TaskWatcher): bool = 384 | watcher.lock.acquire() 385 | result = watcher.isFinished 386 | watcher.lock.release() 387 | 388 | proc wait*(watcher: TaskWatcher) = 389 | while not watcher.isDone(): 390 | sleep(0) --------------------------------------------------------------------------------