Timing, Threading, and Sleeping

Ok, I have a very specific goal I’m trying to achieve in timing on the G120. It’s a bit of a doozey. I’ve demonstrated the effect that I’m trying to achieve using relay’s in a short video. Don’t Worry though, in real life I’m using mosfet’s instead of relays.

http://1drv.ms/1ZEO6dK

I don’t really need to explain how I come up with the timing or what it is used for. There are a ton of variables that all get worked into a single thread that ends up looking some thing like this.



private static IGPOPort[] Pulse = new IGPOPort[16];

private static int[] Delays = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };

private static bool[] Pattern = { true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true };

while (true)
            {
                Thread.Sleep(Delays[0]);
                Pulse[0].Write(!Pattern[0]);
                Thread.Sleep(Delays[1]);
                Pulse[1].Write(!Pattern[1]);
                Thread.Sleep(Delays[2]);
                Pulse[2].Write(!Pattern[2]);
                Thread.Sleep(Delays[3]);
                Pulse[3].Write(!Pattern[3]);
                Thread.Sleep(Delays[4]);
                Pulse[4].Write(!Pattern[4]);
                Thread.Sleep(Delays[5]);
                Pulse[5].Write(!Pattern[5]);
                Thread.Sleep(Delays[6]);
                Pulse[6].Write(!Pattern[6]);
                Thread.Sleep(Delays[7]);
                Pulse[7].Write(!Pattern[7]);
                Thread.Sleep(Delays[8]);
                Pulse[8].Write(!Pattern[8]);
                Thread.Sleep(Delays[9]);
                Pulse[9].Write(!Pattern[9]);
                Thread.Sleep(Delays[10]);
                Pulse[10].Write(!Pattern[10]);
                Thread.Sleep(Delays[11]);
                Pulse[11].Write(!Pattern[11]);
                Thread.Sleep(Delays[12]);
                Pulse[12].Write(!Pattern[12]);
                Thread.Sleep(Delays[13]);
                Pulse[13].Write(!Pattern[13]);
                Thread.Sleep(Delays[14]);
                Pulse[14].Write(!Pattern[14]);
                Thread.Sleep(Delays[15]);
                Pulse[15].Write(!Pattern[15]);
                Thread.Sleep(x);
            }

I’m using a 74hc595 shift register to switch the pins and have calculated the switching time into the equation [20ms]. Sounds bad but I’m dealing with 1Hz here. [All the sleeps add up to 1000ms - the switch delay]

Up to here it works perfectly…But…I need to run a second tiny, little, lite, thread. That is not time crucial.

Could I use some sort of thread lock, pause, timer, thingy. To keep the thread running through until “Thread.Sleep(x);” where x is usually 100ms. and only execute the other thread in that time period?

I know that netmf is not a real time OS and is not meant for this type of thing. But we are dealing with 1hz per cycle, with a tolerance of 10ms give or take. So it should be doable right. Worst case scenario would be to pay someone to right an RLP for me. but because I’m switching pins over SPI. That might be really dear or not doable at all?

Thanks in advance.

Could I interrupt the thread with a command from an xbee. Or would waiting for a uart command occupy a thread and hence muk with the timing?

Andre is right with the 20ms slice, but:
As soon as you call Thread.Sleep in a thread, even with 0 as parameter, you hand the control back to the scheduler, and other threads get the remaining time.
The 20ms rule get only into affect if you don’t call sleep or any other wait operation within 20 MS after getting control.

You can suspend / resume threads, uart will only affect threads if you use hardware handshaking.

David got in too fast there. :-[

I come from Netduino land and I’ve never heard of suspending and resuming threads before. Is there a tutorial somewhere?
Can I “Kill” a thread?

1 Like

Suspending and resuming threads is a technique rarely used in .NET (at least by me, I don’t use it at all).
I’m not sure if NETMF supports it.
Generally you have a Suspend() and Resume() method on the Thread object.
Normally I use as ManualResetEvent or AutomaticResetEvent if I want to suspend a thread for an unknown time.
Just create the event, call Wait() inside the thread proc when you want to suspend and Set() from outside to resume the thread.
To “Kill” a thread you simply exit the the thread proc. Nothing else to to.
Yo can cancel a thread from the outside with the Cancel() method, but this might lead to ressources not being freed correctly and you gen an exception somewhere.

1 Like

:slight_smile: thanks a ton for going the extra mile on the details. I really need it.

Slightly orthogonal way to approach stuff like this : get rid of the threading entirely and get rid of the sequential sleeps.

Instead, treat it like a ‘gaming loop’ - a continuous running frame loop that examines the current time and takes actions based on the absolute time or elapsed time.

If you have a series of timed events, you can store them in a sorted queue; examine the first item; sleep until it’s due time; and then execute all due tasks in the queue; and then again sleep. This removes any threading non-determinism. Threading and deterministic execution are always at odds, and generally the solution is to flatten out the algorithm into something iterative and with only zero or one sleep states.

Here’s an example snippet: In this example, every call to ‘Process()’ returns the next time that process wants to run.


        public void Run()
        {
            while (true)
            {
                if (_queue.Count==0)
                {
                    // The queue is empty - wait for something to get inserted
                    _scheduleChangedEvent.WaitOne();
                }
                else
                {
                    var nextTime = ((ScheduleItem)_queue[0]).RunAt;
                    int delay = TimeSpan.FromTicks(nextTime.Ticks - DateTime.UtcNow.Ticks).Milliseconds;
                    if (delay>0)
                        _scheduleChangedEvent.WaitOne(delay, false);
                }

                if (_fShutdown)
                    break;

                // Re-scheduled items get put in a side list (the mergeList) so that re-insertions with an immediate
                //   execution time don't monopolize the schedule. All 'ready' agents get run before re-scheduled agents
                //   even if the re-scheduled time would have passed.
                do
                {
                    DateTime now = DateTime.UtcNow;

                    if (_queue.Count == 0)
                        break;

                    // Examine the head item
                    var item = (ScheduleItem)_queue[0];

                    if (item.RunAt > now)
                        break; // we need to wait a bit

                    try
                    {
                        // Dequeue the head item
                        _queue.RemoveAt(0);
                        // Process it
                        var runAt = item.Target.Process(now);
                        // Re-schedule it in the merge list
                        if (runAt != DateTime.MaxValue)
                            ScheduleNextRun(_mergeList, item.Target, runAt);
                    }
                    catch (Exception exRun)
                    {
                        Debug.Print("An exception ocurred while attempting to run an agent : " + exRun.ToString());

                        // Attempt to recover the agent
                        try
                        {
                            item.Target.Stop();
                            item.RunAt = item.Target.Start();
                            if (item.RunAt != DateTime.MaxValue)
                                this.ScheduleNextRun(_mergeList, item);
                        }
                        catch (Exception ex)
                        {
                            Debug.Print("Exception while trying to recover a faulted agent : " + ex.ToString());
                        }
                    }

                } while (!_fShutdown);

                if (_mergeList.Count > 0)
                    Merge();
            }

            // Execute shutdown code
            // ...omitted from snippet...
        }

3 Likes

Another out-there thought. What about Signal Generator? https://www.ghielectronics.com/downloads/man/Library_Documentation_v4.3/html/T_GHI_IO_SignalGenerator.htm

I think I’m just going to add what’s needed by tripping an external interrupt and suffering degraded timing for a split second. I’m sure there is a better way. But this is what I can realistically do with my skill set for now . :-[

Thanks for all those who chimed in.

@ mcalsyn - So basically functional vs object oriented programming?

@ Mr. John Smith - Not quite. The design is still object oriented (note the use of Agent and ScheduleItem types, with Target and RunAt members)

The change is in the concurrency model - going to a single-threaded cooperative multi-tasking model instead of using OS-based threads.

The key word there is ‘cooperative’. If one of your Agents takes a long time in its Process() member, then that agent is going to screw up the schedule, so this works best where the sum of the execution times of Agents likely to run within a single frame is lower than the shortest frame-to-frame schedule time. Agents can still spin off threads for long-running tasks though, but in that case you’re going to reduce the determinism of the main loop timing.

I do still use threads in cases where the processing duration for a call to Process() is longer than I could stand to delay the scheduling loop, but still shorter than the average run-to-run interval for that agent. And I use a ThreadPool to limit the cost of those spun-off tasks to a finite number of threads.

@ mcalsyn - I really want to try this with my project, but I just can’t bring myself to do it. I keep thinking that it will get stuck in one particular part and never leave, or a failure of one component will bring down all of them.
I considered breaking up my program into sections within a while loop. the loop will run forever with a counter that increments forever (unsigned, unsafe int32). The single thread will execute a different function on each pass


        uint i = 0;
        while(true){
            uint _functionNumber = i % 5;
            switch (_functionNumber) {
                case 0:
                    //do function 0
                    break;
                case 1:
                    //do function 1
                    break;
                case 2:
                    //do function 2
                    break;
                case 3:
                    break;
                case 4:
                    break;
                default:
                    break;
            }
            unsafe {
                _functionNumber++;
            }
        }

What do you think about this method. Each operation to do is in a function that takes no arguments.

The main difference I see there is that my sample maintains a schedule, and yours is ‘free running’ (which is perfectly fine), but they are essentially the same approach.

You can guard against failures by wrapping each function or the switch body in a try/catch. The rational for wrapping individual case blocks is that you may have different recovery strategies for different functions. I think I would do away with the unsafe block and just use a default: case that sets the value back to zero. Then you never have to deal with the overflow issue and you don’t have to maintain the magic number in the mod operator (i % 5).

It’s a great approach. Threads are great for when you really need parallelism, but they get over used (to the detriment of performance and reliability) because they are conceptually simple.

Got some follow questions about locks and threading still.

What is the default priority that a thread starts in?
If a high priority thread spawns another thread. Does that thread inherit a high priority status?

Is it possible to get the state of a lock or the size of the queue? I use lock like the code below.



private static Object Display_Lock = new Object();

private static void Display()
        {
            lock (Display_Lock)
            {
................


Thanks in advance.

@ mcalsyn - Yes, I’ll definitely do that. Simpler is better. How long have you been programming?

@ xrstokes - a new thread should have default priority in any case.
for lock, you can’t get any information about them.
They lock if a different thread runs on an already entered object.
If the same thread comes to an lock with the same object, it does not block!
If you need more control over the behavior, then use the Monitor class directly.
In fact the compiler does nothing else then converting the lock into code using the Monitor class.

The NETMF version of monitor might have not all features.

I feel like such a forum leach lately. It annoys me when people ask for help building a solution with out having a crack first them selves. They are usually rewarded with sarcasm or no reply’s. Please believe me when I say I’ve tried with this problem for days with no luck, I’m just out of my depth here on this project. I believe the answer lays somewhere in either using the monitor class or interlocked. Connected to my project [G120], on SPI2 is an 74hc595 driving a 4X20 Character LCD. Also using SPI2 is an MCP3208 ADC. Both are set up using the MultiSPI class from Stefan’s awesome tool box.


/*
 * Copyright 2011-2014 Stefan Thoolen (http://www.netmftoolbox.com/)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

using System;
using Microsoft.SPOT;
using Microsoft.SPOT.Hardware;


namespace G120.SAL.ToolBox
{
    /// <summary>
    /// SPI Helper to make it easier to use multiple SPI-devices on one SPI-bus
    /// </summary>
    public class MultiSPI
    {
        /// <summary>Reference to the SPI Device. All MultiSPI devices use the same SPI class from the NETMF, so this reference is static</summary>
        private static SPI _SPIDevice;
        /// <summary>SPI Configuration. Different for each device, so not a static reference</summary>
        private SPI.Configuration _Configuration;

        /// <summary>There is a software ChipSelect feature because of a bug. True when enabled</summary>
        /// <remarks>see http://netduino.codeplex.com/workitem/3 for more details about the bug.</remarks>
        private bool _Use_SoftwareCS;
        /// <summary>Reference to the latch-pin when using software chip-select</summary>
        private OutputPort _SoftwareCS;
        /// <summary>Active state when using software chip-select</summary>
        private bool _SoftwareCS_ActiveState;
        /// <summary>Returns the SPI Configuration</summary>
        public SPI.Configuration Config { get { return this._Configuration; } }

        /// <summary>
        /// Initializes a new SPI device
        /// </summary>
        /// <param name="config">The SPI-module configuration</param>
        public MultiSPI(SPI.Configuration config)
        {
            // The timing of the Netduino pin 4, Netduino Plus pin 4 and Netduino Mini pin 13 have a small bug, probably in the IC or NETMF itself.
            //
            // They all refer to the same pin ID on the AT91SAM7X512: (int)12
            // - SecretLabs.NETMF.Hardware.Netduino.Pins.GPIO_PIN_D4
            // - SecretLabs.NETMF.Hardware.NetduinoPlus.Pins.GPIO_PIN_D4
            // - SecretLabs.NETMF.Hardware.NetduinoMini.Pins.GPIO_PIN_13
            //
            // To work around this problem we use a software chip select. A bit slower, but it works.
            // We will include this work-around until the actual bug is fixed.

            bool SoftwareChipSelect = false;

            //if ((int)config.ChipSelect_Port == 12 && (
            //    Tools.HardwareProvider == "Netduino" || Tools.HardwareProvider == "NetduinoMini" || Tools.HardwareProvider == "NetduinoPlus"
            //))
            //{
            //    Debug.Print("MultiSPI: Software ChipSelect enabled to prevent timing issues");
            //    Debug.Print("MultiSPI: See http://netduino.codeplex.com/workitem/3 for more");
            //    SoftwareChipSelect = true;
            //}

            // Sets the configuration in a local value
            this._Configuration = config;

            // When we use a software chipset we need to record some more details
            if (SoftwareChipSelect)
            {
                this._SoftwareCS = new OutputPort(config.ChipSelect_Port, !config.ChipSelect_ActiveState);
                this._SoftwareCS_ActiveState = config.ChipSelect_ActiveState;
                this._Use_SoftwareCS = true;
                // Copies the Configuration, but without Chip Select pin
                this._Configuration = new SPI.Configuration(
                    Cpu.Pin.GPIO_NONE,
                    _Configuration.BusyPin_ActiveState,
                    _Configuration.ChipSelect_SetupTime,
                    _Configuration.ChipSelect_HoldTime,
                    _Configuration.Clock_IdleState,
                    _Configuration.Clock_Edge,
                    _Configuration.Clock_RateKHz,
                    _Configuration.SPI_mod,
                    _Configuration.BusyPin,
                    _Configuration.BusyPin_ActiveState
                );
            }

            // If no SPI Device exists yet, we create it's first instance
            if (_SPIDevice == null)
            {
                // Creates the SPI Device
                _SPIDevice = new SPI(this._Configuration);
            }
        }

        /// <summary>
        /// The 8-bit bytes to write to the SPI-buffer
        /// </summary>
        /// <param name="WriteBuffer">An array of 8-bit bytes</param>
        public void Write(byte[] WriteBuffer)
        {
            if (this._Use_SoftwareCS) this._SoftwareCS.Write(this._SoftwareCS_ActiveState);
            _SPIDevice.Config = this._Configuration;
            _SPIDevice.Write(WriteBuffer);
            if (this._Use_SoftwareCS) this._SoftwareCS.Write(!this._SoftwareCS_ActiveState);
        }

        /// <summary>
        /// The 16-bit bytes to write to the SPI-buffer
        /// </summary>
        /// <param name="WriteBuffer">An array of 16-bit bytes</param>
        public void Write(ushort[] WriteBuffer)
        {
            if (this._Use_SoftwareCS) this._SoftwareCS.Write(this._SoftwareCS_ActiveState);
            _SPIDevice.Config = this._Configuration;
            _SPIDevice.Write(WriteBuffer);
            if (this._Use_SoftwareCS) this._SoftwareCS.Write(!this._SoftwareCS_ActiveState);
        }

        /// <summary>
        /// Reads 8-bit bytes
        /// </summary>
        /// <param name="ReadBuffer">An array with 8-bit bytes to read</param>
        public void Read(byte[] ReadBuffer)
        {
            if (this._Use_SoftwareCS) this._SoftwareCS.Write(this._SoftwareCS_ActiveState);
            _SPIDevice.Config = this._Configuration;
            _SPIDevice.WriteRead(ReadBuffer, ReadBuffer); // First parameter is actually a WriteBuffer
            if (this._Use_SoftwareCS) this._SoftwareCS.Write(!this._SoftwareCS_ActiveState);
        }

        /// <summary>
        /// Reads 16-bit bytes
        /// </summary>
        /// <param name="ReadBuffer">An array with 16-bit bytes to read</param>
        public void Read(ushort[] ReadBuffer)
        {
            if (this._Use_SoftwareCS) this._SoftwareCS.Write(this._SoftwareCS_ActiveState);
            _SPIDevice.Config = this._Configuration;
            _SPIDevice.WriteRead(ReadBuffer, ReadBuffer); // First parameter is actually a WriteBuffer
            if (this._Use_SoftwareCS) this._SoftwareCS.Write(!this._SoftwareCS_ActiveState);
        }

        /// <summary>
        /// Writes an array of 8-bit bytes to the interface, and reads an array of 8-bit bytes from the interface.
        /// </summary>
        /// <param name="WriteBuffer">An array with 8-bit bytes to write</param>
        /// <param name="ReadBuffer">An array with 8-bit bytes to read</param>
        public void WriteRead(byte[] WriteBuffer, byte[] ReadBuffer)
        {
            if (this._Use_SoftwareCS) this._SoftwareCS.Write(this._SoftwareCS_ActiveState);
            _SPIDevice.Config = this._Configuration;
            _SPIDevice.WriteRead(WriteBuffer, ReadBuffer);
            if (this._Use_SoftwareCS) this._SoftwareCS.Write(!this._SoftwareCS_ActiveState);
        }

        /// <summary>
        /// Writes an array of 16-bit bytes to the interface, and reads an array of 16-bit bytes from the interface.
        /// </summary>
        /// <param name="WriteBuffer">An array with 16-bit bytes to write</param>
        /// <param name="ReadBuffer">An array with 16-bit bytes to read</param>
        public void WriteRead(ushort[] WriteBuffer, ushort[] ReadBuffer)
        {
            if (this._Use_SoftwareCS) this._SoftwareCS.Write(this._SoftwareCS_ActiveState);
            _SPIDevice.Config = this._Configuration;
            _SPIDevice.WriteRead(WriteBuffer, ReadBuffer);
            if (this._Use_SoftwareCS) this._SoftwareCS.Write(!this._SoftwareCS_ActiveState);
        }

        /// <summary>
        /// Writes an array of 8-bit bytes to the interface, and reads an array of 8-bit bytes from the interface into a specified location in the read buffer.
        /// </summary>
        /// <param name="WriteBuffer">An array with 8-bit bytes to write</param>
        /// <param name="ReadBuffer">An array with 8-bit bytes to read</param>
        /// <param name="StartReadOffset">The offset in time, measured in transacted elements from writeBuffer, when to start reading back data into readBuffer</param>
        public void WriteRead(byte[] WriteBuffer, byte[] ReadBuffer, int StartReadOffset)
        {
            if (this._Use_SoftwareCS) this._SoftwareCS.Write(this._SoftwareCS_ActiveState);
            _SPIDevice.Config = this._Configuration;
            _SPIDevice.WriteRead(WriteBuffer, ReadBuffer, StartReadOffset);
            if (this._Use_SoftwareCS) this._SoftwareCS.Write(!this._SoftwareCS_ActiveState);
        }

        /// <summary>
        /// Writes an array of 16-bit bytes to the interface, and reads an array of 16-bit bytes from the interface into a specified location in the read buffer.
        /// </summary>
        /// <param name="WriteBuffer">An array with 16-bit bytes to write</param>
        /// <param name="ReadBuffer">An array with 16-bit bytes to read</param>
        /// <param name="StartReadOffset">The offset in time, measured in transacted elements from writeBuffer, when to start reading back data into readBuffer</param>
        public void WriteRead(ushort[] WriteBuffer, ushort[] ReadBuffer, int StartReadOffset)
        {
            if (this._Use_SoftwareCS) this._SoftwareCS.Write(this._SoftwareCS_ActiveState);
            _SPIDevice.Config = this._Configuration;
            _SPIDevice.WriteRead(WriteBuffer, ReadBuffer, StartReadOffset);
            if (this._Use_SoftwareCS) this._SoftwareCS.Write(!this._SoftwareCS_ActiveState);
        }

        /// <summary>
        /// Writes an array of 16-bit bytes to the interface, and reads an array of 16-bit bytes from the interface into a specified location in the read buffer. 
        /// </summary>
        /// <param name="WriteBuffer">An array with 8-bit bytes to write</param>
        /// <param name="ReadBuffer">An array with 8-bit bytes to read</param>
        /// <param name="WriteOffset">The offset in writeBuffer to start write data from</param>
        /// <param name="WriteCount">The number of elements in writeBuffer to write</param>
        /// <param name="ReadOffset">The offset in readBuffer to start read data from</param>
        /// <param name="ReadCount">The number of elements in readBuffer to fill</param>
        /// <param name="StartReadOffset">The offset in time, measured in transacted elements from writeBuffer, when to start reading back data into readBuffer</param>
        public void WriteRead(byte[] WriteBuffer, int WriteOffset, int WriteCount, byte[] ReadBuffer, int ReadOffset, int ReadCount, int StartReadOffset)
        {
            if (this._Use_SoftwareCS) this._SoftwareCS.Write(this._SoftwareCS_ActiveState);
            _SPIDevice.Config = this._Configuration;
            _SPIDevice.WriteRead(WriteBuffer, WriteOffset, WriteCount, ReadBuffer, ReadOffset, ReadCount, StartReadOffset);
            if (this._Use_SoftwareCS) this._SoftwareCS.Write(!this._SoftwareCS_ActiveState);
        }

        /// <summary>
        /// Writes an array of 16-bit bytes to the interface, and reads an array of 16-bit bytes from the interface into a specified location in the read buffer. 
        /// </summary>
        /// <param name="WriteBuffer">An array with 16-bit bytes to write</param>
        /// <param name="ReadBuffer">An array with 16-bit bytes to read</param>
        /// <param name="WriteOffset">The offset in writeBuffer to start write data from</param>
        /// <param name="WriteCount">The number of elements in writeBuffer to write</param>
        /// <param name="ReadOffset">The offset in readBuffer to start read data from</param>
        /// <param name="ReadCount">The number of elements in readBuffer to fill</param>
        /// <param name="StartReadOffset">The offset in time, measured in transacted elements from writeBuffer, when to start reading back data into readBuffer</param>
        public void WriteRead(ushort[] WriteBuffer, int WriteOffset, int WriteCount, ushort[] ReadBuffer, int ReadOffset, int ReadCount, int StartReadOffset)
        {
            if (this._Use_SoftwareCS) this._SoftwareCS.Write(this._SoftwareCS_ActiveState);
            _SPIDevice.Config = this._Configuration;
            _SPIDevice.WriteRead(WriteBuffer, WriteOffset, WriteCount, ReadBuffer, ReadOffset, ReadCount, StartReadOffset);
            if (this._Use_SoftwareCS) this._SoftwareCS.Write(!this._SoftwareCS_ActiveState);
        }
    }
}

As long as I don’t try to read the ADC whilst updating the display, all is well. But that is what I need to do. I need thread 1 to be updating the display, whilst thread 2 might need to scan the ADC. The fist thing I tried was having a public bool called “Is_Displaying”. I got the displaying thread to hold the bool until the display was updated and return it so the ADC thread could be run. This was a failure of course as it seemed like they could both change the bool at the same time? Then I used locking. Locking actually works to stop the collision of the threads on the spi bus, but it does not suit my project because it holds up thread2 which has other duties that need preforming.

easier solution;
If I could create a code that read the state of the lock, and skipped over trying to read the ADC in the case that the spi was in use. I could live with that. The ADC data wouldn’t be updated but would be a few ms old. that’s ok. It could be updated once the display update was complete.

harder but better solution;
I have quite a crowded little spi_bus going on here. would It be possible to add a priority number to the MultiSPI class? so that every device I added using the constructor[I think that’s the correct term]. I could add a number for a priority. Because the display should always wait for the ADC, even if it is halfway through updating?

If this is to much too ask over the forums but someone would like to offer their services for a small fee. Please PM me.

Thanks

Sounds like you’re awfully close to a solution in option 1. Just create an object and then use lock() on that object to protect you from doing the ADC read when the LCD is updating, this way the bool will never be updated in two places (not that you really need the bool anymore).

I think the lock doesn’t work for him because he wants that thread to keep running to do other work. If the thread stops on a lock(), then the other work gets delayed.

If you want to create a soft lock object (option 1) for the SPI between two devices, then use Interlocked.CompareExchange(). This is the test-then-lock that you are looking for. If you do Interlocked.CompareExchange(ref _iIsLocked, 1, 0) and it returns ‘0’, then you are the new owner of the _iIsLocked variable (and you can use the SPI). If it comes back ‘1’, then somebody else was using it already. When you are done, do Interlocked.Exchange(ref _iIsLocked, 0) to indicate that you are done. _iIsLocked should be an integer that is initialized to 0.

The Interlocked operations are atomic and multi-thread safe. You can create priority stacks with Interlocked too, so that you can create a hierarchy of locks. You can use the Interlocked methods to create exactly the kind of soft lock you are describing.

System.Threading.Interlocked docs : [url]Microsoft Learn: Build skills that open doors in your career

2 Likes