2

By clicking a button I need to call a function f1() asynchronously that starts a long operation. While f1() runs, with another button I want to start another function f2(). When function f1() is done, I would like to recognize that.

I broke the whole thing down to a really easy sample where one button starts a long function writeStars(), that keeps writing '*' to a string every 200 msecs, in sum 20 times. With the other button I can write a '-' into the same string asynchronously.

        private async void btnStartThread1_Click(object sender, EventArgs e)
        {
            Thread thread1 = new Thread(writeStars);
            thread1.Start();
        }

        private void btnWriteMinus_Click(object sender, EventArgs e)
        {
            _Str += "-";
        }

        async void writeStars()
        {
            int count = 20;
            do
            {
                _Str += "*";
                Thread.Sleep(200);
            } while (count-- > 0);
        }

The code above gives me results like : '**-****-*******-****'.

Or when clicking the second button faster : '**--*-**************'.

This is really async :-)

I would like to use async Tasks, to be able to await on the end of writeStars() and then maybee enable a button. I tried that, but I found no way to code this easy sample with Task, async and await ...

tom2051
  • 82
  • 9
  • 2
    Change the signature of `writeStars` to `async Task writeStars()`. Then, you can do something like this: `await Task.Run(() => writeStars()); DoSomethingAfterCompletion();`. For reference, see: [when to return a Task vs void](https://stackoverflow.com/a/12144426/8967612). – 41686d6564 stands w. Palestine Jan 29 '20 at 20:08

2 Answers2

2

You should probably use async and await. Basically, the two functions (and two buttons) have nothing to do with each other, assuming that you don't actively block the GUI.

As an example, you can make a simple UI with 4 controls (I made it in WPF). Two buttons, which each invoke an async method, and two text blocks, which will be updated after the async method.

The WPF code is quite simple. Notice the name of the click events.

<UniformGrid Columns="2">
    <Button x:Name="UpdateSlow" Content="Wait 10 seconds" Click="UpdateSlow_Click" />
    <Button x:Name="UpdateFast" Content="Wait 3 seconds" Click="UpdateFast_Click" />
    <TextBlock x:Name="LongWaitTime" Text="Waiting for text" />
    <TextBlock x:Name="ShortWaitTime" Text="Waiting for text" />
</UniformGrid>

And the code behind, which contains the click events.

private async void UpdateSlow_Click(object sender, RoutedEventArgs e)
{
    await Task.Delay(10000);
    LongWaitTime.Text = "I finished in 10 seconds";
}

private async void UpdateFast_Click(object sender, RoutedEventArgs e)
{
    await Task.Delay(3000);
    ShortWaitTime.Text = "I finished in 3 seconds";
}

If you click the "UpdateSlow" button, there will be a 10 seconds delay, before the "LongWaitTime" text block gets changed. In the mean time you can click the "UpdateFast" button, which will update the "ShortWaitTime" text block after just 3 seconds. Most likely finishing before the 10 seconds wait time is over (depending on when you click of course).

Hope that makes sense...

Modifying a text block:

I cannot figure out exactly what your goal is, which makes it difficult to come up with a really good solution. If it's just for the fun of it, the code below should work. However, I am unsure of any possible side effects of accessing the same string build from two different methods, so I wouldn't recommend this for production.

<UniformGrid Columns="1">
    <Button x:Name="AddStar" Content="Add star to text" Click="AddStar_Click" />
    <TextBlock x:Name="Dots" Text="." Margin="20 0" VerticalAlignment="Center" FontSize="24" />
</UniformGrid>

public MainWindow()
{
    InitializeComponent();
    PrintDots();
}

public async void PrintDots()
{
    // This will run forever, printing a dot every 0.2 seconds, clearing the string, when it reaches 50 characters.
    while (true)
    {
        _sb.Append(".");
        await Task.Delay(200);

        if (_sb.Length == 50) { _sb.Clear(); }

        Dots.Text = _sb.ToString();
    }
}

private void AddStar_Click(object sender, RoutedEventArgs e)
{
    // On click a star (asterix) will be appended to the string
    _sb.Append("*");
    Dots.Text = _sb.ToString();
}

I decided to keep the string in a StringBuilder, as that should be a lot better for performance, as well as improving the readability by having an "Append" method.

A fair warning:

Normally async void should only be used in user interfaces (e.g. on button click events). In most other cases it is not a good way of doing asynchronous programming. The program will have no way to track the method progress, once started. This could lead to all sorts of funky situations where you don't know which methods are done, and which aren't. Instead you should use async Task which can be returned and awaited.

Jakob Busk Sørensen
  • 5,599
  • 7
  • 44
  • 96
  • Thanks a lot for your answer. I tried your sample and the Task.Delay() really runs async for I can click on UpdateFast before UpdateSlow ends. But I would like to let the async task do something like filling a string with stars, one star every 200 msecs. During that I would like to add a hash by clicking the other button. That works fine with my sample but I find no way to do it with task / await. – tom2051 Jan 30 '20 at 18:15
  • @tom2051 See the added example. That should do the trick (if it's only for fun). – Jakob Busk Sørensen Jan 30 '20 at 19:17
  • Thanks indeed, this is very close to my sample, where in difference I expliciteley use a new thread calling the function writeStars. I could solve the problem in my real app using about the code in my sample / your sample. I mean I could solve that without await. I still understand it should work like ... start async task, enable button, await on task to end, disable button, During the time the task runs I can add hashs by clicking the button. I could not get that working ... – tom2051 Jan 31 '20 at 12:14
  • I got it running now, the mistake I made, is to place the await outside the async function. When I place the await inside the async function, I can start the function, do some button clicks and after the await I can do something else, thats what I wanted :-) – tom2051 Jan 31 '20 at 12:31
1

I'm a bit unclear what you want - you start out saying you want to start f2 while f1 is running, but then you ask to await on the end of f1 and enable the button for f2. I'm going to answer the second case, where you don't want to run f2 until f1 has completed.

You could use a design such as this (excuse syntax or formatting weirdness, I've not been in C# land for a long time):

async void btnWriteStars_Click() {
  try {
    btnWriteMinus.Enabled = false;
    await writeStars();
  } finally {
    btnWriteMinus.Enabled = true;
  }
}

Task WriteStars() {
  return Task.Run(() => {
    // much stringy goodness here
  });
}

The strategy being that the 'write me stars' event handler disables the button that you don't want to interrupt the async processing, then waits for async processing to complete, then enables the button again.

Note that just making WriteStars async is not enough - it would still run on the calling thread up to the first await, and of course your WriteStars has no await. So we use Task.Run or a similar function to spawn an async task in which to run the synchronous block of code. Depending on what your real method does, you might be able to write that as async instead of explicitly meddling with Task factories - in any case, the key is that WriteStars returns a Task object for the button event handler to await.

If your requirement is more complex - like you want to leave button2 enabled but have queue up its 'append a minus' operation until after writing the stars has completed - then your design will need to be more complex too. You might be able to stash the Task instance at form level and have button2 do a ContinueWith on it, but for truly sophisticated scenarios you might need to implement a producer-consumer approach where the UI buttons put operations on a queue and a worker thread picks them off. But for the simple "don't do X while Y is running" scenario, the disable-await-enable should suffice.

itowlson
  • 73,686
  • 17
  • 161
  • 157
  • Thanks very much for your answer. The thing is I would like to run async writeStars() by clicking btn1 once. While this function runs async, adding a star all 200 msecs, I want to be able to write hashs inbetween the same string every time I click btn2. With my sample this works, but I do not know how to do this with task / await. Maybee this is not a job for task / await. As I understood await, normally I would start a async task, then do something else and afterwards place a await on the task. But I could not get that working that way. – tom2051 Jan 30 '20 at 18:10