Managing Concurrency - rosco-pc/propeller-wiki GitHub Wiki
Software running on Multiple COGs is equivalent to multiple threads in a single process, and the same sorts of concurrency protection is needed to ensure correct operation. Race conditions are very easy to create. A typical design pattern is a object that uses a number of spin utility methods to access values stored in hub ram (DAT or VAR), and a loop in Spin or PASM main loop that runs on a COG. This generally limits the interaction of the utility methods and the main loop is by the Hub RAM.
Access to HUB ram is atomic for 32 bit reads OR writes. You cannot read and then write back a modified variable atomically. You cannot read more than one long variable atomically. The speed of the code doing the read and writes is likely to be very different as at least one side will be interpreted spin code, and the speed of spin operations is not transperant.
To ensure correct/sane results when pulling data from a Object that uses a COG thread needs to use some way to avoid race conditions. Techniques applicable for Propellers are:
- use a lock
- read a single 'output' variable. i.e. calculate interesting values in the cog and store in a hub ram.
- pack/unpack multiple variables into one LONG (i.e. atomic changes)
- read AND write groups of variables in very carefully timed code so that a race can not occur.
- start and stop the COG on demand (seems probably fast IF it is spin)
- Use some form of queue and/or flag in hub ram for handshaking.
If the value read or written by the utilities is independent, then no further precautions are needed. A variation would be to pack a group of variables into a long.
Handshakes via hub ram can be very simple and effective, so long as the handshakes are one direction. If only dealing with 2 variables, it is pretty simple to ensure a race does not cause in correct behavior.
By carefully analysing the order of memory access and modifications, races may be able to be avoided. This works because the COG's hub access is in lock step, so if one process copies in changes at the same rate as another is reading them out, they cannot overlap and get mixed values. However this would be rather difficult to maintain, as it requires analysis of the spin and/or PASM.
Starting and stopping a COG running spin is actually very fast, and in some cases it may turn out to be practical to stop the cog, and then restart it again. However to be safe in this approach, the stopping of the COG will probably need a handshake to avoid corrupting the state due to partially completed variable updates.
Another related problem is that the looping COG might never wake up if it is using a WAIT type instruction for an external event to occur. Stopping the COG maybe possbible if there is a way to avoid corruption of object variables (lock, handshake, or limited preserved state) Another alternative is to use a IO pin as a interupt and add it to the wait mask.
Example: Calculating speed
Calculating speed from a tacho type source will normally involve 2 values for the time references. From this the worker COG running the main loop can calculate the speed value from its internal variables, and store it in a hub location, after each pulse. But what do you do when it has stopped and there is NO pulses?
In this case it is probably best is to store the BOTH the speed AND then the time of the last pulse, in that order. Check for a RECENT pulse BEFORE using the speed. In this case, a trick is that you need to also watch for roll over if you are using CNT as the time reference. In that case avoid the risk of a false pulse of speed every rollover, WRITE back a speed of zero when stopped is detected. This leaves a small risk of a speed Zero being reported until the COG queriing it has completed antother poll cycle, which is effectively no consequence.
Stopped Cogs
Some designs might be a bit simpler with a interupt to trigger the COG to do something that is outside its normal operation. An example could do with a timer interupt, OR they have to use locking