C
C#4mo ago
Mayka

Properly locking the console in async code

I made an async progress bar which sits at the bottom of the console. It still needs tweaking, but I’m running into issues truly locking the console during the specific moment that the progress bar is being written to the bottom of the screen. In order to restore the initial cursor position after updating the progress bar, I’m storing the cursor position before it to the bottom of the console. However, it seems that the console is still able to be written to during the “locked” portion of the task. This causes lines to be overwritten, because the stored cursor position is no longer accurate. How do I force a true lock in this case?
using ProgressBar;

Task task = Task.Run(async () =>
{
   ConsoleProgressBar progressBar = new(100);

   for (int i = 0; i < 100; i++)
   {
       await progressBar.IncrementAsync();
       Thread.Sleep(50);
   }
});

for (int i = 0; i <= 20; i++)
{
   Thread.Sleep(100);
   Console.WriteLine($"Message {i}");
}

await task;
Console.ReadKey();

namespace ProgressBar
{
   internal class ConsoleProgressBar(int total = 100)
   {
       private static readonly SemaphoreSlim SemaphoreSlim = new(1, 1);

       private int _current;
       private int _progressBarCursorRow = Console.WindowHeight - 1;

       public async Task IncrementAsync(int? value = null)
       {
           await SemaphoreSlim.WaitAsync();

           try
           {
               await Task.Run(() => IncrementProgressBar(value));
           }
           finally
           {
               SemaphoreSlim.Release();
           }
       }

       private void IncrementProgressBar(int? value)
       {
           int incrementValue = value ?? 1;
           _current += incrementValue;

           (int cursorLeft, int cursorTop) = Console.GetCursorPosition();
           Console.SetCursorPosition(0, _progressBarCursorRow);
           Console.Write(new string(' ', Console.BufferWidth - 1));
           Console.SetCursorPosition(cursorLeft, cursorTop);

           if (cursorTop < _progressBarCursorRow)
           {
               Console.SetCursorPosition(0, Console.WindowHeight - 1);
           }
           else
           {
               Console.SetCursorPosition(0, cursorTop + 1);
           }

           _progressBarCursorRow = Console.CursorTop;

           Draw();
           Console.SetCursorPosition(cursorLeft, cursorTop);
       }

       private void Draw()
       {
           Console.Write("[");

           int percent = (int)(_current * 100 / total);
           int p = (int)((percent / 10f) + .5f);

           for (int i = 0; i < 10; ++i)
           {
               Console.Write(i >= p ? ' ' : '■');
           }

           Console.Write("] {0,3:##0}%", percent);
       }
   }
}
using ProgressBar;

Task task = Task.Run(async () =>
{
   ConsoleProgressBar progressBar = new(100);

   for (int i = 0; i < 100; i++)
   {
       await progressBar.IncrementAsync();
       Thread.Sleep(50);
   }
});

for (int i = 0; i <= 20; i++)
{
   Thread.Sleep(100);
   Console.WriteLine($"Message {i}");
}

await task;
Console.ReadKey();

namespace ProgressBar
{
   internal class ConsoleProgressBar(int total = 100)
   {
       private static readonly SemaphoreSlim SemaphoreSlim = new(1, 1);

       private int _current;
       private int _progressBarCursorRow = Console.WindowHeight - 1;

       public async Task IncrementAsync(int? value = null)
       {
           await SemaphoreSlim.WaitAsync();

           try
           {
               await Task.Run(() => IncrementProgressBar(value));
           }
           finally
           {
               SemaphoreSlim.Release();
           }
       }

       private void IncrementProgressBar(int? value)
       {
           int incrementValue = value ?? 1;
           _current += incrementValue;

           (int cursorLeft, int cursorTop) = Console.GetCursorPosition();
           Console.SetCursorPosition(0, _progressBarCursorRow);
           Console.Write(new string(' ', Console.BufferWidth - 1));
           Console.SetCursorPosition(cursorLeft, cursorTop);

           if (cursorTop < _progressBarCursorRow)
           {
               Console.SetCursorPosition(0, Console.WindowHeight - 1);
           }
           else
           {
               Console.SetCursorPosition(0, cursorTop + 1);
           }

           _progressBarCursorRow = Console.CursorTop;

           Draw();
           Console.SetCursorPosition(cursorLeft, cursorTop);
       }

       private void Draw()
       {
           Console.Write("[");

           int percent = (int)(_current * 100 / total);
           int p = (int)((percent / 10f) + .5f);

           for (int i = 0; i < 10; ++i)
           {
               Console.Write(i >= p ? ' ' : '■');
           }

           Console.Write("] {0,3:##0}%", percent);
       }
   }
}
12 Replies
mtreit
mtreit4mo ago
I don't see where you're "locking" the console at all. Also I don't know why this would be async.
Mayka
Mayka4mo ago
@mtreit I was trying to add a lock with this, maybe that’s not the correct way?
public async Task IncrementAsync(int? value = null)
{
await SemaphoreSlim.WaitAsync();

try
    {
    await Task.Run(() => IncrementProgressBar(value));
}
    finally
    {
    SemaphoreSlim.Release();
}
}
public async Task IncrementAsync(int? value = null)
{
await SemaphoreSlim.WaitAsync();

try
    {
    await Task.Run(() => IncrementProgressBar(value));
}
    finally
    {
    SemaphoreSlim.Release();
}
}
Trying to make it async so that I can have a progress bar that updates on a task running asynchronously while other things may be running and outputting to the console at the same time. Hence trying to make it just a bar at the bottom.
mtreit
mtreit4mo ago
That will in theory prevent other code from calling IncrementProgressBar until they acquire the semaphore. It doesn't prevent at all other code from writing to the console.
Mayka
Mayka4mo ago
@mtreit ohhhhhhhh so I was thinking of it backwards, then. Thank you for the clarification! Sounds like perhaps I need to make a wrapper function for writing to the console that uses a semaphore slim and utilize that for any writing to console?
mtreit
mtreit4mo ago
That would be an approach that is much more likely to work, yes.
Mayka
Mayka4mo ago
Thank you! That really clears things up. I’m quite new to async code in general, so sometimes I get things a bit jumbled up.
mtreit
mtreit4mo ago
I'm not sure why you are using async at all to be honest. Unless it's just for learning purposes.
Mayka
Mayka4mo ago
@mtreit no it is not for learning purposes. i may no longer need it to be asynchronous now that I see I had the locking backwards… I’ll see what it ends up looking like when i rework this. @mtreit I think I have something working now, thanks to your pointers. Found out I can lock on Console.Out directly, which made things far simpler. Apparently with the move to .NET Core this feature got originally removed in favor of private locking objects, but it was added back in due to the common use case of needing to change the console color in a thread-safe manner. I’m sure this could be more efficient, but it works for now at least.
using ProgressBar;
using System.Text;

ThreadSafeConsole.ReadLine();

Task task = Task.Run(async () =>
{
ConsoleProgressBar progressBar = new(100);

for (int i = 0; i < 100; i++)
{
progressBar.Increment();
await Task.Delay(50);
}
});

for (int i = 0; i <= 30; i++)
{
Thread.Sleep(100);
ThreadSafeConsole.WriteLine($"Message {i}");
}

await task;
Console.ReadKey();

namespace ProgressBar
{
internal static class ThreadSafeConsole
{
public static void Write(string message)
{
lock (Console.Out)
{
Console.Write(message);
}
}

public static void WriteLine(string message)
{
lock (Console.Out)
{
Console.WriteLine(message);
}
}

public static int Read()
{
int result;
lock (Console.Out)
{
result = Console.Read();
}
return result;
}

public static ConsoleKeyInfo ReadKey()
{
ConsoleKeyInfo result;
lock (Console.Out)
{
result = Console.ReadKey();
}
return result;
}

public static string? ReadLine()
{
string? result;
lock (Console.Out)
{
result = Console.ReadLine();
}
return result;
}
}

internal class ConsoleProgressBar(int total = 100)
{
private int? _progressBarCursorRow;
private int _current;

public void Increment(int value = 1)
{
lock (Console.Out)
{
bool isCursorVisible = Console.CursorVisible;
Console.CursorVisible = false;
_current += value;

if (_progressBarCursorRow == null)
{
_progressBarCursorRow = Console.CursorTop;
Console.WriteLine();
}

(int cursorLeft, int cursorTop) = Console.GetCursorPosition();
Console.SetCursorPosition(0, (int)_progressBarCursorRow);
Console.Write(new string(' ', Console.BufferWidth - 1));

Console.CursorLeft = 0;
Console.Write(this);
Console.SetCursorPosition(cursorLeft, cursorTop);

Console.CursorVisible = isCursorVisible;
}
}

public override string ToString()
{
StringBuilder stringBuilder = new();
stringBuilder.Append('[');

int percent = (int)(_current * 100 / total);
int p = (int)((percent / 10f) + .5f);

for (int i = 0; i < 10; ++i)
{
stringBuilder.Append(i >= p ? ' ' : '■');
}

stringBuilder.Append($"] {percent,3:##0}%");
return stringBuilder.ToString();
}
}
}
using ProgressBar;
using System.Text;

ThreadSafeConsole.ReadLine();

Task task = Task.Run(async () =>
{
ConsoleProgressBar progressBar = new(100);

for (int i = 0; i < 100; i++)
{
progressBar.Increment();
await Task.Delay(50);
}
});

for (int i = 0; i <= 30; i++)
{
Thread.Sleep(100);
ThreadSafeConsole.WriteLine($"Message {i}");
}

await task;
Console.ReadKey();

namespace ProgressBar
{
internal static class ThreadSafeConsole
{
public static void Write(string message)
{
lock (Console.Out)
{
Console.Write(message);
}
}

public static void WriteLine(string message)
{
lock (Console.Out)
{
Console.WriteLine(message);
}
}

public static int Read()
{
int result;
lock (Console.Out)
{
result = Console.Read();
}
return result;
}

public static ConsoleKeyInfo ReadKey()
{
ConsoleKeyInfo result;
lock (Console.Out)
{
result = Console.ReadKey();
}
return result;
}

public static string? ReadLine()
{
string? result;
lock (Console.Out)
{
result = Console.ReadLine();
}
return result;
}
}

internal class ConsoleProgressBar(int total = 100)
{
private int? _progressBarCursorRow;
private int _current;

public void Increment(int value = 1)
{
lock (Console.Out)
{
bool isCursorVisible = Console.CursorVisible;
Console.CursorVisible = false;
_current += value;

if (_progressBarCursorRow == null)
{
_progressBarCursorRow = Console.CursorTop;
Console.WriteLine();
}

(int cursorLeft, int cursorTop) = Console.GetCursorPosition();
Console.SetCursorPosition(0, (int)_progressBarCursorRow);
Console.Write(new string(' ', Console.BufferWidth - 1));

Console.CursorLeft = 0;
Console.Write(this);
Console.SetCursorPosition(cursorLeft, cursorTop);

Console.CursorVisible = isCursorVisible;
}
}

public override string ToString()
{
StringBuilder stringBuilder = new();
stringBuilder.Append('[');

int percent = (int)(_current * 100 / total);
int p = (int)((percent / 10f) + .5f);

for (int i = 0; i < 10; ++i)
{
stringBuilder.Append(i >= p ? ' ' : '■');
}

stringBuilder.Append($"] {percent,3:##0}%");
return stringBuilder.ToString();
}
}
}
cap5lut
cap5lut4mo ago
hey, not sure if u r writing this for the learning effect or if u just didnt know that there are libraries for such out there. so i wanted to inform ya about $spectre console just in case u didnt know about it, its an awesome textual user interface library
MODiX
MODiX4mo ago
Spectre.Console is a .NET library that allows for easy creation of console UIs, text formatting in the console, and command-line argument parsing. https://spectreconsole.net/
Spectre.Console - Welcome!
Spectre.Console is a .NET library that makes it easier to create beautiful console applications.
cap5lut
cap5lut4mo ago
it comes with a lot of interactive stuff as well
Mayka
Mayka4mo ago
@cap5lut I did not know about Spectre.Console, that’s awesome info thanks! It doesn’t sound like their progress bar will work for my particular use case, per the below, but I may use some of the other features they offer.
The progress display is not thread safe, and using it together with other interactive components such as prompts, status displays or other progress displays are not supported.