Concurrency in Go

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
in the first video learn go in 12 minutes we had a very quick look at the main features of the language so you could get going straight away one thing I didn't talk about though was the support for concurrency that NGO has this is a really big part of go it's a big selling point and a reason why a lot of people choose the language so I thought it deserves its own video it's worth understanding that concurrency is not quite the same as parallelism to run things in parallel means to run two things at exactly the same time this is what happens on a multi-core processor you have one core doing one thing and another core doing a different thing both simultaneously typically though the lines of code that make up a program have to run in the right order which makes it hard to parallelize and execute two lines at the same time so concurrency is about breaking up a program into independently executing tasks that could potentially run at the same time and still getting the right result at the end so a concurrent program is one that can be parallelized we're not going to concern ourselves with what is happening at the CPU level and whether something is running on multiple cores or not the goal runtime and the operating system will take care of it for us we can concentrate on the structure of our program and using the tools that go gives us to make our code concurrent so I've got a text editor I'm gonna write a really simple function called count it's gonna take a string as an argument and in an infinite for loop that starts at one and just loops forever counting up I'm gonna output the the number that we're at and the string that we passed in and then I'm just going to sleep for half a second and I can count sheep and then I'll make a call to count to fish afterwards so this is a synchronous program there's no concurrency here it's going to execute the the count function and it's gonna wait for it to finish before moving on to the next line but the count function never finishes so it's just gonna count sheep forever and never get to the fish so just do that until I kill it if however we call the function with the word go in front of it it won't wait for it to finish before moving on to the neck line it'll say go and run this function in the background and then continue to the next line immediately and this creates what is called a go routine and that go routine will run concurrently so we now actually have to go routines the main function with the main execution path of the program is a go routine and now this new one that we've created explicitly so these will both run side by side and we see now it counts fish and it counts sheep go routines are very efficient it's okay to make tens hundreds even thousands of go routines but bear in mind that you can't make a program infinitely fast by adding more and more concurrent go routines because ultimately you are constrained by how many calls that your CPU has I'm gonna make one tiny change to this program and run both count functions as go routines now you might expect this to do exactly the same as before and you'd be almost right but we get a very different result we don't get anything so what has happened well in go when the main goal routine finishes the program exits regardless of what any other go routines might be doing previously the main go routine never finished because it would have this infinite for loop in it but now we've pushed that loop into its own goal routine so the main function will continue immediately to the next line but there are no more lines of code so it's done and the program terminates and the go routines that we've created ourselves haven't had time to do anything if we were to sleep for two seconds here you'll see it now outputs for two seconds and then it terminates you'll often see people add a call to F Mt scan line at the end of the main function to fix this problem and this will stop the main function from immediately terminating because it'll wait this is a blocking call it'll wait for user input so this gives our go routines time to execute and it's gonna keep doing this until I press ENTER and at that point it'll move on on the main functional finish and the program will exit again in reality though this is not a very useful solution because it does require manual user input what we can do instead is use a wait group I'm gonna import the sync package from the standard library LS it alter our program so we have one call to count and let's just count up to five to use a weight group I'm first going to create one and there's nothing scary it's just a counter and I'm gonna increment it by one to say that I have one goal routine to wait for and it doesn't do any magic here it's up to me to increment it the next step is to decrement the counter when the goal routine finishes so after this for loop I want to decrement the counter now I could pass a pointer to the weight group to the count function but I don't think it's really the responsibility of count to deal with this so instead I'm gonna wrap the cult account in an anonymous function this syntax creates a function and then immediately invokes it so this will still run as a go routine and inside here I'm gonna call count again and then afterwards I'm gonna call WG dot done since we've created this function in line we have access to that WG variable which is convenient and done literally just decrement the counter by one so all we have so far as a counter of how many go routines are running the useful bit now is to call wait at the end of the main function this will block until the counter is zero so of any go routines haven't finished it'll wait so now it's gonna count to five count will return will call done which will decrement the counter and then this weight will be like oh the counters at zero and allow the code to continue and the program terminates really easy to use so that's how to create a go routine really simple but not massively useful so far what we need next are channels a channel is a way for go routines to communicate with each other so far the count function has just been outputting directly to the terminal but what if we wanted to communicate back to the main goal routine well we can accept a channel as an argument and it's like a pipe through which you can send a message or receive a message channels have a type as well so this one will be a string channel and we'll only be able to pass messages that are strings any type works though you can even send channels through channels so instead of outputting thing to the terminal I'm gonna use this arrow in tax to send the value of thing over the channel so an arrow pointing in to the channel name will send a message gotta get rid of the waste group stuff now so a nice simple concurrent call to account and now we need to pass the channel in so first we can make one using the make function pass that to count and then we can use an arrow coming out of the channel name to receive a message from the channel so this is going to receive one message output sheep wants and then terminate and it's important to understand that sending and receiving are blocking operations when you try to receive something you have to wait for that to be a value there to receive similarly when you're sending a message it'll waste until a receiver is ready to receive so you can see it does what we expect at output strip and this blocking nature of channels allows us to use them to synchronize go routines imagine you have two independent go routines each line here is a line of code we don't really care what it is except down here we receive on a channel and over in this score routine we send on the channel when they both execute sometimes one will stop and sauce and the other will stop and start there might be executing different code they won't stay synchronized at all but when this go routine on the left tries to receive on the channel it'll stop and wait until something is sent and at some point the other go routine will reach the line where it tries to send and then they'll be able to communicate through this channel so this precise moment they both at this communication point so a communicating and was synchronizing which is an important concept back to the code this just receives one message if we wanted to receive all of them then we could wrap this in a for loop so this is what we expect but then it gets a fatal error we get deadlock this is because the count function is finished but the main function is still waiting to receive on the channel but nothing else is ever gonna send a message on the channel so we'll be waiting forever the program will never terminate go was able to detect this problem at runtime not a compile time at doesn't solve the halting problem but when it actually happens it can see that go routines aren't making any progress to solve this we can close the channel as a sender if we're finished sending and we don't need the channel anymore we can close it if you are the receiver you shouldn't ever close the channel because you don't know whether the sender is finished or not if you close the channel prematurely and then the sender tries to send on that closed channel it will cause an error it'll panic but it's okay for the count function here to close the channel because it knows that it's done and it's not gonna use it anymore when we receive on the channel we can actually receive a second value which tells us whether the channel is still open if it's not open if it's been closed then we can break out of this for loop so now we don't get the the deadlock anymore and there's actually a slightly nicer way we can do this in go by iterating over the range of a channel so this will keep receiving messages and putting the value in to this message variable here until the channel is closed so then we don't need to manually check that it's closed anymore exactly the same result just a bit of a syntactic sugar so we've seen so far that sending to a channel is a blocking operation to demonstrate the constraints of this I'm gonna do something really is simple I'm gonna make a channel of strings I'm gonna send hello across the channel and then I'm going to try to receive from the channel and output it to the terminal naively we might expect this to work and just output the word hello but we're actually going to get deadlock again this is because the send will block until something is ready to receive but the cold never progresses to the receive line because we're blocked at sent to make this work we'd need to receive in a separate go routing alternatively we can make a buffered channel by giving a capacity when we make the channel you can fill up a buffered channel without a corresponding receiver and it won't block until the channel is full so with a capacity of two this will work in the lab port hello we can even put two things into the channel before having to read anything back out so we put two things in and then we read them and nothing box here if we try to send a third time though the channels gonna be full so that call will actually block and we'll get deadlock again the final construct that go has is the Select statement if I have to go routines I'll just create them in line like this I'm gonna make two channels which will send them receive strings the first go routine is going to send on the first channel and it's going to be ready to send every 500 milliseconds so I'm just gonna alice sleep here for half a second the second go routine is gonna send on the second channel and it's gonna do that every two seconds and of course to make it do this infinitely I'm gonna wrap each one in a for loop back in the main go routine I could similarly have an infinite for loop and I could receive from channel one and I could receive from channel two and then loop and do that over and over again but will always get one and then the other and then one and then the other even though this first go routine is ready to send much sooner and this is because we're gonna block each time waiting for the slow one so every time we try to receive from channel 2 we're gonna have to wait two seconds so it's really slowing down that this first call routing instead we could use a select statement which allows us to receive from whichever channel is ready so in the case that channel 1 has a message we can output that but in the case that channel 2 has a message sorry that should be message 1 in the case that channel 2 has a message then we can output message 2 and then we're just going to loop over this so this time we see that we're able to receive a lot more quickly from channel 1 because this is only sleeping for half a second and that select statement keep picking channel 1 because it's available finally I want to demonstrate a common pattern called worker pools this is where you have a queue of work to be done and multiple concurrent workers pulling items off the queue I'm gonna write a really simple Fibonacci algorithm it's going to calculate the nth Fibonacci number and return it if n is 0 or 1 then just return n otherwise return the sum of the previous two Fibonacci numbers and then gonna write a worker which takes two channels one channel of jobs to do and one channel to send results on instead of specifying bi-directional channels we can actually say that will only ever receive from the jobs channel and will only ever send on the results channel and this just reduces the chance of bugs because now if we tried to send on the jobs channel we'd get a compile-time error so jobs is going to be a queue of numbers and we're going to use the range feature to consume items from this queue so this is going to receive on-the-job strangle so we're gonna receive n from the channel we're then gonna calculate the nth Fibonacci number under send it on the results channel in the main function obviously create the two channels I'm gonna make them buffered channels and give them a size of 100 no particular reason why I'm picking a hundreds of so so nice round number and then going to create a worker as a concurrent go routine give it the the two channels that it needs I'm then gonna fill up the jobs channel with 100 numbers so we're just gonna iterate from 0 to 99 and put all of those numbers on this jobs channel and since it buffered we're not gonna block that's gonna be fine once they're on there the worker will concurrently start pulling one off at a time and calculating the the Fibonacci number and then put it back onto the results channel I'm gonna close jobs because we're finished put and stuff on to that channel now we have a sender here so it's okay to close it I then expect 100 items to eventually appear on the results channel so that's gonna be the first 100 Fibonacci numbers cuz I'm just gonna receive each one of those and output it to the terminal so this works this is fine it does the first batch really quickly and it's gonna get progressively slower it's quite an inefficient algorithm and if we look in Activity Monitor it's almost maxing out the CPU it's very close to 100% trying its best to calculate each Fibonacci number as the worker pulls one off the job skew so that's cool but what we can do now is add more workers so I can just copy this line so now we have four concurrent workers all pulling items off the jobs queue and then all pushing back onto the results queue at the end and if we look in Activity Monitor now it's using almost 400 percent CPU because it's using multiple calls so the work will get done faster like I said at the beginning I don't wanna get too involved with how this works and how much faster it makes things because you don't get a massive amount of control over it but it is pretty cool to see that we're taking advantage of the multi-core processor obviously this version doesn't guarantee that the Fibonacci numbers will come out in order but that's the the gist of how worker pools work and that is a quick tour of concurrency in go really easy to do hopefully it wasn't too difficult to understand if you're interested in a career in software development or you just want to improve your skills then you might find it useful to dive further into computer science there's a lot more to computer science than just programming it's a very broad field and it covers maths with topics like linear algebra probability it covers hardware which goes all the way down to how the CPU works algorithms like this Fibonacci algorithm that I wrote and you'd learn how to analyze the efficiency and how to write better versions which don't max out your CPU usage so having an understanding of these topics really helps when you're writing code it can greatly simplify coding projects brilliant org is a great place to learn more about computer science they offer curated courses on many things from the fundamentals all the way up to cool stuff like artificial neural networks the guided courses go in a great detail to build up your knowledge and then walk you through various problems to help your practice and really understand what you're learning understanding how memory works for example it's gonna help you write more efficient code and it'll help you reason about the code because you'll understand what's happening at the operating system and the CPU level pointers in goal will suddenly make a lot more sense if this sounds interesting got a brilliant org slash Drake write the link is in the description you can sign up for free about the first 200 people who go to that link will get 20% off the annual premium subscription if you found this video useful click the like button hit subscribe if you want to see more tutorials like this one and I'll see you next time thanks for watching you
Info
Channel: Jake Wright
Views: 536,564
Rating: undefined out of 5
Keywords: jake, wright, computer, science, british, vlogger, london, software, developer, go, brilliant, concurrency, parallelism, example, how to, beginner, tutorial, golang
Id: LvgVSSpwND8
Channel Id: undefined
Length: 18min 39sec (1119 seconds)
Published: Sun Mar 04 2018
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.