0

I'm a bit confused about how a Tcl server works. I've been coding a small version that works pretty well for my local application but I'd like to understand it better conceptually. I have Ashok Nadkarni's book and used it as a starting point and thought I understood the examples about how the server does not block. And, I went through some of the code from the tclhttpd3.5.1 server to get started (some months ago, now, and was using that general method of using state to repeatedly read until I was pointed to using coroutines).

I think my code has been working because one desktop application with very rapidly processed requests rarely has any issues of blocking. I thought all was correct because the previous session could be restored quickly even though multiple database requests are (or appear to be) processed concurrently.

My plan was to use a small number of web sockets to handle the SQlite database requests and use HTTP for any audio/video requests. (The application is like a digital study library.) When I started to code the audio parts to play, while leaving the rest of the UI active such that database requests could be made while it played, I started to wonder more about the way I structured it. I got it all to work for my particular case and, it likely, will handle everything quickly but I'd like to understand better.

I was stupidly thinking that the server just handles everything without ever blocking because it can handle multiple connections, but I put all my code in the same script as the server code. So, if I "fabricate" a blocking scenario, the server requests pile up but are eventually fulfilled. Although the server doesn't block, if one of the requests is time consuming, the program itself blocks until each request completes, apart from an exception like fcopy with -command option. I know that source can be used to break the script into component scripts but that doesn't make it non-blocking.

If one was going to use Tcl as a "real" server rather than just for one application, how are the requests to be processed without blocking each other? I mean are they to be executed as separate threads, separate child processes, separate interpreters? Maybe those three are almost the same, I don't know.

Should the server script call multiple instances of other scripts to fulfill the requests rather than calling procedures within the same "single-instance" script; and the operating system will process them as efficently as possible? But, if so, how is information shared between the processes? If two similar requests arrived at nearly the same time, could two spearate instances of the same script be loaded to fulfill the requests concurrently, such that one does not block the other?

In looking over the code in the tclhttpd3.5.1 server, it appears that, at times, he is using multiple interpreters and aliasing something; but I don't follow what's going one there.

I apologize for not knowing the proper terminology. Thank you for any guidance you may be able to provide.

Gary
  • 2,393
  • 12
  • 31
  • 1
    You're looking for [the event loop](https://wiki.tcl-lang.org/page/event%2Doriented+programming) – Shawn May 13 '23 at 06:47
  • 1
    The same way node.js does it.. only Tcl did it first - using nonblocking I/O. And just as I always say for my node.js answer you cannot execute multiple code in parallel but you can wait for I/O requests in parallel. So for SQLite you cannot process multiple queries in parallel (because SQLite is just a file format and a library to process that file format) but when reading from disk you can wait for multiple reads in parallel. If you really want to process multiple queries in parallel use a database server such as MySQL (or MariaDB), PostgreSQL etc. – slebetman May 13 '23 at 07:55
  • The explanation for how this works is in my answer the the following questions. Though they are about javascript, tcl works the same way except that tcl really is single-threaded and does not use a thread pool for disk I/O like javascript: https://stackoverflow.com/questions/34855352/how-in-general-does-node-js-handle-10-000-concurrent-requests/34857298#34857298 – slebetman May 13 '23 at 08:06
  • https://stackoverflow.com/questions/29883525/i-know-that-callback-function-runs-asynchronously-but-why/29885509#29885509 – slebetman May 13 '23 at 08:06
  • https://stackoverflow.com/questions/72687621/how-does-node-js-schedule-asynchronous-and-synchronous-tasks/72689337#72689337 – slebetman May 13 '23 at 08:06
  • https://stackoverflow.com/questions/61262054/is-there-any-other-way-to-implement-a-listening-function-without-an-infinite-w/61826079#61826079 – slebetman May 13 '23 at 08:07
  • 1
    @slebetman Thank you for all the links. I think I understand a good bit of this from JavaScript and was getting confused by trying to make a `chan copy` on a SQLite `incrblob` channel not block. The `incrblob` channel just won't work with the `-command` option of `chan copy` and everything I tried failed. Perhaps it just doesn't respond to the events for some reason. I should say that the incrblob channel doesn't block when the option is used, but it also doesn't copy anything either. – Gary May 14 '23 at 06:39
  • @slebetman I've been thinking about this a bit more and wanted to restate that last comment. I shouldn't have wrote that I understand all of the information; for I am just an untrained hack, at best, and should've wrote that I have a moderate grasp of the general concepts, at least enough to code promises and web workers in JS. I didn't mean to give the impression that I already knew all the things you wrote in your linked answers or come across as unappreciative for your reponses. I appreciate you taking the time to provide them and I'm sure it'll be worthwhile for me to study them further. – Gary May 14 '23 at 20:13
  • @Shawn Thank you. I thought I had a decent understanding of the event loop until, in reviewing the link you provided and its other links, read over Ashok Nadkarni's book section on "Avoiding event queue starvation" and interleaving the execution of tasks between the idle event queue and the event queue. Breaking time-consuming tasks into components using `after 0` recursively, and how the two event queues work was all new to me. – Gary May 14 '23 at 20:22
  • Tcl has an additional command that js does not have to deal with letting the event queue process events. The `after 0` or `after idle` trick is similar to the `setTimeout(()=>{}, 0)` trick in js. You break up your computation into smaller blocks that you can schedule execution in the event loop. However tcl can also directly enter the event loop - it has the `update` command. The `update` command forces the interpreter to process pending events. So instead of breaking up your computation into blocks you periodically enter the event loop (eg, every 100th iteration etc.) – slebetman May 14 '23 at 22:41

1 Answers1

2

One of the pieces of Tcl's low level implementation is an event loop. It allows it to wait for an event (typically a file descriptor becoming readable or writable, which covers a great many things, or a message from another thread) in the OS. In particular, it can wait for many file descriptors at once, and it can wait a defined amount of time (that's how timer events work). The implementation of these things isn't trivial, but the API surfaced by the event loop system is: you give a callback to be called when the event happens (with chan event or after), and the event loop implementation looks after the rest.

Things like the http package and chan copy and so on all build on this foundation.

The event loop in a particular thread is synchronous: it doesn't receive events when your code is actively doing something. Each thread will have its own event loop (not an important distinction until you use multiple threads).

There are three ways that the event loop may run:

  1. Implicitly. This is how things work when you've got the Tk package loaded; when you get to the end of your script, the event loop is entered.
  2. Explicitly with vwait (Tk's tkwait is a minor variation on this). In this case, you're running the event loop in waiting mode until the given variable is set (which fires a trivial C trace to set a flag that the outer waiting loop picks up; it's exactly as simple as it sounds).
  3. Explicitly with update. In this case, you're running the event loop in no-waiting mode to consume events that have already become ready without actually waiting for anything. (update idletasks is a variation that only picks up idle events; idle events are things that are scheduled to happen only when there's nothing else to do, and are the core of Tk's Secret Sauce. They're not much used in non-Tk applications.)

It's not a good idea to run an event loop within an event loop. You can, it's not illegal or anything like that, but it tends to produce annoying bugs. Rewriting your code to avoid that (coroutines help!) tends to produce better code.

The other major part is non-blocking I/O, which Tcl supports on as many channels as it can (typically anything except an ordinary file, because your OS doesn't usefully support that either). When a channel is blocking, reads from and writes to it can block (i.e., take time to complete). When it is put in non-blocking mode, those reads and writes can instead fail to go through; typically you can ignore writes that fail that way as Tcl's default behaviour there is to just schedule the write to happen later, but for reads that means you can get a failure to read despite some data being available, and that's what chan blocked and chan pending give you a handle on.

The combination of an event loop and non-blocking I/O lets you do a lot of things with a single thread, and the only times when the single-threadedness will show up will be if you're doing lots of computation or the amount of I/O is getting really large. That's when you use multiple threads (with sending tasks into thread pools being a recommended approach for giving good manageability and resource usage control).

Donal Fellows
  • 133,037
  • 18
  • 149
  • 215
  • The low level of how the event loop works (the notifier engine) is something I'm happily not fully understanding most of the time. There is considerable complexity in there, especially around the handling of inter-thread messages and non-socket resources on Windows. I do know this engine (plus support for non-blocking mode) is one of the main reasons why Tcl doesn't use C stdio. – Donal Fellows May 15 '23 at 11:50