Click here to Skip to main content
15,076,740 members
Articles / Programming Languages / C#
Tip/Trick
Posted 16 Jun 2021

Tagged as

Stats

5.4K views
3 bookmarked

How to Action a Spinner in a Console Application

Rate me:
Please Sign up or sign in to vote.
4.00/5 (2 votes)
16 Jun 2021CPOL2 min read
How to add a Spinner to a long-running library method
In this tip, you will learn how to use a generic method to add a spinner to a library method running in a Console application.

Introduction

A spinner is used in conjunction with a time-consuming silent method to give an indication that work is progressing and that the application has not stalled. Adding a spinner to the source code is straight forward, it's just a question of making sure that the marathon method outputs some sort of active display as it progresses. It's not quite so simple when the method forms part of a sealed library as it's necessary to implement a degree of thread management between the library method running on one thread and the spinner running on a different thread.

A Spinner Class

One way that spinner functionality can be implemented is to instantiate a class that actions a new thread in its Start method and runs some sort of active display on that thread until the long running method completes. Here’s an example:

C#
public class Spinner
 {
     private readonly int delay;
     private bool isRunning = false;
     private Thread thread;
     public Spinner(int delay = 25)
     {
         this.delay = delay;
     }

     public void Start()
     {
         if (!isRunning)
         {
             isRunning = true;
             thread = new Thread(Spin);
             thread.Start();
         }
     }
     public void Stop()
     {
         isRunning = false;
     }

     private void Spin()
     {
         while (isRunning)
         {
             Console.Write('.');
             Thread.Sleep(delay);
         }
     }
 }

It can be used like this:

C#
public class Program
{
   static void Main()
    {
        int lifeValue=42;
        var spinner = new Spinner();
        spinner.Start();
        int meaningOfLife = LongLifeMethod(lifeValue);
        spinner.Stop();
        Console.WriteLine($"\r\nThe meaning of life is {meaningOfLife}");
        Console.ReadLine();
    }

   private static int LongLifeMethod(int lifeValue)
    {
        Thread.Sleep(3000);
        return lifeValue;
    }
}

There’s a hidden gotcha here. If the marathon method throws an exception, the Stop method will not be run. So it’s best to call the Stop method from inside a finally block to make sure that the spinner always stops spinning. But this adds a bit more complexity to the code to the extent that the Spinner class now looks like it needs a spinner service to manage it. There must be an easier way.

An Alternative Approach

The trick here is to use a method that's self-determining so that it switches itself off and can be used on a fire and forget basis. The Task-based Asynchronous Pattern can encapsulate all the required functionality within a single generic method.

C#
public static TResult RunWithSpinner<TResult>
(Func<TResult> longFunc, Action<CancellationToken> spinner)
 {
     CancellationTokenSource cts = new();
     //run the spinner on its own thread
     Task.Run(() => spinner(cts.Token));
     TResult result;
     try
     {
         result = longFunc();
     }
     finally
     {
         //cancel the spinner
         cts.Cancel();
     }
     return result;
 }

Using the Method

The method is used like this:

C#
static void Main()
{
    int lifeValue = 42;
    int meaningOfLife = RunWithSpinner(() => LongLifeMethod(lifeValue), Spin);
    Console.WriteLine($"\r\nThe meaning of life is {meaningOfLife}");
}
private static void Spin(CancellationToken token)
{
    while (!token.IsCancellationRequested)
    {
        Console.Write('.');
        Thread.Sleep(25);
    }
}

The longFunc parameter of the RunWithSpinner method is expressed as a lambda expression. The empty brackets on the left side of the => characters signify that the required method’s signature has no parameters and the call to LongLifeMethod leads the compiler to infer that the returned value is that method’s return value. So, at compile time, it will compile the lambda expression into an anonymous function that calls the LongLifeMethod and returns an int. Although the function itself does not take any parameters, it calls the LongLifeMethod and uses the captured variable, lifeValue, as a parameter for that method. The technique of using captured variables in this manner is very powerful and is commonly used in Linq expressions.

Conclusion

Generic methods can be useful for encapsulating, in a few lines of code, the kind of functionality that usually requires a class instance. In this case, the RunWithSpinner method removes the need for a Spinner class along with the code that's needed to manage it.

History

  • 16th June, 2021: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

George Swan
Student
Wales Wales
No Biography provided

Comments and Discussions

 
QuestionMisunderstanding of try/finally and object lifetime Pin
Stacy Dudovitz19-Jun-21 22:07
professionalStacy Dudovitz19-Jun-21 22:07 
AnswerRe: Misunderstanding of try/finally and object lifetime Pin
George Swan20-Jun-21 1:52
MemberGeorge Swan20-Jun-21 1:52 
GeneralRe: Misunderstanding of try/finally and object lifetime Pin
Stacy Dudovitz21-Jun-21 8:37
professionalStacy Dudovitz21-Jun-21 8:37 
With regards to finally not executing, I would point you to this article in StackOverflow:

try catch - Does the C# "finally" block ALWAYS execute? - Stack Overflow

The important takeaway for your example should be this:

Quote:

It is not totally true that finally will always be executed. See this answer from Haacked:

Two possibilities:

StackOverflowException
ExecutingEngineException

The finally block will not be executed when there's a StackOverflowException since there's no room on the stack to even execute any more code. It will also not be called when there's an ExecutingEngineException, which is very rare.

In fact, for any sort of asynchronous exception (like StackOverflowException, OutOfMemoryException, ThreadAbortException) the execution of a finally block is not guaranteed.

However, these exceptions are exceptions you usually cannot recover from, and in most cases your process will exit anyway.

In fact, there is also at least one other case where finally is not executed as described by Brian Rasmussen in a now deleted question:

The other case I am aware of is if a finalizer throws an exception. In that case the process is terminated immediately as well, and thus the guarantee doesn't apply.


The problem is that you are trying to present a general case whereby you have no idea how the caller will be presenting the labmda that will be called here:

C#
CancellationTokenSource cts = new();
     //run the spinner on its own thread
     Task.Run(() => spinner(cts.Token));
     TResult result;
     try
     {
         result = longFunc();
     }
     finally
     {
         //cancel the spinner
         cts.Cancel();
     }

The whole purpose of a Cancellation token is to allow spinner(...) to periodically check and set a cancellation flag, and optionally set a lambda to run on completion. This code, however, does not address synchronization contexts or other threading issues... in short this could leak memory at best, deadlock the system at worst. It also does not take into account that the console app in question may not be, in fact, running on a console... it could be in a remote desktop, a virtual machine, etc.

I think what I am trying to get it is that when you leave the simple confines of simply sending output to standout out or standard err, where these are known pipes that will be handled gracefully, and you start to make assumptions about threading and memory, you enter a whole other world where you application no longer behaves as a good citizen. Gone are the simple days of writing easy console apps and ANSI escapes sequences... and for good reason. IT admins need tight control over the behavior of such apps, and while you are trying to create a simple example, these examples quickly become production code that becomes someone else's domain. Better to fix it at the source and make the sample bullet proof.

That's all I will add to this, your mileage may vary.
GeneralRe: Misunderstanding of try/finally and object lifetime Pin
George Swan21-Jun-21 12:14
MemberGeorge Swan21-Jun-21 12:14 
SuggestionMisleading article title Pin
Stylianos Polychroniadis17-Jun-21 4:47
MemberStylianos Polychroniadis17-Jun-21 4:47 
GeneralRe: Misleading article title Pin
George Swan17-Jun-21 5:23
MemberGeorge Swan17-Jun-21 5:23 
GeneralRe: Misleading article title Pin
Gary R. Wheeler21-Jun-21 11:19
MemberGary R. Wheeler21-Jun-21 11:19 
GeneralRe: Misleading article title Pin
George Swan21-Jun-21 21:43
MemberGeorge Swan21-Jun-21 21:43 
GeneralRe: Misleading article title Pin
sx200822-Jun-21 11:15
Membersx200822-Jun-21 11:15 
GeneralRe: Misleading article title Pin
George Swan22-Jun-21 13:05
MemberGeorge Swan22-Jun-21 13:05 
GeneralRe: Misleading article title Pin
Stylianos Polychroniadis22-Jun-21 23:01
MemberStylianos Polychroniadis22-Jun-21 23:01 
GeneralRe: Misleading article title Pin
George Swan23-Jun-21 2:04
MemberGeorge Swan23-Jun-21 2:04 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.