| To me, Win32 was already falling away from the more intriguing model, which was Win16. Effectively, a Win16 system is/was basically exactly equivalent to an Erlang node, but one where your "processes" just happened to be paired, component-wise†, with handles to structs of GUI properties held in window-manager memory. Like an Erlang node, Win16 is/was: • green-threaded — i.e. they both have very low-overhead in-memory structures containing a tiny heap/arena and a reference to an in-memory delegate code module, through which execution would pass in turn. In Erlang, these are "processes"; in Win16, these are windows—i.e. actual windows, but also "controls" in the controls library. • cooperatively-scheduled (yes, Erlang is cooperatively scheduled—when you're writing C NIFs. When you're executing HLL bytecode in the Erlang VM, this fact is papered over by the call/ret instructions implicitly checking reduction-count and yielding; but if you're writing native code—like in Win16—you do that yourself.) • Message-passing, with every process having a message inbox holding dynamically-typed message-structs that must be matched on and decoded, or discarded. • Offering facilities to register and hold system-wide handles to large static data-blobs (Erlang large-binaries, Win16 rsrc handles); • Capable of doing IPC only by having processes post messages to another process's queue, and then putting themselves into a mode that waits for a response; • Based on a supervision hierarchy: in Erlang, processes spawn children (themselves processes) and then manage them using IPC; in Win16, root-level windows spawn controls (themselves windows) and then manage them using IPC. And, crucially, in both of these systems, real machine-threads are irrelevant. Both Win16 and Erlang were written in an era when "concurrency" was a desired goal but multicore didn't yet exists—and so they don't really have any concept of thread affinity for processes/windows. Both systems are designed as if there is only one thread, belonging to a single, global scheduler—and then their multicore variants (SMP Erlang and Win32) attempt to transparently replicate the semantics of this older system (though in different ways: Erlang allows processes to be re-scheduled between scheduler threads, while Win32 pins windows to whatever scheduler-thread they're spawned on.) Win32 later introduced an alternative model to take better advantage of multithreading: COM "multi-threaded apartments", allowing windows (or, as a generalization, COM servers, which could now execute and participate in IPC on a thread without spawning any window-instances) to interoperate across thread boundaries without requiring a message be serialized and passed through the scheduler/window-manager process. † http://cowboyprogramming.com/2007/01/05/evolve-your-heirachy... |
I'm not sure your description of COM is quite right. The way I remember it, windows (HWNDS) were and still are objects with thread affinity. COM had the notion of a "single threaded apartment" which basically meant the COM server received RPCs using regular Window messages, and the MTA that you mention simply meant no inter-thread marshalling was done at all i.e. the object was inherently thread safe using locks or whatever. But Windows never changed to a model where the controls library was thread safe: changing the contents of an edit box from another thread, for instance, always required a context switch.
COM's usage and abusage of the window message system for fast inter-thread switching was only ever an ugly hack, which caused all kinds of weird problems and glitches. Most obviously it caused Windows' reliance on actually having a GUI layer to deepen considerably because now inter-thread/inter-process RPC - that on Linux and MacOS were well modularised into things like Mach IPC, SunRPC, DBUS etc - were totally tied to the windowing system.
IIRC the entire apartment concept was also stupidly designed, so there were constant problems with Microsoft using COM internally to implement some APIs, which would by default enter an STA and require the _caller_ of the API to pump the message queue otherwise the API they'd just used would silently fail to work. In the era I was working with it that fact wasn't always properly documented, I think.