Last active
October 15, 2023 11:41
-
-
Save michel-pi/cc7a54a766dc730d7bcef14cebc7e4b5 to your computer and use it in GitHub Desktop.
Best practise thread start, stop, pause, resume and join.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* 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