Search code examples
multithreadingclient-serverchannelnim-lang

How to set up a small client-server example between threads in nim?


For the owlkettle package (a declarative gtk wrapper) in nim I am researching how one could implement multi-threading "properly".

For that I am looking into how one would setup a client-serer architecture generally.

I know nim is able to run multi-threaded when compiled with --threads:on (or on nim 2.0 by default), so this should be possible. This minimal example showcases a mechanism called "channels", but how do I turn this into a more client-server based example where both threads keep going until I stop it?


Solution

  • That just requires a small while-loop on both ends.

    The following example is a modification of the one linked above. One "frontend" thread reads in user input from the terminal and sends it to the "backend" thread which can do things with it.

    Note that you could also use an object-variant instead of a string (so Channel[YourMessageVariant]) to allow for sending different kind of messages.

    Also as a sidenote, the following example will have 3 threads running:

    1. The main thread running the nim application and main-proc, which at the end is waiting for the sender and receiver threads to finish, thus consuming no resources
    2. The sender thread which never finishes
    3. The receiver thread which never finishes
    import std/[os]
    
    proc setupSender(commChan: var Channel[string]): Thread[ptr Channel[string]] =
      proc sendMsg(chan: ptr Channel[string]) =
        echo "Type in a message to send to the Backend!"
        while true:
          let terminalInput = readLine(stdin) # This is blocking, so this Thread doesn't run through unnecessary while-loop iterations unlike the receiver thread
          echo "Sending message from frontend from thread ", getThreadId(), ": ", terminalInput
          while not chan[].trySend(terminalInput):
            echo "Try again"
    
      createThread(result, sendMsg, commChan.addr)
    
    proc setupReceiver(commChan: var Channel[string]): Thread[ptr Channel[string]] =
      proc recvMsg(chan: ptr Channel[string]) =
        while true:
          let response: tuple[dataAvailable: bool, msg: string] = chan[].tryRecv()
          if response.dataAvailable:
            echo "Received message at Backend on Thread: ", getThreadId(), " :", response.msg
          sleep(0) # Reduces stress on CPU when idle, increase when higher latency is acceptable for even better idle efficiency
      
      createThread(result, recvMsg, commChan.addr)
    
    proc main() =
      var commChan: Channel[string] # A queue of messages send from one thread to another, can have a pre-defined size
      commChan.open() # Must be done before using the channel
      let senderThread = setupSender(commChan)
      let receiverThread = setupReceiver(commChan)
      joinThreads(senderThread, receiverThread)
    
    main()