This is going to be extremely light on details with respect to the current state of 
the Parrot interpreter. 

It is also going to be expressed in terms of Win32 APIs.

For both of these I apologise in advance. Time, and the "or forever hold your peace" 
imperative has overridden my desire to do otherwise. 

My attempts to get up speed enough on the sources to work out how to apply the ideas I 
am going to express, and to become conversant with the *nix 
pthreads implementation and terminology, have moved too slowly to afford me the 
opportunity to do more.

Hoping this stimulates discussion rather than a flame war.

Regards, Nigel Sandever.

PS.  Sorry if this gets posted twice.
------------------------

THE BASIS OF THE IDEA

Modern OSs succeed in having multiple threads of execution share a single copy of 
process memory without the operations of one thread being able to 
interfere with the state of another. The state of the code running in those threads 
may be corrupted through mismanagement. But the state of the 
threads themselves, their program counters, registers and stacks cannot. The 
mechanisms for this incorruptibility are: 

        Each operation (opcode) performed by the thread is atomic. 
            The scheduler can never interrupt a thread whilst an operation is in 
progress. Only between operations.
        
        Before the start of an operation, and after the end of one, the state of the 
thread is entirely encapsulated within the registers and stack. 
            By swapping the entire state of the CPU register set, when switching from 
one thread to another, the state of each thread is preserved
            and reconstituted. No other mechanisms or interlocks are required.

By analogy, a virtual machine that wishes to have multiple threads of execution must 
achieve the same level of atomicity for each operation it performs. 

VIRTUAL MACHINE INTERPRETER

At any given point in the running of the interpreter, the VM register set, program 
counter and stack must represent the entire state for that thread. 
Once an opcode has started execution on a given thread, no other thread of execution 
within that interpreter much be allowed to start an operation until 
the first thread completes its opcode. 

NON-VMI THREADS 

ASYNCHRONOUS IO

Note that this does not mean that no other thread in the process can take a timeslice, 
only that any thread that is allowed to run should not be able to 
affect the state of the VM in question. A thread charged with performing asynchronous 
reads on behalf of the user program running within the VM 
interpreter can go ahead so long as it doesn't directly modify the VMI state. 

EVENT MANAGER THREAD

Equally, an event thread can also be run concurrently in the background to receive 
asynchronous notifications (signals, messages, asynchronous read 
completions etc.). It can then queue these events and set a flag that the VMI can 
inspect between each iteration of the "opcode execution loop" and action 
appropriately. This gives the benefit of "safe signals" along with safe and timely 
processing of other, similarly asynchronous events.

GARBAGE COLLECTION

The garbage collector would need to run *synchronously* with the interpreter opcode 
loop. Effectively, it would be another (possibly long running) opcode. 
An analogy of this are all the long running syscalls that operate within a 
multi-tasking OS. Eg. synchronous IO, virtual memory allocation, swapping etc. 
Just as virtual memory operations suspend the affected processes until the operations 
are complete, so garbage collection can be see as a virtual memory 
operation for the virtual machine that requires the VMI to be suspended until the 
operation is complete.

PREREQUISITES

The key is for the VM to be reentrant, and the use of (in win32 terms) a "critical 
section".

REENTRANCY

Not only must the VMI be coded in a reentrant fashion, with all state addressed 
through pointers (references) loaded into it's Virtual register set. All 
the code underlying it, including syscalls and CRT must equally be reentrant. Many 
APIs within many CRTs *are not reentrant* (eg. strtok()). All state 
must be on a per-thread not a per-process basis.

To this end, I would propose that no CRT APIs are used directly from the main code!

Instead, a full set of CRT-like macros would be inherited from a header file, where 
either the real CRT API would be called, or an alternative 
implementation. This header file would be conditionally included on the basic of the 
target platform. This concentrates the bulk, if not all, platform 
specific code into a single file (or set of files). 

EXPANDING THE VIRTUAL MACHINE

What I am suggesting here is that the virtual machine analogy be extended to encompass 
not just the VHLL-user-program view of the registers and stack, 
but also it's view of the entire machine and underlying OS. 

By so doing, not only does it allow those working on the core of the VMI to be 
isolated from the underlying machine and OS architecture. It allows them 
to extend the VMI's view of the world, beyond the concepts of any single architecture. 
It also serves the very practical purpose of avoiding the morass 
of platform-conditional compilation directives in the bulk of the code, by 
concentrating them in single (groups of) files, by platform.


ATOMICITY AND CRITICAL SECTIONS

Atomicity of the VMI opcodes can be achieved by having the core loop of the 
interpreter enter and exit a critical section each time it processes an 
opcode. For those not familiar with critical sections, they are a (Win32) OS construct 
that prevents any cooperating thread within a process, from having 
timeslice until the thread that entered the critical section has exited. 

Unlike semaphores and events, critsecs only operate within a single process. They are 
relatively lightweight as there is no need to enter kernel mode for 
their operation. They are, in effect a CPU specific "test and set" operation applied 
to a piece of user mode memory. Their lightweight nature means that 
they are faster than inter-process semaphore mechanisms. When used in a process that 
currently has only a single thread in operation, they have almost 
negligible performance effect upon that thread. They also operate correctly on SMP 
machines.

PROCESS GLOBAL STATE

Unavoidably process global state, such as filehandles etc., should only be usable 
(from the user program being run by the VMI) through the use of opcodes. 
Done this way, the serialisation of the opcodes at the VMI level protects the global 
state.

THIS IS ONLY PART OF THE SOLUTION

None of the above addresses the problems of synchronising user program state. This 
will still need to be addressed. This could either be done by the user 
code through the use of something like the P5 :shared and lock() mechanisms. Or 
preferably, transparently by the VMI itself.  Possibly, by having each PMC 
contain an 'in-use' flag that would be set each time an opcode (method) was dispatched 
on a PMC (object) and cleared when the operation completes. 

If two threads attempt concurrent operations on the same PMC, the first thread 
accessing the PMC sets the flag. When a second thread attempt to 
access it, it finds the flag set and blocks (relinquishing it's timeslice) until the 
first thread has completed and cleared the flag. This doesn't solve the 
potential for deadlock problems arising from the user-level code, though it may be 
possible for the VMI to detect and diagnose these at runtime in a 
similar way that deep-recursion detection is done in P5.

The cost to the single-threaded user application is for a test&set operation (a few 
cycles), per object-method, plus the flag, which need only be one bit, 
but maybe more efficiently coded as a 32-bit entity.

CONCLUSION (TENTATIVE, NO PROOF)

As all VHLL entities are PMCs at the VMI level, the sharing of data between threads at 
the VHLL level is done entirely through those PMCs. If no single 
PMC can be the subject of an opcode on two threads concurrently, there /should/ be no 
opportunity for conflict. 

As all VMI internal state are encapsulated within the VMI register set, and each 
thread has it's own set of registers, the internal state of the VMI(s) 
should be equally secure. 

Other internal housekeeping operations, memory allocation, garbage collection etc. are 
performed as "sysopcodes", performed by the VMI within the 
auspices of the critical section, and thus secured.

All asynchronous operations are performed by one or more non-VMI threads and do not 
adjust the state of the VMI directly. Instead, they queue 
notifications of events, and results to the VMI, and these are detected by the VMI 
within the body of the main loop. Once detected, an appropriate 
sysopcode is dispatched within the critical section in the normal way.

SOME ADDITIONAL IDEAS

MEMORY MANAGEMENT

I've been looking at (glancing through) the memory management used by Parrot. The 
basic concept of memory pools, if I have understood them correctly, is 
to allocate similarly size object from the same pool. The idea behind this is that it 
will be easier to find a suitable size piece of memory available for 
reallocation having been GC'd, from a pool dedicated to allocation of a similar size 
to that needed. This should reduce the fragmentation of originally large 
allocations into lots of small allocations, thereby reducing the need for coalescence, 
and overall memory allocation.

The thought struck me that with the exception of strings and (other) compound data 
types, all the instances of any given class are always the same size. 
Where an existing class is dynamically extended in some way, the result is the 
creation of a new (anonymous) class as pre-existing instances of the 
original class would not be modified. As such, the class would seem to be the ideal 
point at which to pool the allocation and deallocation of memory. 

If each class is given it's own memory pool that is a multiple of its instance size. 
That pool effectively becomes an  (C-style) array of instances. As each 
element is exactly the right size, the only time the pool would need to grow, is when 
all the existing elements are in-use and another is required. Once an 
instance has been freed, it's slot in the array would be available and exactly the 
right size for the next instantiation. There would never be a requirement 
to coalesce allocations. Whether the free slots are found through a free space chain, 
or even a linear search, the GC would only need to process this pool 
when a new instance of this class is being allocated. It would then only need to scan 
a single pool of (same-sized) reference objects to GC the existing 
instances.

If all references to this type where allocated from another pool dedicated to 
references-to-this-class, that also hung off the parent class, then the 
volume of references to be scanned would also be minimised.

If the larger segments of memory allocated for each pool, were allocated as virtual 
memory segments, rather than as arbitrary chunks of heap memory, the 
size of each individual pool can be grown in-place, at runtime, without the need to 
copy then existing space to the new larger allocation. And without the 
need to shuffle any other memory pools around to accommodate the increase in size. The 
OS's Virtual Memory Management takes care of the whole 
process. It also becomes possible to reserve a sizeable pre-allocation for important 
pools and those known to be likely to grow quickly, without actually 
consuming the entire reservation from the physical memory pool before it is actually 
needed. The OS will then take care of notification of an individual 
pool's need to grow through page faults and the growth becomes a process of simply 
committing another (few) pages from the pre-allocation. 

The memory requirements of variable size objects can be managed through Global Heap 
and sub-allocation APIs as now.

Thread specific local storage required for the VMI's register stacks etc. can be 
handled using Thread Local Storage APIs.

COOPERATIVE MULTIPROCESSING AND CO_ROUTINES.

Some applications benefit from the programmer being able to have much tighter control 
over the scheduling of threads of execution within the process. 
The extreme example of this are co-routines where control is passed back and forth 
between two (or more) pieces of code as the demands of the algorithm 
and data dictate rather than on the basis of arbitrary time-slicing. Win32 has a 
concept of Fibres. The are manually dispatched threads. A given thread 
(fibre) receives it's timeslice in the normal way. However, it can transfer the rest 
of it's current timeslice to another fibre on demand. The first fibre 
remains suspended until the second fibre transfers control back. The state of the 
first fibre is retained just as it would be if the thread had been 
swapped by the scheduler. In this way two (or more) fibres can cooperatively share 
their timeslices whilst other threads (and processes) continue to 
receive their timeslices in the normal way. This, I think, is the essence of 
co-routines?

I have no understanding (yet) of how the co-routines that Parrot is promising are 
implemented on *nix systems. But unless the concept of co-routines is 
encapsulated at the higher level with the implementation being pushed down to platform 
specific code, unless *nix also supports a concept very similar to 
Win32 fibres, it will be impossible to utilise this "tailor made" OS facility. Thus, 
the implementation of co-routines designed to operate well on *nix 
platforms is likely to need to be emulated on Win32 with high level requirements that 
won't fit well with the underlying OS. This could result in a 
similarly sub-optimal emulation of a *nix concept on Win32 as currently exists for the 
fork emulation. Other non-*nix platforms would likely also suffer 
from this force fit as well.



Reply via email to