2

I have a simple Winforms application. I would like to background TCP connections/print requests and check the output of all tasks at a set point in my code.

I would expect ReportOnTasks to block until WaitAll is complete. Please could someone explain why this is not the case? I'm also worried I haven't structured this correctly.

Edit, to clarify my intentions: I would like to send the print jobs as soon as I receive the data. Then continue with some other DB operations. Once all the print operations are complete, I would like to update the UI to state the result.

I've attempted to simplify the code as much as I can. Maybe too much. HomeController just inits some stuff. There are buttons on the form and file watchers that trigger the main functionality.

public class HomeController 
{

    public HomeController(){

        MessageBox.Show("1");

        oPrintController.PrintAsync("192.168.2.213", Encoding.ASCII.GetBytes("string to print"));

        MessageBox.Show("2");

        // Block here untill tasks are complete
        ReportOnTasks();

        MessageBox.Show("Report on tasks complete");
    }


    public async void ReportOnTasks()
    {
        await Task.WhenAll(oPrintController.Tasks);

        foreach(Task<PrintController.PrintResult> PR in oPrintController.Tasks)
        {
            // do something with the result of task
        }
    }
}

and the PrintController

public class PrintController
{

    public List<Task<PrintResult>> Tasks = new List<Task<PrintResult>>();


    public async void PrintAsync(string sIP, List<byte[]> lsToPrint, int iPort = 9100)
    {
        var s = await Task.Run(() => PrintAsync1(sIP, lsToPrint));
    }

    public async System.Threading.Tasks.Task<PrintResult> PrintAsync1(string sIP, List<byte[]> lsToPrint, int iPort = 9100)
    {

        using (TcpClient tc = new TcpClient())
        {
            await tc.ConnectAsync(sIP, iPort);
            using (var ns = tc.GetStream())
            {
                foreach (byte[] btLabel in lsToPrint)
                {
                    await ns.WriteAsync(btLabel, 0, btLabel.Length);
                }
            }
        }

        Thread.Sleep(10000);

        return new PrintResult();
    }
}

public class PrintResult
{
    bool bSuccess = false;
}
atoms
  • 2,789
  • 2
  • 17
  • 37
  • 2
    The problem is, is that you're trying to call an async method from your constructor, which isn't/cannot be async. You should check this question: [Call asynchronous method in constructor?](https://stackoverflow.com/questions/23048285/call-asynchronous-method-in-constructor) – Jeroen van Langen May 29 '19 at 08:38
  • 1
    I would create an `public async Task Initialize() { }` method to initialize the class. – Jeroen van Langen May 29 '19 at 08:42
  • What **exactly** do you mean by block? Do you mean "block UI"? If so, then no, point of async/await is to *not* block. However, if you mean, not continue executing until ... and then not completing its task until ... then yes, that should happen. – Lasse V. Karlsen May 29 '19 at 08:48
  • However, ReportOnTasks is a method returning a Task, you're not awaiting this task, so essentially you're kicking off this task in a fire-and-forget manner. – Lasse V. Karlsen May 29 '19 at 08:48
  • I would like to send the print jobs as soon as I receive the data. Then continue with some other DB operations. Once all the print operations are complete, I would like to update the UI to state such. @LasseVågsætherKarlsen I would like to wait until tasks are complete then do somthing – atoms May 29 '19 at 08:50
  • Thanks for the comments. Have resolved the issue by refactoring using whats been so kindly explained here. – atoms May 30 '19 at 08:01

2 Answers2

4

You are not awaiting the call to ReportOnTasks()
Moreover, you can't await within a ctor, because they can't be async.

Depending on how your HomeController is used, you could use a static async method which returns an instance of HomeController, created by a private ctor instead:

Something like this:

public class HomeController 
{

    //notice private - you can't new up a HomeController - you have to use `CreateInstance`
    private HomeController(){

        MessageBox.Show("1");

        //not clear from your code where oPrintController comes from??
        oPrintController.PrintAsync("192.168.2.213", Encoding.ASCII.GetBytes("string to print"));

        MessageBox.Show("2");

        MessageBox.Show("Report on tasks complete");
    }

    public static async Task<HomeController> CreateInstance() {
        var homeController = new HomeController();

        await homeController.ReportOnTasks();

        return homeController;
    }


    //don't use async void! Change to Task
    public async Task ReportOnTasks()
    {
        //not clear from your code where oPrintController comes from??
        await Task.WhenAll(oPrintController.Tasks);

        foreach(Task<PrintController.PrintResult> PR in oPrintController.Tasks)
        {
            // do something with the result of task
        }
    }
}

Usage:

var homeControllerInstance = await HomeController.CreateInstance();
Alex
  • 34,123
  • 47
  • 189
  • 315
  • 1
    Thanks for this Alex. In Main.cs I have a `MainForm()` method which inits `HomeController`. HomeController then inits the PrintController in its constructor. Am attempting to implement this now – atoms May 29 '19 at 08:45
0

It's generally not recommended to perform heavy operations in class constructors, but I suppose you won't change that part, so in order to wait for ReportOnTasks to finish, you need to make it synchronous.

Take into account, that constructor itself doesn't support async/await, it's not possible to mark it async.

Having said that, you won't have real performance enhancement marking void ReportOnTasks as async. In addition, it is not recommended to mark void methods as async due to issues with exceptions handling, which is usually not possible.

So, you can either postpone ReportOnTasks like Alex showed you, or you can synchronously wait until all tasks are finished (which is possible inside ctor).

    public void ReportOnTasks()
    {
        Task.WhenAll(oPrintController.Tasks).GetAwaiter().GetResult(); //synchronously wait

        foreach(Task<PrintController.PrintResult> PR in oPrintController.Tasks)
        {
            // do something with the result of task
        }
    }

However, I wouldn't suggest this approach, because instance creation will take a while and most importantly block UI thread - and that's usually signal something is really fishy

Darjan Bogdan
  • 3,535
  • 1
  • 18
  • 29
  • If you call `.GetAwaiter().GetResult()` on the UI thread, it can cause a deadlock, which is what the OP is trying to avoid http://blog.stephencleary.com/2012/07/dont-block-on-async-code.html – Alex May 29 '19 at 08:46
  • 1
    Thanks this code is simplified. There are no big calls in the constructor – atoms May 29 '19 at 08:47
  • 1
    @Alex I'm aware of that, but since question asks to block inside ctor, it's obvious that thread which instantiates HomeController will be blocked? In addition, I enlisted why that is not ok to do... – Darjan Bogdan May 29 '19 at 08:50