Thread Pools in C (using the PTHREAD API)

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
in today's video i want to take a look at threadpools what are they and how to implement them using the pthread api uh first thing first what what are they well they're just a simple way of handling tasks in a multi-threaded manner so suppose we have something that generates long-running tasks right it could be a simple program that looks at the file and so on and so forth and there's some calculations every so often and those stocks could be a lot like let's say 20 30 100 tasks okay and the idea is since you know every computer nowadays has multiple cores on its cpu so if we were to just uh iterate over each task and execute it sequentially well it wouldn't be that efficient we would only be using really one core so uh we would like that if for example there are three tasks that have to be executed that like they are created at the same time they should also be executed at the same time so these thread pools are what are gonna help us understand or execute these tasks uh multiple tasks at the same time but before we start i want to give an overview over what exactly we need to implement because that's quite a bit and i think it will help first things first a thread pool usually has a set number of threads always running right so the trade is either executing a task that it has been given or it's just waiting for a task to appear in the queue so i'm gonna denote that that threat pool with this rectangle here i'm going to say let's say four threads let's say that we have four threads in this strat pool in platform okay and the next element is the task really and the task is going to be found inside the task queue right so i'm going to have here a task queue it's going to be probably around here let's say and uh now it's a queue because the tasks are the tasks are going to be taken from the uh queue in the order that they were inserted so as we don't have any issues with ordering of the tasks right and lastly in the picture we do have the main thread but this could be really anything that is using our uh our thread pool so i'm gonna see here main thread but just keep in mind that in it might not be the case it just might be a user a simple user like that now let's think about the operations that we do between all these right first things first the most obvious one is to be able to create a task and submit it to the task queue right you have something like let's say uh the user the main thread wants you to add two numbers right and um we want to send the task to the task queue and then we'll see from there but that's what the main trend should be able to do in this case we have this arrow drawn here and we're gonna write on it that uh it should be able to insert and create i guess create a task right so that's gonna be that operation next up it should it should be possible for us to uh execute these tracks but first before actually executing the threads we have to get it and assign it a thread right because we have multiple threads we have to somehow take a task and assign it to a track so we're gonna have to have a mechanism for getting a task right and waiting for it and i'm gonna denote that with an arrow here it is on the task queue itself and it's going to be something like uh wait and get a task right this is going to simply either wait if there's nothing to get at the time if the queue is empty or i simply get it and do something with it afterwards now once we get a task there's one last thing that we have to do on the threads themselves and that is to actually execute the task so i'm going to denote that with i'm going to move this a little bit to the top here and i'm going to denote that with an arrow to itself right and this is going to be executing our task and these are the main the three main operations that we should be able to do with the thread now sure we can also send back a result to the main thread but for now for this tutorial we're only going to cover this much the the tasks are not going to be able to give us a result they're just going to let's say print something on the screen and that's it now let's go over first creating the task itself so the task is going to be for our purposes is going to be a struct all right so i'm going to have here type destruct let's say a task and type that to the name of task and very simple and straightforward i'm going to keep the tasks uh just incredibly simple but they they could be uh more generalized but in this case what i'm gonna do is gonna add uh two variables a and b in the task itself and what the task is gonna be is just adding those two numbers that's what the task is going to represent in our case i'm going to have here two variables and that execute task function is going to add them together and print indoor screen or something that's that's all i want the program to do in the late in a later video that i'm gonna probably link up top uh we're gonna find out how to actually execute whatever on those track rules but now it's gonna be very simple next up we have the task queue so i'm gonna have here a simple task array let's say task queue and uh let's give it a match size of 256 that should be plenty of space then of course we have an int that represents the i guess task count really and that's going to be initialized to 0 because we don't have any any task at the beginning and lastly i want a function that executes the task so i'm going to create here a simple void function it says execute task of actually the task itself and i'm going to get a reference to it simply because uh well we shouldn't really create a copy of it there's no point in doing that and what i'm going to do is say result equals task arrow a plus task arrow b now as i said think about this operation is something that takes a long time is a lot of processing and does whatever we're just abstracting here to for you to understand easily and then of course let's bring this result on the screen so uh the sum of percent d and percent d like that is percent d i'm going to print out all the variables here so task of a task of b and the result that we have here okay that's awesome and that's really all there is to executing the task very simple and straightforward i know now we can actually test this whole thing we can simply create a task and just execute it on the main thread so to do so we can just say task i'll say t1 and i'm gonna initialize it like this and i'll say the a variable's gonna be let's say five and b is gonna be something like 10 right and then all we have to do is just say execute task of our t1 here okay if i try to launch this we should be able to get a result let's see and we did get the sum of 5 and 10 is 15. amazing so that works so now that we have the basic block uh for the thread pools mechanism that is the task and actually executing the task this is not really what we want right we want to actually execute the task in a multi-tiered manner right now we're literally executing the task on the main thread sequentially so that's not what we want therefore we should start by creating those threads since you've seen me create threads and uh all throughout this series of videos i'm gonna fast forward through that okay so that's basically everything here i have created here a uh reference to a start trend function and that's going to be our function that waits and gets a task from the queue so here we're gonna have uh start thread and it's gonna take in arguments but we're not gonna pass any arguments now because of the nature of tread pulls basically every thread is either waiting or getting a task and executing it it's never gonna terminate it's never gonna finish its execution therefore we should have actually a while loop here and in this while loop well we're gonna take tasks from the queue so basically what we're gonna have to do is let's say declare here a task and we're simply going to take the first element inside the queue so that is task q of 0 that is because in the queue think of the q as let's say having some integrals just all the integers from one to five what we want to do is since one was inserted first we want to take the first one that was inserted therefore we have to take in one so that's our task q of zero but we also need to move everything to the left so i'm gonna have to make it two three four five and that's what the next step is gonna be and it's gonna be a simple for loop it goes from zero up to task count minus one we don't need the last element and it's gonna say task q of i equals task q of i plus one this week i'm going to release a video regarding uh how to work with cues how to implement the operations on them and uh be on the lookout for that and of course lastly we have to decrement task account because we have taken an element from this task queue now to make sure we don't take an element that doesn't exist we should actually surround all this in an if statement that checks if we have actually a task right so i have here an if statement and uh there we go and lastly well we have to think about this being a critical section simply because the task queue is going to be modified by multiple threads but we don't want for example to trust to take the same task right we want only one to take a task therefore the action of taking from the queue and of course checking if there's anything in the queue is going to be a critical section so it's going to be surrounded by a mutex lock which we have to create let's say up here and i have here put p thread with xt and i'm gonna call it let's say mutex for our cube this is the naming scheme i usually choose just prefix uh every variable name with their type but in this case it's not a mutex it's not a queue of mutexes it's the mutex for the queue all right so i hope it's not too ambiguous i'm gonna initialize it like any other and to use them we're gonna have them in between this if statements i'm gonna have here pthread mutex lock and in here we're having the mutex queue locked and unlocked after we have taken a task from the queue and if everything goes well that means that we can actually execute the task now it's very important to know where we actually execute the task because if we call execute task inside of here well then that that's going to be a problem and that problem is regarding this critical section since we are in between a lock and analog of a mutex that means that this section is being executed by one thread at a time that means that a task is only executed by one thread at a time so we have to actually move it outside this critical section that is very important and of course we have to somehow check if we have a task because right we might not actually have a task so i'm going to say here a a int let's say found i'm going to set it to 0 and if there's something found i'm going to say found equals 1 right and i'm going to say here if found equals 1 then yes you can execute that task that you were given amazing that is our start thread function that is the function it gets and waits for the task now let's go on and implement the submit task function so submit task function is uh very straightforward we have we want to add a task to the queue and simply increment the count right we have here a uh let's say task queue of task count equals task and now we know that the last element in the queue is task count minus one right that's it's index so therefore the next empty slot is definitely task count right that's why that's how we're adding here to the queue and we should actually increment the count once we're done some people actually do like incrementing it in here i see i think this is more readable but you can do it however you want it now because we're actually dealing with the queue here in a multi-threaded program we have to make sure that uh this is properly synchronized and we don't have any race conditions therefore i'm going to add here a p12 mutex lock for again our [Music] q mutex and unlock at the end of this section and that's about it for submitting a task now we have all three operations finally finished now we can start actually using these uh threads to do so first i'm gonna remove this example that was just executing a task on the main thread and i'm going to submit let's say a hundred tasks and i have here an i4 from 0 to 100 and that's going to submit a task that we create here task d and say a is uh let's use random numbers why not let's say that and that so any number between zero and 99 and of course initialize the random generation function with this and lastly we want to submit this task and we're calling submit task of our t here so that's very straightforward i think now let's try to execute this program so as you can see it indeed actually processed a lot of tasks and let's say that there are a hundred tasks i didn't actually count them but it's pretty safe to say there are a hundred uh summations here but there's one issue with this uh with this little program if we take a look at a in different in a different terminal in each top here is the cpu usage of the computer and we can notice that it is quite high and it is actually because our program if i stop the program it's going to be going down to like five percent now why is that happening remember in our diagram how we actually thought that okay uh this operation should be waiting for a task if there's no task and it should be getting the task and executed if there is a task right but we never waited for a task right are we we're never really calling any sleep any weight any nothing in here and what these trends actually do is they they one of the threads let's say locks the the mutex then checks if the task count is higher than zero it looks easy and says no it's not then it exits it unlocks the mutex and then another thread comes in checks r thread comes in and so on and so forth for forever and ever and ever really really really really fast it's doing that and this is not great this is not great because it's using cpu resources for no reason right this checking forever is uh incredibly costly so to fix that we have to add condition variables we actually have a video on condition variables and how to use that and i suggest you take a look at it up top if you haven't already but the idea is very simple we're going to have a condition variable on this condition and that is a simple while loop on our task count if it's if it's actually zero so i'm going to check if it's actually zero and if it is i'm going to continue waiting so keep that weight on our condition variable let's say point q and we should define this as well let's define it up top here p thread t conned q and i'm going to initialize it just like so so we're going to wait on the quant q and of course we have to pass in the mutex here so i'm going to have to pass in the mutex queue so that when we're waiting uh it unlocks the mutex for us so that other threads could check if there is something or other threads could actually modify the queue all right now the nice part about this while loop is that if we are here right that means that we have passed this while loop okay straightforward but that also means that the task count is definitely not zero and it's most likely not negative one so therefore it's one or something higher so we don't have to check again if the task count is zero because we know for sure it isn't so that kind of gets removed and this also gets removed because we know for sure that we have we have a task to execute if we are executing this code here so we can remove this found variable here that we have created just like so and this is our final start thread function that waits and gets a task and then executed if if it finds one and lastly we need to actually signal on this condition because just waiting it is just going to block it forever and ever so to signal it we're going to actually do that in sumita so whenever we're submitting a task we should signal a single thread to check if there is actually a task there and if it is it should execute it and we don't need to call broadcast here because we only for if we only add one task and for one task we only need one thread at most so i'm just going to say p thread on signal of our coned q and now finally we can start executing this without having too many issues and as you can see we have a hundred tasks well hopefully a hundred we can change this to a more manageable number let's say we can say 10. and if we count them they should be exactly 10 and that is correct and if we take a look at each top again our cpu usage is very much better now finally we're done implementing the thread pulls right we can now try to play around with it and see what are the advantages of having a thread pull so first things first let's create here again 100 tasks right if we launch this again it's going to be very instantly it's going to be all the threads are uh or the tasks are being finished almost instantly but we can make the tasks take quite a bit longer by simply saying you sleep here or 50 milliseconds and think about uh this you sleep as being something like an operation that takes a long time right so actually i'm just going to use sleep and if we try to launch this now we should see that it takes quite a while but it's still quite fast right we don't have any issues now what happens if we do the same thing but we are going to be using let's say one only one thread we can have a thread pull of only one thread but it's like executing it sequentially we should see a pretty big difference now if i try to launch this you will notice how much slower that is so whereas before it was almost instant executing 100 tasks now it's kind of going along but not that fast now as you can see with four threads it's very much faster and we could add more than four threads it is possible but i don't think in our case it's gonna matter too much simply because the cpu that i'm running on right now has only four cores so yeah that's that's uh that actually all there is for implementing thread pools themselves in the next video i'm gonna take a look at how to actually make them useful because right now we're literally just summing numbers and sleeping for 50 milliseconds what if we want to do something more generic right how can we do that and you'll see that next for now i hope you got something out of this video if you do have any questions leave them down in the comments below or on our discord server again the source code for all this will be found on our website link in the description below alright take care bye
Info
Channel: CodeVault
Views: 7,101
Rating: 5 out of 5
Keywords: codevault, c (programming language), thread, pools, pool, pthread, mutex
Id: _n2hE2gyPxU
Channel Id: undefined
Length: 22min 54sec (1374 seconds)
Published: Wed Jan 06 2021
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.