Programming in C#, Part 11: Async, Tasks, & Threads

Hello everyone! Well this is awkward huh? It’s been a few years since I updated this particular series. Even when I wrote a couple Unity tutorials that was back in 2020. Well you can all thank my friends Nax and LSD (Those are usernames, if that wasn’t clear), who asked me to write this one.

They wanted to know about async, tasks, threads, all the fun stuff that lets you run code over time. Want to increase a progress bar over time? Thats the type of thing these can be great for!

It should be mentioned, these are definitely veering into the more complex areas of programming. So if you’re confused on what an integer is, you might want to start at an earlier tutorial.

Basic Idea of Threading

Before we leap straight into tasks and threads, I think theres a bit of a preface thats required on threads specifically, to better understand their role as well as their difference to tasks.

Everything is run off of threads pretty much. Every application is run off of one main thread at least, and its essentially a process. Ever open your Task Manager on windows and see all the processes you have going? Yeah, those are all threads pretty much.

Each program you write has one main thread, the start of the application. On each thread, things happen sequentially. If you write

public int Example()
{
    int bob = 0;
    for(int i = 0; i < 10000; i++)
    {
        Console.WriteLine("Progress: " + i + " out of 10000");
        bob += i;
    }

    return bob;
}

Then on this thread its always going to run from the top to the bottom. bob will be defined first, then it will run through the for loop, then it will return bob. It will always run in that order.

But lets say you want to run a really long function. This function maybe has to download something, or its just a really long math equation that takes a while. Well, on a single-threaded application (a program that only runs on the main thread), while its doing that long function, it can’t do anything else. If you have a user interface, none of the buttons will work because its dedicating its entire life to run that function. Normally it would hop around to running small things, and then checking if you’re trying to hit a button. But if its tied up trying to run your long math equation, it can’t do that. So this is why your program might freeze up during that time.

So how do we solve that? Our two friends in this department are tasks and threading!

Tasks & Async

Our first topic will be going over tasks, and async which is tied up in the same concepts. At a base level, you can use these to help our program keep hoping around and do what it needs without freezing up. It can also be used to add delays to things in a pretty easy manner.

It should be noted in order to access tasks, you’ll need to add using System.Threading.Tasks;  to the top of your program.

So you want to make a task? How ambitious! Lets do that, its not super hard. We use the async keyword to let our task know we’re going to have some delays and such going on inside our task. Lets convert our Example() function. First we need to simply add the async keyword before the return type to let our program know we’re going to run this function asynchronously. Additionally, we need to change our return type to a Task, this allows us to delay our function. In this case however we’re also returning a value. Because of that, we will use Task<int> which will allow us to use a task AND return an int.

Then, in our for loop we’ll add a very short delay. This way every loop it takes a break, this lets the program go do its other business like running buttons. We could also have a longer delay for pretty much whatever presentation needs you might have.

So what does it look like now? Lets take a look

public async Task<int> Example()
{
    int bob = 0;
    for(int i = 0; i < 10000; i++)
    {
        bob += i;
   
        Console.WriteLine("Progress: " + i + " out of 10000");
        await Task.Delay(1);//1ms
    }

    return bob;
}

So each loop we’re waiting a very short 1 millisecond, so it should still be accomplished very quickly while allowing our program to handle its other duties, all still on one thread.

Now how do we run our task? It’s not too difficult, we can just use Task.Run(Example) and it will begin! But don’t forget, this task also has a return type, so we’ll want to get that. Task.Run returns a Task<int> because thats the type we specified on our function. So we can store that for later. We can also wait on the task to do something else if we like. We could run it like this, and then we can use .Wait() to wait until the task is done to show the result.

pubic void Run()
{
    Task<int> coolTask = Task.Run(Example);

    coolTask.Wait();

    int result = coolTask.Result;

    Console.WriteLine("Result: " + result);
}

Now isnt that nifty? However, you may notice this runs into a similar issue… we’re waiting to finish our task, which locks our thread to waiting anyway! So whats the point! Well if you’re clever, you could use another task to wait on it, and then use that result later, while we do other cooler things.

pubic void Run()
{
    Task.Run(WaitForBob);

    while(true)
    {
        //Do other COOLER things, like checking if you pressed a button
    }
}

private async Task WaitForBob()
{
    Task<int> coolTask = Task.Run(Example);

    coolTask.Wait();

    int result = coolTask.Result;

    Console.WriteLine("Result: " + result);
}

So with this, the WaitForBob() task will run, waiting for bob, but not lock up our main thread, which is doing other cool things.

There are other cool things that can be done with Tasks, and I encourage you to explore them yourself. You can cancel tasks, there are other ways of making tasks as well, etc. Check out the Microsoft Docs for a ton of information.

Cool Kid Threads

Now that you’ve learned the beauty of tasks, you may ask me: “Yo programmer guy, why would I use threads? Tasks are so cool!” Well let me tell you anonymous reader, threads are ALSO cool. They’re also a bit more mature in that they’re lower level. Tasks were made to be pleasant and easy to use, threads are interacting with lower levels of your operating system and such, to create a new thread, a new process to run.

It’s important to remember while tasks seem to be happening at the same time, everything is still single threaded. They’re technically still being run on your main thread, so your program is simply peaking back by to check up on your task, to make sure its doing well, still progressing, didn’t forget its lunch, etc.

A brand new thread doesn’t have to care as much. Its moved out of the house, has its own apartment and a stable job. Its not quite a main thread like its dad, but it has all it’s parents support to make its own choices all on its own. This can be good for really long and/or intensive operations that need to be done. Threads also take more resources, they’re allocating an entire new thread! A whole new process to play around in.

How do you make a thread? Well I’m sure glad you asked. Lets go back to our bob example, it’s pretty simple. It’s a lot more like writing a normal function actually, we could take our very first example and be fine! I’ll call this one CoolThread so we still have both.

Note, here you’ll need to add using System.Threading; at the top.

public int CoolThread()
{
    int bob = 0;
    for(int i = 0; i < 10000; i++)
    {
        Console.WriteLine("Progress: " + i + " out of 10000");
        bob += i;
    }

    return bob;
}

See? Just like our original, no fancy keywords or anything! Now, to run it isn’t too hard either. We can just do this

public void Run()
{
    Thread niceThread = new Thread(CoolThread);
    niceThread.Start();

    while(true)
    {
        //Cool stuff, buttons, you get it
    }
}

Then your thread runs! Though threads can be a bit more tedious to actually return a value. Notice we haven’t done that in this case.

In order to handle that, if we want to return a value to our main thread its more annoying. This is the type of thing better suited to tasks, where threads are better at running and handling their own set of independent duties. But if we do want to accomplish that, we could do this.

public void Run()
{
    int result = 0;
    Thread niceThread = new Thread(() => { result = CoolThread(); });
    niceThread.Start();

    niceThread.Join(1000);

    while(true)
    {
        //Cool stuff, buttons, you get it
    }
}

Here we define a variable to hold our result, then create an anonymous function that calls our thread, capturing the value. Calling .Join() joins the child thread back to the parent. Its like the kid coming back home to their parents to live for a while. The 1000 is a delay that it waits before joining up again.

You may also notice it executes much faster than the task. It doesn’t have to wait milliseconds like our task did, so its able to accomplish what it needs done much faster.

As with tasks, there can be a lot more to threads, and as mentioned they’re lower level, and have been around for a while. You should again take a look at the Microsoft Docs to get an idea of all of their uses.

Conclusion

I hope this explanation will help you to write all sorts of cool programs that don’t lock up your main thread! Lots of cool possibilities and options with both Tasks and Threads. Now go into the world and use them!

Support

Are you having trouble with understanding this tutorial? Please feel free to contact me via email at KoseckCory@gmail.com or message me on discord at 7ark#1120.

I would love to get feedback from people so I can add and improve these tutorials overtime. Letting me know what you’re confused about will let me update the tutorials to be more concise and helpful.

If you’re interested in supporting me personally, you can follow me on Twitter or Instagram. If you want to support me financially, I have a Patreon and a Ko-fi.

0 comments on “Programming in C#, Part 11: Async, Tasks, & ThreadsAdd yours →

Leave a Reply

Your email address will not be published. Required fields are marked *