0

I've created a simple thread controller class managing the thread's execution, here its code:

 public class ThreadController {
  int waitCount;
  Thread myThread;

  public ThreadController() {
    //
  }

  public void StartThread() {
    waitCount = 0;
    // launch a thread to show an alert when conditions are met!
    myThread = new Thread(new ThreadStart(ThreadAction));
    myThread.IsBackground = true;
    myThread.Start();
  }

  // method is async as it call an async method itself!
  void ThreadAction() {
    while (myThread.IsAlive) {
      Thread.Sleep(5000);
      bool doStop = DoStopTest().Result; // some async function testing stop criterion
      if (doStop) {            
        MainForm.BeginInvoke(new MethodInvoker(delegate() {
            MessageBox.Show("Thread stopped!");
          }));
        // 
        myThread.Abort();
      }
      ++waitCount;
      if (waitCount >= 15) {
        myThread.Abort();
      }
      Thread.Sleep(5000);
    }
  }
}

Now, I want to make sure the above created threads (there might be several) are killed when I close the MainForm, which I read should be done in the FormClosing event as follows:

void Main_FormClosing(object Sender, FormClosingEventArgs e) {
  // unfortunately, an error is thrown when I call following line...
  Environment.Exit(Environment.ExitCode);
}

The Environment.Exit call actually generates some weird exceptions... Sometimes a "vhost32.exe stopped working", sometimes an error System.ComponentModel.Win32Exception (0x80004005): Error creating window handle or other painting events that use "Invalid Parameters"...

Am I missing something here? What is the suggested way to cleanly close the form with all associated threads, without running into errors?

neggenbe
  • 1,697
  • 2
  • 24
  • 62
  • Why not use *Tasks* and async/await? The `ThreadController ` looks like it's trying to emulate Tasks. Why use any of this when `DoStopTest()` is already a Task? – Panagiotis Kanavos Jul 31 '18 at 08:39
  • All of it could be replace eg with a System.Threading.Timer whose callback walls `await DoStopTest();`, plus a CancellationToken to cancel the call if needed. Or a loog that contains `await Task.Delay(5000); await DoTestResult();` and updates the UI. Check Stephen Toub's [Cancelaltion and Progress Reporting in Async APIs](https://blogs.msdn.microsoft.com/dotnet/2012/06/06/async-in-4-5-enabling-progress-and-cancellation-in-async-apis/) – Panagiotis Kanavos Jul 31 '18 at 08:42
  • No, you've built a class that stops threads executing when you have no idea what changes to shared program state they've made. There is no real use case for `Thread.Abort`. If you're looking for "clean closure", you need to find something where the target thread is "in on it" - such as using `CancellationToken`s. – Damien_The_Unbeliever Jul 31 '18 at 08:51
  • Also, whoever told you that you "should" do `Environment.Exit` in your form close was probably confused or looking at a different case. *background* threads get automatically cleaned up *anyway* when the process exits and won't stop the process exiting – Damien_The_Unbeliever Jul 31 '18 at 08:53
  • @PanagiotisKanavos Ok, I might go with `Task`. I read I should actually never manually abort a task - as stated. Could you provide an example to show how to make sure the Task is either completed or cleaned properly after some time and, for sure, when the program closes? THX! – neggenbe Jul 31 '18 at 08:53
  • @neggenbe you should never abort a thread either. You can use `return` from inside the function to terminate the thread. There's nothing to clean with tasks anyway. What is the purpose of this code? It looks like it's trying to poll something up to 15 times and exit? – Panagiotis Kanavos Jul 31 '18 at 08:55
  • @PanagiotisKanavos This is a sample code, of course. The task is started after sending an SMS via web portal, and Task should check delivery status for a while, but not forever (dest phone might be switched off, so no need to check over and over again, only within next 10min or so, once every minute)... – neggenbe Jul 31 '18 at 09:02
  • PS: server portal has no push service or so, the API only offers a delivery status method.... – neggenbe Jul 31 '18 at 09:03
  • @neggenbe instead of posting "sample" code that doesn't match the actual logic explain what the *real* problem is. A retry timeout doesn't need loops for example. Retrying calls to the HTTP API provided by an SMS provider is easily done using libraries like Polly – Panagiotis Kanavos Jul 31 '18 at 09:06
  • This is an [old Windows bug](https://stackoverflow.com/q/18036863/17034). I did my best to get it fixed, which worked, but it came back in Win10. Very awkward bug to deal with. Be sure to keep your Windows version updated, afaik it got fixed again. You need to document the exact build version in your question. A temporary workaround to keep going is to use Environment.FailFast(). – Hans Passant Jul 31 '18 at 10:08

1 Answers1

1

The code would be a lot clearer if you used tasks and async/await. DoStopTest() seems to return a Task already, so there's no need to use a raw Thread.

The code could be something as simple as a loop :

public async Task MyTestAndWait()
{
    await Task.Delay(5000);
    var waitCount=0;
    while( waitCount++ < 15 && !(await DoStopTest()))
    {
        await Task.Delay(10000);

    }
    MessageBox.Show("Thread stopped!");
}

After each call to await execution resumes on the original synchronization context. For desktop applications, that's the UI thread. That means there's no need to use BeginInvoke

Threads should not be aborted. The correct way is to check a thread-safe signal, like a ManualResetEvent that's raised when a thread needs to exit. When signalled, the thread's code itself should exit.

Using a lot of events can get a bit messy which is why .NET 4.5 added the CancellationToken and CancellationTokenSource classes that can be used to notify both threads and Tasks they need to cancel and exit gracefully.

public async Task MyTestAndWait(CancellationToken ct,int initialDelay,int pollDelay)
{
    await Task.Delay(initialDelay,ct);
    var waitCount=0;
    while(!ct.IsCancellationRequested && waitCount++ < 15 && !(await DoStopTest()))
    {
        await Task.Delay(pollDelay,ct);

    }
    MessageBox.Show("Poll stopped!");
}

This will cancel the delays and the loop but it won't cancel the call to DoStepTest(). That method will have to accept a CancellationToken parameter as well

CancellationTokens are created by CancellationTokenSource classes. One of the overloads accepts a timeout, which could be used to cancel the overall operation :

public async void SendSMS_Click(object sender, EventArgs args)
{
    var cts=new CancellationTokenSource(TimeSpan.FromMinutes(15));
    await MyTestAndAwait(cts.Token,5000,10000);       
}

The cts could be stored in a field, to allow cancellation due to another event like a button click :

CancellationTokenSource _cts;
public async void SendSMS_Click(object sender, EventArgs args)
{
    SendSMS.Enabled=false;
    Cancel.Enabled=true;

    _cts=new CancellationTokenSource(TimeSpan.FromMinutes(15);
    await MyTestAndAwait(cts.Token,5000,10000);       
    _cts=null;

    SendSMS.Enabled=true;
    Cancel.Enabled=false;    
}

public async void Cancel_Click(object sender, EventArgs args)
{
    _cts?.Cancel();
}

The same code can be used to signal cancellation when closing the form :

void Main_FormClosing(object Sender, FormClosingEventArgs e) 
{
    _cts.?Cancel();
}

BTW there's no reason to call Environment.Exit() in the form's Closing or Closed events. Closing the main form will end the application unless there's another thread running.

UPDATE

It looks like the actual question is how to verify that an SMS was sent by polling for its send status. The code in this case would be different, while still using task. The method shouldn't have any reference to the UI so it can be moved to a separate Service-layer class. After all, changing providers shouldn't result in changing UIs

Assuming HttpClient is used, it could look like this :

//In an SmsService class

public async Task<(bool ok,string msg)> SendSmsAsync(string phone,string message,CancellationToken ct)
{
    var smsMsg=BuildSmsContent(phone,string);
    await _httpClient.PostAsync(smsMsg,ct);

    //wait before polling
    await Task.Delay(_initialDelay,ct);
    for(int i=0;i<15 && !ct.IsCancellationRequested;i++)
    {
        var checkMsg=CheckStatusContent(phone,string);
        var response=await _httpClient.GetAsync(check,ct);

        if (ct.IsCancellationRequested) break;

        //Somehow check the response. Assume it has a flag and a Reason
        var status=ParseTheResponse(response);
        switch(status.Status)
        {
            case Status.OK:
                return (ok:true,"Sent");
            case Status.Error:
                return (ok:failed,status.Reason);
            case Status.Pending:
                await Task.Delay(_pollDelay,ct);
                break;
        }
    }
    return (ok:false,"Exceeded retries or cancelled");    
}

This method could be used from a button event :

CancellationTokenSource _cts;

public async void SendSMS_Click(object sender, EventArgs args)
{
    DisableSending();

    var phone=txtPhone.Text;
    var message=txtMessage.Text;
    _cts=new CancellationTokenSource(TimeSpan.FromMinutes(15);

    var (ok,reason)=await _smsService.SendSmsAsync(phone,message,cts.Token);       
    _cts=null;

    if (ok)
    {
        MessageBox.Show("OK");
    }
    else
    {
        MessageBox.Show($"Failed: {reason}");
    }

    EnableSending();
}

public void EnableSending()
{
    SendSMS.Enabled=true;
    Cancel.Enabled=false;    
}

public void DisableSending()
{
    SendSMS.Enabled=false;
    Cancel.Enabled=true;    
}
Panagiotis Kanavos
  • 120,703
  • 13
  • 188
  • 236
  • +1 for clarity. However, I don't want to await on UI thread until the delivery status is returned - I will go on and, from the task/thread, send a tray alert from on successful delivery. This is why I went for thread in the first place - it should run behind without interaction on UI... I.e. no button de-activation etc... Does this change your solution? – neggenbe Jul 31 '18 at 12:32
  • @neggenbe you aren't awaiting on the UI thread anywhere. `await` doesn't block the current thread, it *awaits* for a result from an already running asynchronous operation asynchronously. When that completes it *resumes* execution on the original thread. That's why `await` doesn't block the UI but allows you to modify controls and display message boxes after it returns – Panagiotis Kanavos Jul 31 '18 at 12:39
  • So it's NOT what I wanted here... I mean, the first part of the `SendSMS_Click` method is ok on UI thread - up to saying "Sent"... BUT, the show message part should be done behind the scenes... As far as I understand, the `SendSMS_Click` doesn't return until the delivery status is found. That's NOT the idea here - the method SHOULD return BUT the async task keep running independently until some termination crireria, as discussed previously! – neggenbe Jul 31 '18 at 12:53
  • @neggenbe no it shouldn't. It can't. You *can't* modify the UI from another thread and that's that. No windowing system, wheter on Windows or Linux allows you to modify the UI from another thread. Your own code doesn't do that either. `BeginInvoke` marshals the call to the *UI* thread – Panagiotis Kanavos Jul 31 '18 at 12:54
  • @neggenbe and the code I posted here does run until the either a response arrives or the conditions are fulfilled - the timeout or the number of retries and cancellation when the form closes. If you wanted something else you should have explained it in the *question*, not the comments. – Panagiotis Kanavos Jul 31 '18 at 12:56
  • @Neggenbe if you want progress reporting from the async method, you can do that with the IProgress interface described in Stephen Toub's article. – Panagiotis Kanavos Jul 31 '18 at 12:57
  • Ok one step back - you seem upset about my comments so take it easy. We moved away from the initial question which is EXACTLY what I want to do, but possibly not the correct way to do it - which explains my question in the first place. So going back to it - when sent, program continues and launches a monitor thread that, when delivered, notifies on the UI thread using `BeginInvoke`, that's the correct way, for sure. The whole point how I program the threads and make sure they're stopped whenever I want. So let's get back to that please. – neggenbe Jul 31 '18 at 13:02
  • Actually, replacing the initial calls of `myThread.Abort()' with `return` seems to do the trick... Does this seem reasonable to you? – neggenbe Jul 31 '18 at 13:03