Skip to content

Instantly share code, notes, and snippets.

@michel-pi
Last active October 15, 2023 11:41
Show Gist options
  • Save michel-pi/cc7a54a766dc730d7bcef14cebc7e4b5 to your computer and use it in GitHub Desktop.
Save michel-pi/cc7a54a766dc730d7bcef14cebc7e4b5 to your computer and use it in GitHub Desktop.
Best practise thread start, stop, pause, resume and join.
/*
* Best practise thread start, stop, pause, resume and join.
* Example code at the end of the file.
*/
using System;
using System.Globalization;
using System.Threading;
namespace Snippets.Threading
{
/// <summary>
/// Executes an operation on a seperate thread.
/// </summary>
public abstract class WorkerThread
{
/// <inheritdoc cref="Timeout.Infinite" />
public const int TimeoutInfinite = Timeout.Infinite;
private readonly object _lock = new();
private readonly AutoResetEvent _pauseEvent;
private readonly AutoResetEvent _resumeEvent;
private readonly AutoResetEvent _stopEvent;
private readonly AutoResetEvent _waitEvent;
private volatile bool _isPaused;
private volatile bool _isRunning;
private Thread? _thread;
/// <summary>
/// Determines whether the thread is currently paused.
/// </summary>
public bool IsPaused => _isPaused;
/// <summary>
/// Determines whether the thread is currently running.
/// </summary>
public bool IsRunning => _isRunning;
/// <summary>
/// Gets or sets the apartment state of this thread. Can only be set when the thread is not running.
/// </summary>
public ApartmentState ApartmentState { get; set; }
/// <summary>
/// Gets or sets the culture for the current thread. Can only be set when the thread is not running.
/// </summary>
public CultureInfo Culture { get; set; }
/// <summary>
/// Gets or sets the current culture used by the Resource Manager to look up culture-specific resources at run time. Can only be set when the thread is not running.
/// </summary>
public CultureInfo UICulture { get; set; }
/// <summary>
/// Gets or sets a value indicating whether or not a thread is a background thread. Can only be set when the thread is not running.
/// </summary>
public bool IsBackground { get; set; }
/// <summary>
/// Gets or sets a value indicating the scheduling priority of a thread.
/// </summary>
public ThreadPriority Priority { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="WorkerThread" /> class.
/// </summary>
public WorkerThread()
{
var currentThread = Thread.CurrentThread;
ApartmentState = ApartmentState.MTA;
Culture = currentThread.CurrentCulture;
UICulture = currentThread.CurrentUICulture;
IsBackground = true;
Priority = ThreadPriority.Normal;
_pauseEvent = new AutoResetEvent(false);
_resumeEvent = new AutoResetEvent(false);
_stopEvent = new AutoResetEvent(false);
_waitEvent = new AutoResetEvent(false);
}
/// <summary>
/// Represents the method that executes on the thread.
/// </summary>
protected abstract void Execute();
/// <summary>
/// Blocks the calling thread until the thread represented by this instance terminates.
/// </summary>
/// <param name="timeout">The number of milliseconds to wait for the thread to terminate.</param>
/// <returns>Returns <see langword="true"/> when the thread wasn't running or exited before the <paramref name="timeout" />.</returns>
public bool Join(int timeout = TimeoutInfinite)
{
if (!_isRunning) return true;
var thread = _thread;
if (thread == null) return true;
try
{
return thread.Join(timeout);
}
catch
{
return true;
}
}
/// <summary>
/// Gracefully pauses the thread.
/// </summary>
/// <param name="timeout">The number of milliseconds to pause the thread. See <see cref="TimeoutInfinite"/>.</param>
/// <returns></returns>
/// <exception cref="ThreadStateException">The thread isn't running.</exception>
/// <remarks>
/// When calling <see cref="Pause(int)"/> 4 times you need to call <see cref="Resume"/> 4 times before the thread actually resumes.
/// </remarks>
public bool Pause(int timeout = 0)
{
lock (_lock)
{
if (!_isRunning)
{
throw new ThreadStateException();
}
_pauseEvent.Set();
return _waitEvent.WaitOne(timeout);
}
}
/// <summary>
/// Gracefully resumes the thread.
/// </summary>
/// <returns>Indicates whether the thread was in pause state and has been signaled.</returns>
/// <exception cref="ThreadStateException">The thread isn't running.</exception>
/// <remarks>
/// When calling <see cref="Resume"/> 4 times you need to call <see cref="Pause"/> 4 times before the thread actually pauses.
/// </remarks>
public bool Resume()
{
lock (_lock)
{
if (!_isRunning)
{
throw new ThreadStateException();
}
return _resumeEvent.Set();
}
}
/// <summary>
/// Causes a thread to be scheduled for execution.
/// </summary>
/// <exception cref="ThreadStateException">The thread has already been started.</exception>
public void Start()
{
lock (_lock)
{
if (_isRunning)
{
throw new ThreadStateException();
}
_isRunning = true;
_isPaused = false;
_thread = new Thread(Execute);
_thread.TrySetApartmentState(ApartmentState);
_thread.IsBackground = IsBackground;
_thread.CurrentCulture = Culture;
_thread.CurrentUICulture = UICulture;
_thread.Priority = Priority;
_thread.Start();
}
}
/// <summary>
/// Gracefully terminates the thread.
/// </summary>
/// <param name="timeout">The number of milliseconds to wait for the thread to terminate. See <see cref="TimeoutInfinite"/>.</param>
/// <returns>
/// <see langword="true"/> if the thread has terminated;
/// <see langword="false"/> if the thread has not terminated after the amount of time specified by the <paramref name="timeout"/> parameter has elapsed.
/// </returns>
/// <exception cref="ThreadStateException">The thread isn't running.</exception>
public bool Stop(int timeout = 0)
{
lock (_lock)
{
if (!_isRunning)
{
throw new ThreadStateException();
}
_isRunning = false;
_isPaused = false;
_stopEvent.Set();
_resumeEvent.Set();
try
{
return _thread!.Join(timeout);
}
catch // already stopped
{
return true;
}
}
}
/// <summary>
/// Process <see cref="Pause(int)" />, <see cref="Resume" /> and <see cref="Stop(int)" /> events.
/// </summary>
/// <returns>Returns <see langword="false"/> when the stop event is signaled.</returns>
protected bool ProcessThreadEvents()
{
if (_pauseEvent.WaitOne(0))
{
_isPaused = true;
_waitEvent.Set();
_resumeEvent.WaitOne();
_isPaused = false;
}
return !_stopEvent.WaitOne(0);
}
}
/// <summary>
/// Implements an example <see cref="WorkerThread" />.
/// </summary>
public sealed class ExampleThread : WorkerThread
{
/// <summary>
/// Initializes a new instance of the <see cref="ExampleThread" /> class.
/// </summary>
public ExampleThread()
{
Priority = ThreadPriority.AboveNormal;
}
/// <inheritdoc />
protected override void Execute()
{
int count = 0;
do
{
Console.Title = $"Count: {count}";
count++;
Thread.Sleep(1000);
} while (ProcessThreadEvents());
}
}
public static class Program
{
public static void Main(string[] _)
{
Console.WriteLine("Commands: start, stop, pause, resume, exit");
var exampleThread = new ExampleThread();
string? command;
do
{
command = Console.ReadLine();
} while (TryProcessCommand(exampleThread, command ?? string.Empty));
}
private static bool TryProcessCommand(WorkerThread thread, string command)
{
try
{
return ProcessCommand(thread, command);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
return true;
}
}
private static bool ProcessCommand(WorkerThread thread, string command)
{
switch (command.ToLower())
{
case "start":
thread.Start();
break;
case "stop":
thread.Stop(Timeout.Infinite);
break;
case "pause":
thread.Pause();
break;
case "resume":
thread.Resume();
break;
case "exit":
return false;
default:
Console.WriteLine("Unknown command!");
break;
}
return true;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment