Unassgined variable in scope disposed upon sleep?

Not that i would ever need or promote coding like this, but Im trying to figure out what is it about the TinyCLR environment might cause the following unpredictable behavior.

Below i declare 2 timers, assigning only one.

After several minutes I will find the unassigned timer will stop triggering the callback, while the assigned continues to run as expected.

internal static class Program
{
	private static void Main()
	{
		var pwmController1 = PwmController.FromName(SC13048.Timer.Pwm.Controller1.Id);
		var statusLamp = new LampController(pwmController1, pwmController1.OpenChannel(SC13048.Timer.Pwm.Controller1.PA8));

		var v = new Timer(_ =>
		{
			statusLamp.SetMode(LampController.Mode.Off);
		}, null, TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(5));

		new Timer(_ =>
		{
			statusLamp.SetMode(LampController.Mode.On, 30);
		}, null, TimeSpan.FromSeconds(2), TimeSpan.FromSeconds(2));

		Thread.Sleep(Timeout.Infinite);
	}
}

I can speculate that something about TinyCLR seeing during sleep the timer unassigned and therefore unused so it cleans up, but it just seems so arbitrary that it happens a few minutes into the execution.

Fwiw I would argue that the variable is still in scope, especially as the thread was only put to sleep rather than exited somehow. And again while not recommending it, ive done this on windows where an unassigned timer would remain functional after its scope has otherwise executed, and the object doesn’t seem to be disposed of.

As usual i am running in debug if that matters.

Your second timer has been garbage collected because there is no ref to it that can be traced back to the stack or a static. If you force a GC before the sleep, it should stop firing immediately.

The reason that it is taking a while is because there is low or no memory pressure, so GC’s aren’t firing aggressively.

There is no ‘variable’ that is in scope for the second ‘new’. The ‘new’ operation completed and left a result on the stack, and with no assignment op, that result was popped and discarded on return from the ‘new’ - so there’s no ref left to keep that result around. Again, the delay is because no GC was needed yet, though a routine housekeeping occurred somewhere down the line.

Thanks again for the detailed explanation. So, it might be wise to force GC before sleep to avoid any unwarranted mistakes/behavior.

Like i said, i never noticed this before, but most likely wasnt under any resource pressure to act

no, the wise thing to do would be to assign the resource. Forcing a GC will only sometimes help you identify (early) if you have bad code…

I think with the info presented here we can all agree assigning objects you want to keep in scope is necessary.

But unless your sleep/wake period is so tight that adding unneeded GC is counterproductive, what could then make it unwise to force GC before sleep? Even if it does only sometimes exposes bad code.

So then I have a related question then - I have several times I use a fire-and-forget thread to accomplish a task in parallel-

new Thread(DoAThing).Start();

Will this thread ever get GC’d and the code would stop running before the thread completes?

Threads are special and are not collected by GC.

Gus

Could you please elaborate on what you said?

Are you saying only thread objects are cleared by another mechanism, while objects created by a thread are cleared by GC? Or both threads and objects created by threads use another mechanism?

A thread object is used internally (referenced) in the system itself. It will not be garbage collected until it had excited/terminated and therefore is not being referenced internally.

Other objects inside a thread are treated just like objects in the main program. By the way, the main program is a thread that is create by the system on power up to spin main().

I believe this is the same on the PC as it is on TinyCLR.

I was not commenting on whether it’s a good practice or not to force GC. I was simply saying that this is not a robust approach to address what you asked:

It will not show this, It might, or it might not, or you might not see the success this strategy brings unless some other factor kicks in as well; basically, it’s non-determinate so relying on it would not bode well imho.

I certainly should have clarified the statement better.

Rather than asking:

might be wise to force GC before sleep to avoid any unwarranted mistakes/behavior.

I should have asked:

might it be wise to force GC before sleep to help avoid certain unintended behavior.

If a tight sleep/wake cycle isnt an issue, i still dont see a reason not to consistently force GC before sleep.
Thats really all im trying to learn/figure out here.

OK, to be clear, there’s no reason NOT to use GC - those calls are provided so that you can force the GC to happen, and memory to be reclaimed and reorganised. It’s just that I believe you’re thinking about it from an inverse point - it won’t guarantee to show you whether you have an error in your code or not. And doing it prior to, or after, a sleep won’t make it any more reliable in terms of identifying an issue - if the null referenced item that gets cleaned up by the GC is only ever touched in a certain situation, your testing can still never show the code problem unless you have explicitly tested that situation.

GC forces the memory management to be more organised. Yes, it takes time, but that likely results in less ad-hoc GC and this can lead to a more deterministic execution time when you need to allocate memory (and you’re under memory pressure for that allocation). If you have a well optimised codebase, that doesn’t allocate big chunks dynamically (for example, not something that is processing strings from a serial port), then repeatedly doing GC is overkill. If on the other hand your code does do string manipulation and needs to do so dynamically, then forcing GC after a cycle may make perfect sense so that the next set of allocations that happen won’t accidentally trigger GC at a critical time.

I’m just trying to point out the reason why your comments are not universally applicable - you have the right knowledge of why your initial problem occurred, and what you can do to stop that from happening in the future. In your situation, forcing a GC periodically may have no adverse side effects. but that’s not the same for everyone’s situation

Threads should have a restart function

Yup. Threads are considered ‘referenced’ by the scheduler, so long as they don’t exit. True in both the TinyCLR interpreter and all the other .net runtimes.

Aborted threads are not restartable.

You can suspend and resume a thread.

Why would you ever need a restart function? Restart after what?

Because when you have a thread that does some timed work (do this for 20 seconds, do that for 20 seconds etc.) its getting hard to track and i end up just suspending, aborting, terminating… and then spinning up an new thread just to start it again. Do that for few dozen times and it sometimes don’t get collected and it ends in memory overflow and watchdog reboot.

can you just have a single thread that runs in perpetuity with an if statement sandwiched inside?
while(true) if(bool run) { //do stuff }

Then just use a timer to turn run on and off?

What I use is a main loop and a schedule of ‘agent’ tasks. Each agent reports when it next needs to run. The main loop sleeps until the soonest agent needing cycles. It then runs that agent (in the context of the scheduler thread - not a new thread) and the agent returns a value indicating when it next needs to run. That agent is re-inserted into the queue of waiting agents.

What this does is ensures that we sleep as much as possible for energy efficiency; get more deterministic run times; and avoid multiple threads entering the ‘ready’ state (which causes task switching).

Threads are occasionally necessary, of course, but in most code they seem to be used as a programming crutch. Over-use of threads is also a key source of memory pressure, as thread context data structures are quite large, and energy inefficiency since task switching burns a lot of extra cycles.

I’d be interesting to see an example template of such a program’s architecture :thinking:

1 Like

I’ll go ahead and extract it into a standalone nuget and publish the source on Github. My ability to do that might be limited in the near term. I am out of the country at the moment and separated from all my lovely GHI toys. I was supposed to be back in Oct, but now will be in Europe until December. But in any case, I will publish at least the core code asap, even if I can’t build out a full example.

In the meantime, to whet your appetite, here’s some of the interfaces:

    public interface INodeEngine {
        void ScheduleNextRun(IAgent agent, DateTime runAt);
        void Run();
        void Stop();
        void SetEngineState(string engineState);
        IDriver FindDriver(Type driverType);
    }

    public interface IAgent {
        /// <summary>
        /// Initialize internal start and prepare for process calls
        /// </summary>
        DateTime Start();

        /// <summary>
        /// Perform one cycle of processing. Return a value representing when this agent wants to be processed again
        /// </summary>
        /// <returns>Time this agent next needs processing</returns>
        DateTime Process(DateTime now);

        /// <summary>
        /// Clean up any resources created in 'start'
        /// </summary>
        void Stop();
    }

‘engineState’ refers to the fact that you might want to run different sets of agents during first-run or wifi provisioning than you would during normal runtime operation, so you can change engine states and agents will get spun up or shut down using a least-flow algorithm.

And here is the core scheduling algorithm:

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

                if (this.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 {
                    var now = DateTime.UtcNow;

                    if (this.queue.Count == 0)
                        break;

                    // Examine the head item
                    var item = (ScheduleItem)this.queue[0];

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

                    try {
                        // Dequeue the head item
                        this.queue.RemoveAt(0);
                        // Process it
                        var runAt = item.Target.Process(now);
                        // Re-schedule it in the merge list
                        if (runAt != DateTime.MaxValue)
                            this.ScheduleNextRun(this.mergeList, item.Target, runAt);
                    }
                    catch (Exception exRun) {
                        Debug.WriteLine("An exception ocurred while attempting to run an agent : " + exRun.ToString());
                        this.Logger?.LogEvent(EventClass.OperationalEvent, new ExceptionEvent(EventSeverity.Error, exRun));

                        // Attempt to recover the agent
                        try {
                            item.Target.Stop();
                            item.RunAt = item.Target.Start();
                            if (item.RunAt != DateTime.MaxValue)
                                this.ScheduleNextRun(this.mergeList, item);
                        }
                        catch (Exception ex) {
                            Debug.WriteLine("Exception while trying to recover a faulted agent : " + ex.ToString());
                            this.Logger?.LogEvent(EventClass.OperationalEvent, new ExceptionEvent(EventSeverity.Error, ex));
                        }
                    }

                } while (!this.fShutdown);

                if (this.mergeList.Count > 0)
                    this.Merge();
            }

            // Execute shutdown code
            foreach (var agent in this.agents) {
                try {
                    agent.Stop();
                }
                catch (Exception exAgent) {
                    Debug.WriteLine("Exception during agent stop : " + exAgent);
                    this.Logger?.LogEvent(EventClass.OperationalEvent, new ExceptionEvent(EventSeverity.Error, exAgent));
                }
            }

            foreach (var driver in this.drivers) {
                try {
                    driver.Stop();
                }
                catch (Exception exDriver) {
                    Debug.WriteLine("Exception during driver stop : " + exDriver);
                    this.Logger?.LogEvent(EventClass.OperationalEvent, new ExceptionEvent(EventSeverity.Error, exDriver));
                }
            }
        }
1 Like