I have the following method:
public void PutFile(string ID, Stream content)
{
try
{
ThreadPool.QueueUserWorkItem(o => putFileWorker(ID, content));
}
catch (Exception ex)
{
OnPutFileError(this, new ExceptionEventArgs { Exception = ex });
}
}
The putFileWorker method looks like this:
private void putFileWorker(string ID, Stream content)
{
//Get bucket name:
var bucketName = getBucketName(ID)
.ToLower();
//get file key
var fileKey = getFileKey(ID);
try
{
//if the bucket doesn't exist, create it
if (!Amazon.S3.Util.AmazonS3Util.DoesS3BucketExist(bucketName, s3client))
s3client.PutBucket(new PutBucketRequest { BucketName = bucketName, BucketRegion = S3Region.EU });
PutObjectRequest request = new PutObjectRequest();
request.WithBucketName(bucketName)
.WithKey(fileKey)
.WithInputStream(content);
S3Response response = s3client.PutObject(request);
var xx = response.Headers;
OnPutFileCompleted(this, new ValueEventArgs { Value = ID });
}
catch (Exception e)
{
OnPutFileError(this, new ExceptionEventArgs { Exception = e });
}
}
I've created a little console app to test this.
I wire up event handlers for the OnPutFileError and OnPutFileCompleted events.
If I call my PutFile method, and step into this, it gets to the "//if the bucket doesn't exist, create it" line, then exits. No exception, no errors, nothing.
It doesn't complete (i've set breakpoints on my event handlers too) - it just exits.
If I run the same method without the ThreadPool.QueueUserWorkItem then it runs fine...
Am I missing something?
ThreadPool threads are background threads (see the link). They will not keep your application running if the main thread exits.
Typically, in WinForms apps, this is not a problem, because the main UI thread calls Application.Run and starts processing events. For your console app, if your Main method doesn't wait for the work item to complete somehow, the main thread will queue the work item and then exit.
You could create a background thread yourself and set its IsBackground property to false. Or you could create a thread and call Thread.Join to wait for it to finish.
-- EDIT --
As suggested in the comments below, you could also use a ManualResetEvent, or even a custom synchronization class as suggested by Linik. The goal is to block the main thread until the the background threads have completed.
To use a ManualResetEvent, create it in your main thread and pass it in as an argument. (I'll assign it to a static variable here just for brevity.)
ManualResetEvent s_WaitEvent;
ManualResetEvent s_WaitEvent = new ManualResetEvent(false); // non-signaled
// queue work item here
s_WaitEvent.WaitOne();
At the end of your worker thread, signal the event:
s_WaitEvent.Set();
Link's CountDownLatch is nice if you have many threads that must process before you can exit. You can also use separate ManualResetEvents for each thread and wait for them all to complete using WaitHandle.WaitAll(WaitHandle[]). (ManualResetEvent inherits from WaitHandle.)
Put a Console.ReadLine() in your Main thread to block it while you test your worker thread. This will keep main from exiting. Just hit enter when you're done.
Use a CountDownLatch to force the main to wait for all of the threads that you have queued up:
public class CountDownLatch
{
private int m_remain;
private EventWaitHandle m_event;
public CountDownLatch (int count)
{
if (count < 0)
throw new ArgumentOutOfRangeException();
m_remain = count;
m_event = new ManualResetEvent(false);
if (m_remain == 0)
{
m_event.Set();
}
}
public void Signal()
{
// The last thread to signal also sets the event.
if (Interlocked.Decrement(ref m_remain) == 0)
m_event.Set();
}
public void Wait()
{
m_event.WaitOne();
}
}
In Main:
static void Main(string[] args)
{
CountDownLatch latch = new CountDownLatch(numFiles);
//
// ...
//
putFileWorker("blah", streamContent);
//
// ...
//
// waits for all of the threads to signal
latch.Wait();
}
In the worker method:
private void putFileWorker(string ID, Stream content)
{
try
{
//Get bucket name:
var bucketName = getBucketName(ID)
.ToLower();
//get file key
var fileKey = getFileKey(ID);
try
{
//if the bucket doesn't exist, create it
if (!Amazon.S3.Util.AmazonS3Util.DoesS3BucketExist(bucketName, s3client))
s3client.PutBucket(new PutBucketRequest { BucketName = bucketName, BucketRegion = S3Region.EU });
//
// ...
//
}
catch (Exception e)
{
OnPutFileError(this, new ExceptionEventArgs { Exception = e });
}
}
finally
{
latch.Signal();
}
}
Related
I have a system with 10 machines where I need to perform a certain task on each machine one by one in synchronize order. Basically only one machine should do that task at a particular time. We already use Consul for some other purpose but I was thinking can we use Consul to do this as well?
I read more about it and it looks like we can use leader election with consul where each machine will try to acquire lock, do the work and then release the lock. Once work is done, it will release the lock and then other machine will try to acquire lock again and do the same work. This way everything will be synchronized one machine at a time.
I decided to use this C# PlayFab ConsulDotNet library which already has this capability built in looks like but if there is any better option available I am open to that as well. Below Action method in my code base is called on each machine at the same time almost through a watcher mechanism.
private void Action() {
// Try to acquire lock using Consul.
// If lock acquired then DoTheWork() otherwise keep waiting for it until lock is acquired.
// Once work is done, release the lock
// so that some other machine can acquire the lock and do the same work.
}
Now inside that above method I need to do below things -
Try to acquire lock. If you cannot acquire the lock wait for it since other machine might have grabbed it before you.
If lock acquired then DoTheWork().
Once work is done, release the lock so that some other machine can acquire the lock and do the same work.
Idea is all 10 machines should DoTheWork() one at a time in synchronize order. Based on this blog and this blog I decided to modify their example to fit our needs -
Below is my LeaderElectionService class:
public class LeaderElectionService
{
public LeaderElectionService(string leadershipLockKey)
{
this.key = leadershipLockKey;
}
public event EventHandler<LeaderChangedEventArgs> LeaderChanged;
string key;
CancellationTokenSource cts = new CancellationTokenSource();
Timer timer;
bool lastIsHeld = false;
IDistributedLock distributedLock;
public void Start()
{
timer = new Timer(async (object state) => await TryAcquireLock((CancellationToken)state), cts.Token, 0, Timeout.Infinite);
}
private async Task TryAcquireLock(CancellationToken token)
{
if (token.IsCancellationRequested)
return;
try
{
if (distributedLock == null)
{
var clientConfig = new ConsulClientConfiguration { Address = new Uri("http://consul.host.domain.com") };
ConsulClient client = new ConsulClient(clientConfig);
distributedLock = await client.AcquireLock(new LockOptions(key) { LockTryOnce = true, LockWaitTime = TimeSpan.FromSeconds(3) }, token).ConfigureAwait(false);
}
else
{
if (!distributedLock.IsHeld)
{
await distributedLock.Acquire(token).ConfigureAwait(false);
}
}
}
catch (LockMaxAttemptsReachedException ex)
{
//this is expected if it couldn't acquire the lock within the first attempt.
Console.WriteLine(ex.Stacktrace);
}
catch (Exception ex)
{
Console.WriteLine(ex.Stacktrace);
}
finally
{
bool lockHeld = distributedLock?.IsHeld == true;
HandleLockStatusChange(lockHeld);
//Retrigger the timer after a 10 seconds delay (in this example). Delay for 7s if not held as the AcquireLock call will block for ~3s in every failed attempt.
timer.Change(lockHeld ? 10000 : 7000, Timeout.Infinite);
}
}
protected virtual void HandleLockStatusChange(bool isHeldNew)
{
// Is this the right way to check and do the work here?
// In general I want to call method "DoTheWork" in "Action" method itself
// And then release and destroy the session once work is done.
if (isHeldNew)
{
// DoTheWork();
Console.WriteLine("Hello");
// And then were should I release the lock so that other machine can try to grab it?
// distributedLock.Release();
// distributedLock.Destroy();
}
if (lastIsHeld == isHeldNew)
return;
else
{
lastIsHeld = isHeldNew;
}
if (LeaderChanged != null)
{
LeaderChangedEventArgs args = new LeaderChangedEventArgs(lastIsHeld);
foreach (EventHandler<LeaderChangedEventArgs> handler in LeaderChanged.GetInvocationList())
{
try
{
handler(this, args);
}
catch (Exception ex)
{
Console.WriteLine(ex.Stacktrace);
}
}
}
}
}
And below is my LeaderChangedEventArgs class:
public class LeaderChangedEventArgs : EventArgs
{
private bool isLeader;
public LeaderChangedEventArgs(bool isHeld)
{
isLeader = isHeld;
}
public bool IsLeader { get { return isLeader; } }
}
In the above code there are lot of pieces which might not be needed for my use case but idea is same.
Problem Statement
Now in my Action method I would like to use above class and perform the task as soon as lock is acquired otherwise keep waiting for the lock. Once work is done, release and destroy the session so that other machine can grab it and do the work. I am kinda confuse on how to use above class properly in my below method.
private void Action() {
LeaderElectionService electionService = new LeaderElectionService("data/process");
// electionService.LeaderChanged += (source, arguments) => Console.WriteLine(arguments.IsLeader ? "Leader" : "Slave");
electionService.Start();
// now how do I wait for the lock to be acquired here indefinitely
// And once lock is acquired, do the work and then release and destroy the session
// so that other machine can grab the lock and do the work
}
I recently started working with C# so that's why kinda confuse on how to make this work efficiently in production by using Consul and this library.
Update
I tried with below code as per your suggestion and I think I tried this earlier as well but for some reason as soon as it goes to this line await distributedLock.Acquire(cancellationToken);, it just comes back to main method automatically. It never moves forward to my Doing Some Work! print out. Does CreateLock actually works? I am expecting that it will create data/lock on consul (since it is not there) and then try to acquire the lock on it and if acquired, then do the work and then release it for other machines?
private static CancellationTokenSource cts = new CancellationTokenSource();
public static void Main(string[] args)
{
Action(cts.Token);
Console.WriteLine("Hello World");
}
private static async Task Action(CancellationToken cancellationToken)
{
const string keyName = "data/lock";
var clientConfig = new ConsulClientConfiguration { Address = new Uri("http://consul.test.host.com") };
ConsulClient client = new ConsulClient(clientConfig);
var distributedLock = client.CreateLock(keyName);
while (true)
{
try
{
// Try to acquire lock
// As soon as it comes to this line,
// it just goes back to main method automatically. not sure why
await distributedLock.Acquire(cancellationToken);
// Lock is acquired
// DoTheWork();
Console.WriteLine("Doing Some Work!");
// Work is done. Jump out of loop to release the lock
break;
}
catch (LockHeldException)
{
// Cannot acquire the lock. Wait a while then retry
await Task.Delay(TimeSpan.FromSeconds(10), cancellationToken);
}
catch (Exception)
{
// TODO: Handle exception thrown by DoTheWork method
// Here we jump out of the loop to release the lock
// But you can try to acquire the lock again based on your requirements
break;
}
}
// Release and destroy the lock
// So that other machine can grab the lock and do the work
await distributedLock.Release(cancellationToken);
await distributedLock.Destroy(cancellationToken);
}
IMO, LeaderElectionService from those blogs is an overkill in your case.
Update 1
There is no need to do while loop because:
ConsulClient is local variable
No need to check IsHeld property
Acquire will block indefinitely unless
Set LockTryOnce true in LockOptions
Set timeout to CancellationToken
Side note, it is not necessary to invoke Destroy method after you call Release on the distributed lock (reference).
private async Task Action(CancellationToken cancellationToken)
{
const string keyName = "YOUR_KEY";
var client = new ConsulClient();
var distributedLock = client.CreateLock(keyName);
try
{
// Try to acquire lock
// NOTE:
// Acquire method will block indefinitely unless
// 1. Set LockTryOnce = true in LockOptions
// 2. Pass a timeout to cancellation token
await distributedLock.Acquire(cancellationToken);
// Lock is acquired
DoTheWork();
}
catch (Exception)
{
// TODO: Handle exception thrown by DoTheWork method
}
// Release the lock (not necessary to invoke Destroy method),
// so that other machine can grab the lock and do the work
await distributedLock.Release(cancellationToken);
}
Update 2
The reason why OP's code just returns back to Main method is that, Action method is not awaited. You can use async Main if you use C# 7.1, and put await on Action method.
public static async Task Main(string[] args)
{
await Action(cts.Token);
Console.WriteLine("Hello World");
}
I want to use that load some in backgound , But Jobsystem probably use the main thread , So how to do use jobsystem Wituhot Freezes , is it impossible ??
Or I just use C# Thread?? Dont Use JobSystem ??
struct SleepJob : IJobParallelFor
{
public void Execute(int index)
{
Debug.LogFormat("[SleepJob.Execute] Thread Id {0}", Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(1);
}
}
struct SleepJob2 : IJobParallelFor
{
public void Execute(int index)
{
Debug.LogFormat("[SleepJob2.Execute] Thread Id {0}", Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(1);
}
}
[ContextMenu("JobSleep")]
public void JobSleep()
{
Debug.LogFormat("[JobSleep.Execute] Thread Id {0}", Thread.CurrentThread.ManagedThreadId);
SleepJob job = new SleepJob() { };
SleepJob2 job2 = new SleepJob2() { };
JobHandle jh = job.Schedule(100, 64);
JobHandle jh2 = job2.Schedule(100, 64, jh);
JobHandle.ScheduleBatchedJobs();
jh2.Complete(); // freezes
Debug.LogFormat("[JobSleep.Execute] jh.Complete();");
}
Note that Complete makes your execution freeze because:
The JobSystem automatically prioritizes the job and any of its dependencies to run first in the queue, then attempts to execute the job itself on the thread which calls the Complete function.
I think this is not the method you want to use.
Rather try waiting for the Job to complete in a Coroutine like e.g.
[ContextMenu("JobSleep")]
public void JobSleep()
{
Debug.LogFormat("[JobSleep.Execute] Thread Id {0}", Thread.CurrentThread.ManagedThreadId);
SleepJob job = new SleepJob() { };
SleepJob2 job2 = new SleepJob2() { };
JobHandle jh = job.Schedule(100, 64);
JobHandle jh2 = job2.Schedule(100, 64, jh);
JobHandle.ScheduleBatchedJobs();
StartCoroutine(WaitFor(jh2));
}
IEnumerator WaiFor(JobHandle job)
{
yield return new WaitUntil(() => job.IsComplete);
Debug.LogFormat("[JobSleep.Execute] job IsComplete");
}
Unfortunately you didn't add the code you actually want to be executed in background.
In general maybe simply using a Thread or async might already solve your problem.
e.g. with a Thread
// for sending back responses to the main thread
private ConcurrentQueue<Action> callbacks = new ConcurrentQueue<Action>;
// work the callbacks in the main thread
private void Update()
{
while(callbacks.Count > 0)
{
Action callback;
if(callbacks.TryDequeue(out callback)) callback?.Invoke();
}
}
// optional callback on success
public void StartBackgroundTask(Action onSuccess = null)
{
var thread = new Thread(new ParameterizedThreadStart(TheTaskThatTakesAWhile);
// pass in parameters here
thread.Start(onSuccess);
}
// Will be running in a background thread
private void TheTaskThatTakesAWhile(Action onSuccess)
{
// hand this back to the main thread
callbacks.Enqueue(() => Debug.Log("Long task started ..."));
// TODO whatever takes so long
// Note btw that sleep is in milliseconds!
Thread.Sleep(1000);
hand this back to the mainthread
callbacks.Enqueue(() =>
{
Debug.Log("Long task started ..."));
onSuccess?.Invoke();
}
}
e.g. using async (which internally also runs in a Thread)
private async Task TheTaskThatTakesAWhile()
{
// do whatever takes long here
}
// usually you should avoid having async void
// but when including an Action as callback this is fine
public async void StartBackgroundTask(Action onSuccess = null)
{
await TheTaskThatTakesAWhile();
onSuccess?.Invoke();
}
Both you could call like e.g.
StartBackgroundTask(() => {
// what should happen when done?
Debug.Log("I'm done!");
});
Make JobHandle jh2 global variable. Check JobHandle.isComplete in Update method or in Coroutine
I'm trying to use an AutoResetEvent object to block the thread until the async. download of a WebClient is done.
My problem is that once I call WaitOne(), the thread just locks there and VS never reaches the breakpoint in the DownloadComplete event handler method.
Here's my code
//Class used to pass arguments to WebClient's events...
public class RunArgs
{
public JobInfo jobInfo;
public int jobTotal;
public int jobIndex;
public AutoResetEvent AutoResetEventObject;
}
List<JobInfo> jl = ConfigSectionWrapper.GetAllJobs();
int jobAmount = jl.Count;
int jobIndex = 0;
RunArgs args = new RunArgs();
args.jobTotal = jl.Count;
foreach (JobInfo ji in jl)
{
if (ji.enabled == "0")
{
args.jobIndex++;
continue;
}
try
{
args.jobIndex++;
args.jobInfo = ji;
appLog.Source = ji.eventSource;
appLog.WriteEntry(string.Format("Started job {0}...", ji.jobName), EventLogEntryType.Information);
ji.fullFileName = string.Format(ji.reportFileName, string.Format("{0}-{1}-{2}", DateTime.Now.Year.ToString(), DateTime.Now.Month.ToString().PadLeft(2, '0'), DateTime.Now.Day.ToString().PadLeft(2, '0')));
ji.fullFileName = string.Format("{0}{1}", ji.downloadDirectory, ji.fullFileName);
using (WebClient wc = new WebClient())
{
AutoResetEvent notifier = new AutoResetEvent(false);
args.AutoResetEventObject = notifier;
wc.Credentials = CredentialCache.DefaultNetworkCredentials;
wc.DownloadFileCompleted += new AsyncCompletedEventHandler(DownloadCompleted);
wc.DownloadFileAsync(new Uri(args.jobInfo.reportURL), args.jobInfo.fullFileName, args); //Pass the args params to event handler...
notifier.WaitOne();
}
}
catch (Exception ex)
{
appLog.WriteEntry(string.Format("Error starting report execution: {0}", ex.Message), EventLogEntryType.Error);
DeleteFile(ji.fullFileName);
}
}
private void DownloadCompleted(object sender, AsyncCompletedEventArgs e)
{
RunArgs args = (RunArgs)e.UserState;
//Do things....
args.AutoResetEventObject.Set();
}
So I instantiate notifier with false in the constructor, because I don't want its status to be signaled already. Unless I'm reading MSDN wrong ?
Anything obviously wrong ?
WebClient uses the AsyncOperationManager to manage async operations. So, make sure you are aware of how these async operations get called under different scenarios.
Under WinForms
Under a WinForm application AsyncOperationManager uses the WindowsFormsSynchronizationContext. As #Michael says, the WaitOne() call blocks the main form thread from firing the DownloadCompleted event. In WinForms the DownloadCompleted is executed on the main WinForm thread.
So, remove the notifier.WaitOne() and it should work. DownloadCompleted needs to be called by the main window thread (presumably, the one which you're blocking with WaitOne()).
Under Console applications
Under a Console type application the AsyncOperationManager uses System.Threading.SynchronizationContext and the DownloadCompleted is executed asynchronously by a thread form the threadpool.
There is no issue with calling notifier.WaitOne() under a Console app; and the code above works as expected.
I haven't found any documentation to support this, but looking at the WebClient code in Reflector, it appears that the events are raised on the main thread, not the background thread. Since your main thread is blocking when you call notifier.WaitOne(), the event handler never gets called.
If the example you provided accurately represents your code, there's absolutely no need to use wc.DownloadFileAsync() instead of wc.DownloadFile(). That notifier.WaitOne() call ultimately makes this into a synchronous operation. If you're looking for a true asynchronous operation, you'll have to do this differently.
Here's what I ended up doing:
private AutoResetEvent notifier = new AutoResetEvent(false);
Now the main loop looks like:
foreach (JobInfo ji in jl)
{
if (ji.enabled == "0")
{
args.jobIndex++;
continue;
}
args.jobInfo = ji;
Thread t = new Thread(new ParameterizedThreadStart(startDownload));
t.Start(args);
notifier.WaitOne();
}
private void startDownload(object startArgs)
{
RunArgs args = (RunArgs)startArgs;
try
{
args.jobIndex++;
appLog.Source = args.jobInfo.eventSource;
appLog.WriteEntry(string.Format("Started job {0}...", args.jobInfo.jobName), EventLogEntryType.Information);
args.jobInfo.fullFileName = string.Format(args.jobInfo.reportFileName, string.Format("{0}-{1}-{2}", DateTime.Now.Year.ToString(), DateTime.Now.Month.ToString().PadLeft(2, '0'), DateTime.Now.Day.ToString().PadLeft(2, '0')));
args.jobInfo.fullFileName = string.Format("{0}{1}", args.jobInfo.downloadDirectory, args.jobInfo.fullFileName);
WebClient wc = new WebClient();
wc.Credentials = CredentialCache.DefaultNetworkCredentials;
wc.DownloadFileCompleted += new AsyncCompletedEventHandler(DownloadCompleted);
wc.DownloadFileAsync(new Uri(args.jobInfo.reportURL), args.jobInfo.fullFileName, args); //Pass the args params to event handler...
}
catch (Exception ex)
{
appLog.WriteEntry(string.Format("Error starting report execution: {0}", ex.Message), EventLogEntryType.Error);
DeleteFile(args.jobInfo.fullFileName);
notifier.Set();
}
}
So now the AutoResetEvent is blocking the main thread but t can successfully trigger the DownloadFileCompleteEvent. And in the DownloadFileCompleted event, I'm obviously doing a notifier.Set() too.
Thanks all!
RulyCanceler canceler = new RulyCanceler();
Thread workerThread = new Thread(() =>
{
try { Work(canceler, crawler, propertyBag); }
catch (OperationCanceledException e)
{
LogError.WriteError("Working thread canceled!" + Environment.NewLine);
LogError.WriteError(e);
}
catch (Exception e)
{
LogError.WriteError(e);
}
});
workerThread.Start();
bool finished = workerThread.Join(120000);
if (!finished)
{
LogError.WriteError("Aborting thread");
workerThread.Abort();
}
I can not figure out if my program stop all threads when Abort is calling or just current thread and program still executing. Sometime program stop unexepected but i do not know if abort is guilty. Can someone tell me?
EDIT:
I modify class like John suggest
class RulyCanceler
{
readonly object _cancelLocker = new object();
bool _cancelRequest;
private bool IsCancellationRequested
{
get { lock (_cancelLocker) return _cancelRequest; }
}
public void Cancel() { lock (_cancelLocker) _cancelRequest = true; }
public void ThrowIfCancellationRequested()
{
if (IsCancellationRequested) throw new OperationCanceledException();
}
}
class Test
{
static void Main()
{
var canceler = new RulyCanceler();
new Thread (() => {
try { Work (canceler); }
catch (OperationCanceledException)
{
Console. WriteLine ("Canceled! ");
}
}). Start();
Thread. Sleep (1000);
canceler. Cancel(); // Safely cancel worker.
}
static void Work (RulyCanceler c)
{
while (true)
{
c. ThrowIfCancellationRequested();
// . . .
try { OtherMethod (c); }
finally { /* any required cleanup */ }
}
}
Thread.Abort will only abort the thread which you call it on. However, it's a really bad way of terminating a thread - you should do it in a more controlled and cooperative way. Thread.Abort is a safe way of aborting the currently executing thread, but otherwise you don't know that the thread is at a suitable place to abort without leaving the application in an inconsistent state. You should usually reserve it for times when your whole application is going down anyway.
If you want to know what's taking your application down then attach an event handler to the AppDomain.UnhandledException event and log the stacktrace (or set a debug break point).
I know I can use ReadKey for that but it will freeze the app until user presses a key. Is it possible (in console app) to have some loop running and still be able to react? I can only think of events but not sure how to use them in console.
My idea was that the loop would check for input during each iteration.
They way I have done this for my own application was to have a dedicated thread that calls into System.Console.ReadKey(true) and puts the keys pressed (and any other events) into a message queue.
The main thread then services this queue in a loop (in a similar fashion to the main loop in a Win32 application), ensuring that rendering and event processing is all handled on a single thread.
private void StartKeyboardListener()
{
var thread = new Thread(() => {
while (!this.stopping)
{
ConsoleKeyInfo key = System.Console.ReadKey(true);
this.messageQueue.Enqueue(new KeyboardMessage(key));
}
});
thread.IsBackground = true;
thread.Start();
}
private void MessageLoop()
{
while (!this.stopping)
{
Message message = this.messageQueue.Dequeue(DEQUEUE_TIMEOUT);
if (message != null)
{
switch (message.MessageType)
{
case MessageType.Keyboard:
HandleKeyboardMessage((KeyboardMessage) message);
break;
...
}
}
Thread.Yield(); // or Thread.Sleep(0)
}
}
Have the loop run in separate thread.
class Program
{
private static string input;
public static void Main()
{
Thread t = new Thread(new ThreadStart(work));
input = Console.ReadLine();
}
private static void work()
{
while (input == null)
{
//do stuff....
}
}
}
Try KeyAvailable property starting with .NET Framework 2 through now (current .NET 6 - including .NET Core). A single thread can process in a loop without being blocked.
// loop start
if (Console.KeyAvailable) // Non-blocking peek
{
var key = Console.ReadKey(true);
// process key
}
// continue without stopping
// loop end