Computer Science: INTRODUCTION TO MULTI-THREADING (Golang) | Scott Reese | Skillshare

Playback Speed


1.0x


  • 0.5x
  • 0.75x
  • 1x (Normal)
  • 1.25x
  • 1.5x
  • 1.75x
  • 2x

Computer Science: INTRODUCTION TO MULTI-THREADING (Golang)

teacher avatar Scott Reese, Engineer & Investor

Watch this class and thousands more

Get unlimited access to every class
Taught by industry leaders & working professionals
Topics include illustration, design, photography, and more

Watch this class and thousands more

Get unlimited access to every class
Taught by industry leaders & working professionals
Topics include illustration, design, photography, and more

Lessons in This Class

    • 1.

      Introduction

      1:07

    • 2.

      Processes & Threads

      10:48

    • 3.

      Basic Thread Examples

      19:05

    • 4.

      Building a Threadpool

      16:22

    • 5.

      Wrapping Up

      0:43

  • --
  • Beginner level
  • Intermediate level
  • Advanced level
  • All levels

Community Generated

The level is determined by a majority opinion of students who have reviewed this class. The teacher's recommendation is shown until at least 5 student responses are collected.

203

Students

1

Projects

About This Class

Multithreading is a very important concept in the world of software engineering. It essentially allows the programmer to write applications that can multitask (do multiple things at the same time), which serves to provide significant improvements in speed and efficiency! 

This course will give you a great high-level introduction to the concept of multithreading and will walk you through some concrete examples using Golang to illustrate how threads work. Moreover, you will also learn how to build a threadpool, which is a very popular threading construct often used in multithreaded applications!

Meet Your Teacher

Teacher Profile Image

Scott Reese

Engineer & Investor

Teacher
Level: Beginner

Class Ratings

Expectations Met?
    Exceeded!
  • 0%
  • Yes
  • 0%
  • Somewhat
  • 0%
  • Not really
  • 0%

Why Join Skillshare?

Take award-winning Skillshare Original Classes

Each class has short lessons, hands-on projects

Your membership supports Skillshare teachers

Learn From Anywhere

Take classes on the go with the Skillshare app. Stream or download to watch on the plane, the subway, or wherever you learn best.

Transcripts

1. Introduction: Hey there and welcome to my class on an introduction to multi-threading, which is a very, very important concept in the world of software engineering. Because if you can properly apply this concept into your own programming, it's going to dramatically improve the speed and efficiency of your applications. And on top of that, if you do have a firm grasp on multi-threading and can demonstrate its use cases in an interview that's going to really help you stand out and give you a much higher chance of landing a software engineering job. And so this class is designed to give you a very good introduction into the concept of multithreading. So we're going to start off by talking about the key concepts that you must understand before you can actually start writing code. And then after that, I'm going to show you some very clear examples using goaling of how threads work. And then by the end of the course, we're going to build a thread pool. And if you don't know what that is, then watch this course and you'll find out, it's very, very cool. And also if this is the first course of mine have come across, my name is Scott Reese and I currently work as a software engineer in the financial services industry. And I also have degrees in economics and Computer Science from UC Berkeley. So with that being said, you'll definitely be in very good hands in this course. And now we're going to jump on over to my computer and we'll get things started. 2. Processes & Threads: Okay, welcome to the first video in this course on an introduction to multi-threading, which essentially is just multitasking for computers. So if and when you can't introduce multithreading into your programs, this is going to give you a dramatic speedup of your program execution. And it will be getting into all the nitty-gritty details of how this stuff works in this course. And so in this first video, I just want to cover a few main high-level topics, specifically in this slide. First, I want to talk about what is a process, specifically a process in regards to computer programming. And so a process is simply a computer program that's in execution. So whenever you run your own code or if you just open up a program on your computer that's already installed like Microsoft Word, google Chrome, et cetera. The execution of that program is going to be packaged up as a process. And every process that's running on your computer has its own address space in memory or RAM, right? So for example, the computer I'm recording this video on has 16 gigabytes of RAM. And those 16 gigabytes will be divided into individual blocks of memory, where each block corresponds to a specific process or a specific program that's running on my computer. And every block of memory for each individual process on your computer is itself divided into four main components. We have the stack, the heap, the data or static section, and the text or code section. And here is a diagram to help you visualize how this works. And actually let me back out of this PowerPoint real quick here so you can see my cursor. So you can visualize this entire block right here, this rectangle as the address space that's dedicated for one individual process. And this address space or this block of memory, is divided into the four main segments. The stack, the heap, the data or static section, and text slash code. And starting off with the stack first, what is this? The stack basically contains temporary data such as function parameters, variables within functions, return values, et cetera. Basically, when you call a function in your code, all the parameters that were passed into that function and any variables within that function that are used to do any computation. All of that gets stored in the stack space. And you can see this arrow here, meaning that the stack will actually grow down. It will get larger as your program executes, right? Because when you think about it, when your program first starts, it will call some function. And like I said, that will put some stuff onto the stack space here. And then as that function is executing, a might call another function. And when that second function gets invoked, it will have to allocate its parameters and its variables onto the stack space as well. And then very similar, that second function, as it's executing, might call a third function, and so on and so forth. It's like I said, as your program is executing and more and more functions are being called, the stack space will grow in size. Now eventually once your functions are done executing and they return, well at that point, you're no longer going to need the parameters and variables that were stored on the stack space, so they'll get removed. So ultimately as your program is executing, the stack space will grow and shrink along the way. Now the heap space is for dynamically allocated memory. So basically for any object or for any variable where you can't know the size ahead of time before the code actually executes. We'll during the actual execution of the code, when that variable or object gets instantiated, the memory used for that object will be allocated in the heap space. Now this is where things can get a bit confusing in terms of understanding the difference between a stack and the heap, right? Because depending on the datatype of certain variables, sometimes that variable will get stored in the stack space of the times it might go into the heap. And actually understanding how all this stuff works is not required for this course. So I'm not gonna spend too much time on it here, but just in general, if you had a function and then in that function you made a variable that just stored in integer. Well, in that case, we know ahead of time before the code executes how much memory is going to be needed to hold that integer variable value and might be 32 bits, it might be 64 bits, just depends on the exact data type you're using. But something very simple like that would get stored in the stack space. And then if also in that function you had another variable that stores some data structure object, maybe a LinkedList, maybe a HashMap or something like that, where the size of that object and actually grow and shrink. It's not known ahead of time, then the memory used for that variable for that object will be dynamically allocated in the heap space as the code actually gets executed. So very similar to the stack space, the heap space will also grow and shrink as well as your code is executing and you're creating more objects or removing more objects, all of the memory allocated for those objects will grow and shrink in the heap space as well. Next we have the data or static section, and this is simply where your global variables or your static variables will go. So basically if you have any constants or variables that are defined outside the scope of any function, which therefore makes them global, then they will go in the data section of the address space allocated for your entire process. And then finally, we have the text or code section. And this basically just stores the actual instructions to execute your code. Basically once your code is compiled and converted into computer code or into binary, those binary instructions will get stored in the text section of your process address space. So in summary here, once you boot up a process on your computer, and that process gets loaded into your memory or into your RAM. Your computer will look to the text or code section of that process to start executing the actual instructions of your program. And as those instructions are being executed, the stack space will grow and shrink based on the parameters and variables that are being used by your functions in your code. And alongside that, the heap space will grow and shrink as well. And if your code also requires the usage of any global constants or variables, they'll simply access those in the data section right here. And that's it. That's what a process is. And that's generally on a high level how it all works. So now let's come over to the next slide here. And we're going to discuss what is a thread. And a thread is simply a single flow of execution through your process code. So basically the code you're executing in your process, in your program, a thread is one single execution of that code. And so with this definition, I'm basically implying also that you can have multiple executions of the same code. And so each thread maintains its own program counter and stack or stack space within the process. So each thread is its own isolated execution of your program code. And each thread can be executing different parts of that code at the same time. Now, outside of the stack space, outside of its own execution of your code, threads do share all other memory in the process. They show the heap, they show the data or static section, and they also share the text or code section. And threads are a lot more lightweight than a full fledged process, which basically means that there's a lot more overhead and it takes a lot longer to boot up a full process than just a single thread. It's very quick and easy to start up and stop threads to swap them out on the CPU, et cetera. And this is one of the reasons why multithreading or using multiple threads in your process is such a very powerful tool. And so the purpose of using threads in your program or in your process to execute code simultaneously, right? You can have multiple copies of your code running at the same time or to replace other threads that are currently blocked. And what I mean by that is let's say one thread, as it's executing your code, has to communicate over the network with your database. Perhaps it's talking to your database and it wants a certain set of data to be returned back to it. And that can take some time, especially if it's a lot of data you're trying to pull back from your database. It could take a few seconds or even a few minutes. And so while your database is doing that, while it's gathering the data, you're a thread is just sitting there waiting, doing nothing. And that's burning valuable time on the CPU that could be used for a different thread that can actually keep executing code. And so you'll see in more detail how this is all going to work in the coming videos in this course. And once again, I have a diagram just to show everything in a visual format. And once again, let me back out of the PowerPoint here so you can see my cursor. So very similar concept. This blue box represents your entire process. And in this case we have three different threads that are running three different flows of execution of the same code. And so like I said, each thread has its own stack space, its own independent execution of your code. But as you can see within this purple dotted box, these threads do share the same data or static section of your memory. They show the heap space and they also share the code or text section as well. And hopefully this design should make some sense to you, right? I think it's pretty obvious that all these threads should be sharing the same code section because they're all executing this same exact code. Now they might be in different places of the execution, but there's still all executing this same exact code. And then for the data or static section, we're the global constants or global variables are stored well by their very name. These are global variables that are global constants. So anything in here should transcend the barriers that are in place. Isolate each individual threads execution. All threads should have access to the same global constants and global variables. And then for the heap space, this is a bit more nuanced. You could argue that perhaps every thread should have its own heap space too. But the heap space is a great way to allow for threads to communicate with one another, right? Perhaps this thread creates an object of some sort, maybe a HashMap, and that object would get stored into the heap space. And then perhaps this thread wants to access something within that map or put something into it. Well, it can just go into this shared heap space and do that. And then the first thread can see what the second thread did. How did it manipulate that HashMap? And then it can use that information for its own execution of the code. Again, the heap space is a great way to allow for threads to communicate with one another. So this is exactly what a multithreaded application or multithreaded process would look like. And this is what will allow your computer program to do a lot of multitasking, to do a lot of things at the same time. And of course, as I said earlier, that we'll introduce a lot of speedup into your application. And so in the next video coming up, I'm going to show you some very basic examples using goaling of how threads actually work. So thanks for watching, and I'll see you in the next one. 3. Basic Thread Examples: Okay, welcome back to the next video in this course. And in this video here, I'll be showing you a few very basic examples that will demonstrate how threads actually work. And so what I already have written here is just a very basic main function written in goaling. And so this is the function I will execute that will actually show you how the examples work. Now before getting started here, I just want to say that if you do not know goaling, then don't worry, it is absolutely not required to understand goaling for this course. And that's because one goaling is very easy to understand. It's a very clean language. And the number two, I'm going to walk you through in very great detail how the code I'm about to write will actually work. So for this example here, I'm going to create a new function. This function is going to contain the actual work that's going to be executed by either the main thread, which I'll talk about in a second or any other threads. So I'll go ahead and write func and we'll call our function do work. And it will take in one parameter and it's going to be an id, which you'll see how that works in a second. So that's going to be an integer, and that's it. So this is how you actually create a function and go Lang, it's very simple. And now in this function, I'm simply going to make a for-loop that will print out the numbers one through 10. So I can type in for I equals 1, I less than or equal to 10, I plus, plus, and then brackets. So basically what this is going to do here is create a variable called i. It's going to instantiate it to be the value of one. And then every time this loop iterates, it's going to increment the value of I by one. That's what I plus plus means. And this will become a lot more clear once I actually run this code. And so then in this for loop we're just going to have a basic print statement. So FMT dot print f. And this is where the ID parameter is going to come into play here. So the text that's going to get printed out by this print statement is going to be thread percent d. And you'll see what that means in a second% d again, and then a newline character. And so those Percent d symbols are basically just placeholders that we're going to actually fill in with whatever values that we want. And the D specifically means digits. So these are going to be numbers. And because we have two of them here, that means we need two arguments, two integers to actually get placed into the message here. So the first one is going to be the ID, and the second one is going to be I. And that's it. That's our work function. And then in the main function here, we're just going to call it. So we just type in do work and we'll pass in the value of one as our argument. That's the id value. And that's it. We'll save that file. And oops, I forgot to import the FMT package. Let me do that real quick. So right here I'll type in import parentheses and then FMT save the file again. There we go. So basically what this code is going to do once they do work function gets executed. We're going to hit this for-loop where like I said, I is going to be initialized to the value of one. And so in the first iteration of this loop, it's going to print out the message thread 1, right? Because the ID here we're passing in is one. So thread one colon, and then it's going to print out the value of I, which is going to be one for the first iteration of this loop. And then for the second iteration, the ID will not change, but I will get incremented by one. So that means I will become 2. So the second message that will get printed will be thread one, colon two, and then so on and so forth all the way until gets to 10. So as a demonstration, let's bring up the terminal here. And first we'll go ahead and compile this code. So typing go build. And the name of the file is threads dot go, press Enter. All right, it's compiled and then we'll run the code here. So dot slash threads. There we go. Like I said, as you can see here, we have 10 messages and they've all been printed by threat of one. Now as I mentioned earlier, there is a main thread, every program, every process has to have at least one thread that is the threat that actually runs your code, right? And that initial thread that gets booted up and runs your code the first time, that is the main thread. So whether your process is just single-threaded, if there's just one thread, or if your process is multi-threaded, in either case there is always one main thread. And so again, in this case we just have a single threaded process. There's just only the main thread that's executing. And as you can see here, it just went through that for-loop and printed out numbers one through ten. Very simple. So now what I'm gonna do at this point is I'm going to introduce two more threads. So we're still going to have the one main thread that will never change. But on top of that, we're also going to have two other subsidiary threads. So back to the code here. And so in goaling here, the way you create a thread is actually very, very simple. All you have to do is before you make a function call is type in the word go, That's it. We're going to have go do work one, and then we're gonna do it a second time. Go do work too. So now what's going to happen here is once I run this program, the main thread is going to start up and it's going to first execute the main function. And once the main function gets to this line, is going to kick off or create a totally new subsidiary threat. And once that subsidiary thread is ready to go, that thread is going to start executing the do work function. And once that's happening, once that new thread is executing this for-loop, the main thread can continue forward and execute more code. So the main thread then comes to this line, go do work too, and that kicks off another thread. Which would be the third total thread in our program here. And once this Third Thread is up and running, again, it will also execute the same do work function. So tying back to the previous video, I hope you can see here that threads basically execute this same exact code. Now there can be some subtle differences. In this case, the parameters that we're passing in are going to be different. The IDs are going to be different basically, but the code that actually runs afterward is identical. And so finally, once the main thread is done with this line, once that main thread has kicked off, this second subsidiary thread, it's going to move forward. And so finally I'll add a print statement down here. And the message is it's going to be main thread done. And after that the program will terminate. And then actually one more thing I'm gonna do here is change this loop to instead go all the way to 100. So now once each of these subsidiary threads gets kicked off and starts running, they're each going to loop through numbers one through 100 and print out this message. So finally, let's bring back the terminal here and we'll compile our code just like that. Now before I run this, there's actually going to be something weird that's going to happen here. This code actually has a problem. So when I hit execute, all you see is the main thread printing out done. But what about the other threads? There's opposed to each print out the numbers one through 100 each. And so the issue here is the main thread is actually finishing. It prints out this message and then terminates the whole program and does so before these other threads can actually finish their loops. And so this is a very key point that you want to keep in mind about the main thread. The main thread is basically the boss wants the main thread is done, the entire program is done, and any other subsidy or thread that's running gets killed immediately. So again, just to recap here, we start the program. The main thread executes the main function. It kicks off two subsidiary threads, and both these threads go off and they run independently on separate processors, right? So that means the main thread and these two subsidiary threads are all running at the same exact time on my computer. They're just running on different processors. And once these two threads are created and the running this code, there are looping through their numbers. Then the main thread can continue forward and execute its own code down here. So prints out this message and then it terminates. That's all the code that's left. And again, the main thread gets to this point. It prints out its message before the two subsidy or threads that are elsewhere running on different processors can actually finish executing their own code. And that's why none of their print statements actually came out and got printed when I ran the program. So what this means now is I have to find a way to make the main thread wait, wait for its two subsidiary threads to finish before actually moving on to the print statement and then terminating the whole entire program. So now this is where I introduce threads synchronization, which basically means we have to coordinate all the threads that are running in this program in such a way that the entire program actually executes in the way that we want. Because right now as you just saw, this program is not executing properly. So in order for the main thread to actually wait for these to other threads to finish, we have to use what's called a weight group. First I have to import another package and it's gonna be called sink. And then from the sink package, I can create a weight group. And I'll explain what that means here in a second. So it's just going to be a variable. I'll call it WG for short. And like I said, is going to be a weight group just like that. And so what awake group is, it's basically just a counter that every thread can access. So even if we have all the threads in this program executing on different processors, on the CPU on my computer, they can still all access the same exact weight group or same exact counter. And so now I'm gonna do, is I'm going to initialize this counter with a value of two. So I can just type WG, add two. Very simple. I made a counter and it has the value of two, That's it. And so the next step is when these do work functions get executed by the subsidy or threads that get created is I'm also going to pass in this weight group variable into the function. So first in the actual do work function, I have to make room for a second parameter. So it's going to be WG and it's going to have the type of a weight group, of course, just like that. And then now I can actually pass in my new weight group object into these function calls. In here. I'll just type in comma w g. And then same thing here, comma WAG. Don't worry too much about what these symbols mean. This ampersand and this star symbol here. This basically just means I'm passing in the actual memory address that corresponds to where this weight group is actually stored, not just a copy of the actual object itself. And that's because I want all the threads to access the exact same weight group, the exact same counter. So we can't have multiple copies of the counter in different places in memory. We just want one counter in one specific place in memory. And we want every single thread to access that exact specific address in memory. So they can all use the same counter. Okay, so now in here, once our new threads have finished executing this for-loop, we need some way for these threads to signify that they're done. And the way we do that is we just type in WG dot done. That's it. And what this is going to do is it's going to decrement the count in our wake group by one. So first I initialize the count to be two. And then once a thread finishes running this for loop, and it calls the W1 function on our weight group object. It's going to decrease the count in that weight group by one. So if this first thread here that gets created, if this one finishes first and it calls done, that means that count in our wake group will go from two down to one. And then once the other thread finishes and it also calls done, that will also reduce the counter from one down to 0. And so now finally, we need a way for the main thread to actually know when these other threads are actually done. Because right now all we have is a counter that's initialized to two. And then we have our other threads that simply decrement the counter by one. Well, how is that going to be useful for the main thread? And the way we make it useful is before our last print statement here, we call WG dot weight. So this function, this weight function is going to execute and then the main thread is going to stop because the main thread is the one that actually executes this weight function call. So the main thread is going to stop. It's going to wait until the counter has a value of 0. So we first give the counter a value of two. Then the main thread kicks off two more threads very quickly. Comes down here, takes a look at the value that's in the counter. It's probably still too, that's not 0 obviously. So it stops, it waits. And then while the other two subsidy or threads are running and they're executing there FOR loops. Once they're done, they obviously call the W1 function on the weight group, decreases the counter by one. So naturally once both threads are done, the counter will go from two down to 0. And once that happens, the main thread will continue forward. You will print out it's done statement, and then finally the entire program will stop executing. So let's save this file here, bring back the terminal. There we go, recompile the code, and now we can run it and it should work just fine. So hit enter and there we go. So as you can see here, both threads, both of these subsidy or threads I should say, were able to run and execute their for-loops and print out all their messages, numbers one through a 100. So let's scroll to the top here. And so because thread one got created first, that's why you can see it's print statements first, thread 1 and thread 1 and thread 1, so on and so forth. Now if I scroll down here, you can see the first message from Thread 2. And this happened right after Thread 1 finished printing 27. So obviously thread 1 still has a long way to go at this point to finish all the way to 100. But because both threads are sharing just this one terminal, they have to both compete and fight for printing out their statements. So ultimately what that means when I ran the code here is that you basically get a flip-flop back and forth between thread 1 and thread 2, printing out their statements. So thread one printed out its messages for quite a while. And then thread 2 gotta turn that you can see here. And I'll keep scrolling. And then back to thread one again, keep scrolling. And then thread 2 got a few more print statements out. Back to thread one, so on and so forth. Like I said, it's just flipping back and forth between the two threads. Now don't confuse this for the two threads actually not running at the same time. They are, like I said, they simply have to compete with trying to print their statements out to one terminal. But in reality, both of these threads are running at the exact same time. And then finally, once both threads have printed out numbers one through 100, at that point they're both done, which tells the main thread to stop waiting, and then it can print out its last message. The whole program then terminates. Now there's one last thing I want to show you here in this video, which will basically prove that multithreading can really speed up your program. So now I'm going to comment out this for loop here. We don't need it anymore. So a slash star and then star slash write. This basically prevents this code from getting executed. So this for-loop is not going to run. And then beneath it, we're just going to have one line. And it's going to be time dot sleep. And then we'll do three times time dot second. And then I also have to import the time package. There we go. So what this is going to do is once both of these threads get spun up and they're running, they're going to hit this statement. And they're basically going to go to sleep. They're just going to stop executing for three full seconds. And this is basically meant to mimic perhaps making a call over the network to your database, right? I mentioned that in the previous video. Perhaps you want to communicate with your database to pull in some data that can take some time. It can take a few seconds or a few minutes. And that's what this is meant to mimic, right? Because once your thread and makes that call to your database saying, Hey, I want this data. The thread has to just sit there and wait and not do anything until the database returns that data. And then what I'm also going to do here is I'm going to remove these go keywords here because I first want the main thread and only the main thread to execute all this code. So basically all this weight group stuff is not going to do anything because now by removing those go keywords, we're now left with a single threaded application or a single threaded process, just the main thread. So that means once the main thread gets to this point, it itself has to execute the work function means the main thread is going to sleep for three seconds and then it's going to wake up and move on. Which then means it's going to execute the work function a second time. Which means the main thread is going to, again, going to have to sleep for three more seconds. And then finally it'll be done. So that means the full runtime of this program should be six seconds. So I'll save the file, bring back the terminal, and let me clear the screen real quick and move this over here. Go ahead and compile the code and run it. And actually I'm going to add this time command beforehand. This basically just records the runtime of the program I'm about to execute. So press Enter, and it should take 64 seconds for this thing to finish. And there we go. Runtime exactly six seconds. And again, that should make sense because we just have a single threaded application, just the main thread. And the main thread has to sleep two times for three seconds each time. So six total seconds. And so finally now I'm going to add back the go keywords. So go do work, go do work, just like before. And so now we're going to have two subsidiary threads get kicked off and they're each going to independently sleep for three seconds. And they're going to do so at the same exact time. Which means now this program should execute and only three total seconds, right? Because this sleep command is not executing sequentially before with just the main thread. The main threat had to sleep for three seconds, then wake up, and then sleep again sequentially for three more seconds. So that's why it took six total seconds. But now we're making two subsidiary threads that are both going to sleep for three seconds at the same time. So finally, we will save the code, bring back the terminal, and we'll go ahead and compile it and then run it. And like I said, this should take exactly three seconds. Now. There we go, only three seconds. So now you can imagine if you had a multi-threaded application or each thread was making calls to your database or doing some sort of computation. All that stuff could be happening at the same exact time, not one after another. And so of course, when you're able to multitask, do multiple things at once, That's going to make your program run a lot faster. And with that being said, that's gonna do it for this video. And in the next one, we're going to take things one step further. And I'm going to show you how to create a thread pool, which is basically a queue, a queue that contains a bunch of tasks or work to be done. And then you'll have a pool of threads. Or workers basically could be dozens, hundreds, or even thousands of them that will grab a task off the queue and perform their work independently. So you can basically think of it as an assembly line of sorts. So I'll see you in the next one. Thanks. 4. Building a Threadpool: Okay, welcome back to the final lesson in this course. And so now in this video here, we're going to create a thread pool, which is going to build off the previous video where I showed you a few examples of how threads work in general. But now we're gonna take it one step further and we're going to implement this design you can see here in this picture. And so a thread pool, conceptually speaking, is very simple. It is just a collection of threads or collection of workers that are just sitting there and waiting to pick up a specific task or a job to do. So it's very similar to an assembly line. This job queue here represents the assembly line which contains the various tasks or work to be done. And then your worker threads are waiting to pick up one of these tasks and complete it. And then of course, over here, you have the producers of the work that needs to get done. So these guys put a task onto the job queue, and then once one of the worker threads becomes available, becomes free to pick up a new task. It will pick it off of the queue and then run that task. So this model here is a great way to speed up the execution of your program. If your program has a lot of different things that need to get done. And specifically, of course, if the things that are getting done, your program can be done at the same time, or at the very least, these jobs are totally independent, then this would be a great use case to use a thread pool. So finally, we're gonna come over here and we're going to create one. So just like in the last video, I have some very basic Go code already written. Pretty much just the main function and a few variables here, right? This flight package, for example, allows you to pass in some specific parameters into your program when you run it. And you'll see how this all works here in a minute. So the first thing we need to do here is create our queue that is going to hold all the different tasks or jobs that need to get done. And so up here above the main function, I'm going to create a global variable that's going to hold this Q. So I can just type var for variable and I'll call the queue work queue to make it obvious. And the datatype of this queue and goaling is going to be Chan integer. And just for your awareness, Chan is short for channel, and the channel and goaling is basically just a q, except it has some added features that allows multiple threads to try and access it atomically. Basically just one at a time, right? Because for example, you wouldn't want to threads trying to grab the same job at the same time. Only one job can go to one thread. So that's why you want to make sure that the data structure you are using Azure queue is capable of handling multiple threads, trying to access the contents within it at the same time. And so in this case with a channel, if two threads were to try to access the channel or the queue at the same time, only one is going to be granted access to grab a specific job off the queue. And once it has its task, then the second thread will be granted access to the queue. And so now the next step is we have to actually instantiate our q. So in our main function here, after we gathered the input parameters, we're just going to write work q equals make chan int comma 1, 0, 0, 0, 0. So this built-in function that comes with goaling is what you use to actually create or instantiate a queue or any other data structure. And this 100 means we are allocating 100 spots or 100 positions in the queue. So basically this Q can have 100 tasks sitting and waiting there at a time. And that's definitely more space then we'll need, but we'll just keep it at 1000. Okay, So next we have to create a new function that's going to create our worker pool or our thread pool. So this function is going to be called create thread pool. And it's going to take in two arguments, and it's going to make it an integer that corresponds to the number of threads that we want to create, the number of workers. And then also like in the last video, it has to take in a weight group because this is how we're going to coordinate all the threads that are running at the same time potentially. So once again, I'll use EWG for short. And the type of course is going to be a weight group. And there we go. Next we're going to create a for-loop here that's going to actually create all these threads. So I'll type for I equals 1, I less than or equal to num threads, I plus, plus brackets. Now before we actually create the thread, we have to increment the counter that's in the awake group, right? If you recall from the last video, that counter in the weight group initially needs to be set to the exact number of threads that we're going to create. If we have 20 threads, 20 workers in our pool than the weight group counter needs to get set to 20. So in that case, we can just type in WG dot add one. So now for every thread that gets created, we're going to increment the counter by one. And then down below we can type go. And of course we need to actually have a do work function that these threads can execute. So the next step is to actually create this do work function again. So down here, I'll type in funk do work. And this function is going to take in two arguments. It's going to take an ID, just like in the previous video, that's going to be an integer. And then also it will take in the awake group. Because again, we need to synchronize all our threads to be able to notify the main thread when they're all done. So sinc dot weight group and then brackets again. And so that means back in our create thread pool function, we can complete the invocation of do work for each thread that we're creating. And so I'll pass in I, that's going to be the ID of each thread and the weight group. And there we go. So now when I run this program, and let's say I pass in the number of 20 for the threads parameter. That means our program here is going to spin up and create 20 threads, 20 separate threads apart from the main one of course, and all 20 of those threads are going to run that do work function. So now back in our main function, after we instantiate the work queue, we can call create thread pool. And we'll pass in the num threads parameter. That'll pass him when I execute the program and the awake group which I forgot to create. So actually above the word queue, I'll make it right here. I'll do a var WG, sink dot, weight group. There we go. So now that I've created the actual weight group, I can pass it in to our create thread pool function here. And once again, this ampersand means I'm passing in the actual memory address where this weight group is actually stored. And that's important because we want all the threads to access the exact same weight group. So at this point, we've created our thread pool and we've created our job queue. But now we need to actually create the work producers. Let's come back here and do just that. So now down here below are do work function. I'm going to create one more function called q work. And this is the function that's going to actually put tasks onto the work queue. And it's going to take in two parameters or two arguments, the first of which is going to be num tasks. So how many jobs do we want to put onto the queue? And then the second one is going to be sleep time, which is also going to be an integer. And there we go. So just like in the last video, in order to simulate or mimic work that's actually being done, we're just going to have our thread pool or each individual threads within that pool. We're going to put them to sleep for a certain amount of time. And that's going to represent those threads actually doing some sort of work for a few seconds. And actually both of these arguments are going to be passed into the program once I execute it, right? You can see sleep time here and NAM tasks. So this functions can be very simple. We're going to have another for loop. So I'll say for I equals 0, I less than num tasks, I plus, plus brackets. And the way in Golan you put something onto a work queue or a channel I should say, is we just type the name of the word queue, which is where Q. And then we make this little arrow symbol. And then finally I write sleep time. So for example, let's say num tasks is 20. We have 20 total tasks that the worker threads need to get done. And each task that we're passing onto the queue simply corresponds to how much time we want each thread to sleep. So if sleep time is three seconds, we're going to pass the number 3, 20 times onto the queue. And for each worker that picks up that number, the number three, it will then notice sleep for three seconds. And then once that's done below the for loop, after it completes, we're going to type in close work queue. And this close function here simply indicates that we're no longer going to put any more work onto the queue. We're closing the queue. And this is very important. You can't forget to do this because if you do forget it, workers in a thread pool will have no idea when to stop trying to grab more work off the queue. They're just gonna keep sitting there and waiting forever. So again, you need some way to indicate that there's no more work to be done. So that's our q work function. And then back in our main function, we can call it. So I'll type in queue work and I'll pass in num tasks and sleep time just like that. And so finally, we have two more main steps to complete. The next one is to actually fill out the do work function. So again, keep in mind that every single thread that we create is going to be executing this function, some of which at the same exact time. Now I will say if we create maybe a 100 threads, well, I don't have a 100 processors on the CPU of my computer. I only have eight. So at most we can have eight threads running at the same time. But also keep in mind that once a thread goes to sleep, it no longer needs to be running on the CPU. In which case, any sleeping thread will just get removed from the processor altogether. And that way it will make room for new thread to come on, come on to the CPU and start running. And I'll explain this again in a minute here. So each thread that's running the do work function is going to continuously try to pull some task off of the work queue. So we'll type for sleep time in range of the work hue. We'll do time dot sleep and then time dot duration, sleep time times, time dot second. So basically each thread that's running the work function is going to continuously loop through and try to pull something off of the work queue. And of course, what comes off of the queue is a certain duration of time to actually go to sleep. So it pulls that sleep time off of the work queue and then it just sleeps for that amount of time. Don't worry about this time dot duration function. This is simply converting the sleep time into the right datatype to actually multiply against the time dot second datatype. But again, going back to my earlier example. Let's say the sleep time is three, corresponding to three seconds. That means when this function gets executed, each thread is going to sleep for three seconds. That's it. Now once the work queue is empty, and again, the threads will know the word queue is empty because we closed it once we were done putting stuff onto the queue. Then at that point this for loop will break. And the last thing each thread needs to do is call the method on the weight group, right? Because if you recall from the previous video that done function decrements the counter in the awake group by one. And once the counter gets down to 0, that's when the main thread knows all the subsidiary threads, all the worker threads, totally done executing. So finally the last step back in the main function. Now, after the main thread creates the worker pool, and after the main thread, cuz all the work to be done. Now the main thread has to wait. It has to wait for all the worker threads to finish their work. So we call it W G dot weight, just like before. And then once all the threads are done, we'll print out a message and the message is going to be main thread done. And actually one final, final thing, we should also print out a message from each of the worker threads that are actually running. So before each one goes to sleep or print out a message right here. So FMT dot print f. And just like in the previous video, the message will be thread percent d, which is where the ID will go. And then we'll just say doing work dot-dot-dot and then newline character, and then we'll pass in the ID, the thread. So there you go. At this point we should have a fully functional thread pool. Let me go ahead and save the file to make sure there's no errors. And there we go, Everything looks good. So now let's go ahead and bring up the terminal here, and we'll run the program. So first step is to compile it. So go build and the files called thread pool dot go compile it. There we go. And then now we're running it with dot slash thread pool. And here's where I can now pass in the parameters like the sleep time, number of tasks, etc, into the program. So for the num tasks, let's do 100. Then for the number of threads we want to have, let's do ten. So now in this case we have more tasks that need to get done then the threads that we're going to create. So that means of course, each thread is going to have to run multiple tasks. And then we'll set the sleep time to 1 second, which again is going to simulate or mimic the work that's actually being done by each worker thread. So now I'll press Enter. There we go. You can see each one of the 10 threads got creative. And they're all completing their own individual tasks independently. And should be almost done. And there we go. So for example, here's what I mean when I said that each thread is going to have to complete multiple tasks. So thread one is doing some work. And then once I've finished its work, it picked up a new task later down the road and completed that work. And notice how quickly the entire program finished. It was just a few seconds. We had 100 tasks to complete, but because we had 10 threads executing those tasks, some of which at the same time, we achieved a lot of speedup as opposed to if I did everything sequentially. So now if I change the number threads just to one, so now just one thread completing 100 tasks, it's gonna take a lot longer. See you, there you go. And I'll go ahead and kill the program at this point because it's just going to take too long. So you obviously see my point here. Doing everything sequentially is going to be a lot slower than doing a lot of things at the same time, or just doing things a lot more efficiently. Like I said, when one thread picks up that task and it goes to sleep, at that point, there is no reason for that thread to be on the CPU. It's doing nothing. The operating system on my computer is smart enough to know that. And it will remove that thread for the time being from the CPU, which then frees up space for a new thread, a new worker thread that just picked up a new task to come on to the CPU and start running. And then once a sleeping thread wakes back up after that 1 second, again, my operating system will recognize that, put that thread back onto the CPU so it can finish its task and then pick up a new one. So basically in summary, my operating system is smart enough to know that any thread that needs to actually execute code, it will prioritize that thread and put it on the CPU so it can run. And the moment that thread is no longer actually running code, if it's just sleeping. For example, in a real-life situation, if a thread was waiting for a remote database to return data to it, my operating system will know to just remove that thread for the time being. There's no need to have it on the CPU and just waste time. So even though my computer may only have eight processors, so that means technically speaking, only eight threads could be running at the same time. But because of how efficiently my operating system will orchestrate all the threads and my program. It's almost as if all the threads are still running at the same time, right? I could create 100 threads or a thousand worker threads. And it will still appear that a lot of them are running at the same time, even though they may not be. Now of course, this only works up to a limit. You can't have 10 billion threads and expect them to all seemingly run at the same time. There is a ceiling to this, but you get my point. So with that being said, that's going to wrap this video up. I hope this all made sense. And in the final video coming up next, I'll just be wrapping some things up and then I'll send you on your way. Thanks. 5. Wrapping Up: Okay, Can got some finishing this course. And by now you should have the basic tools and information that you'll need to start designing multi-threaded applications. And to help give you some practice with that, you can take a look at the course project down below. And so with that being said, thank you so much for watching this course. I am Scott race again, and I do appreciate any and all feedback that you may have. Moreover, if you've got questions or if you need clarification, something, please let me know in the discussion section of this course below, and I'll get back to you as soon as I can. Please also check out my other Skillshare courses. I've got a few other classes on computer science topics in addition to a lot of courses on options trading and stock market investing. And finally, please follow me on Skillshare platform so that you'll get notified for every new class that I published. So thanks again for watching and happy coding.