 This is many ways to conquer. It's a concurrency talk. I'm Abhinav and this is functional card. I work for Flipkart. The colors are terrible over there. So, but a lot of work that inspired this talk was done when I used to work for Nail and so. Okay. Anyone recognize what this picture means? The dining philosophers problem. It's a classic concurrency problem. So, when we started getting into functional programming languages, it was a hard sell to us that, well, not hard, but yeah. It was sold to us saying that functional programming languages are really good for concurrency. They're easy to work with when you're writing concurrent code, right? Which is true. Functional programming languages have really nice models of concurrency which are much better than, in my opinion, as found in other imperative languages. But there are a bunch of them, right? And you may not be aware of all of them. So, this is sort of a survey or an overview or review talk about the different kinds of concurrency models across a bunch of functional programming languages. We're going to talk about Haskell, Erlang, and core Async library enclosure. So, what is concurrency? Simon Marlow says in parallel and concurrent programming in Haskell Book, that concurrency is a program structuring technique in which there are multiple threads of control which execute in quotes at the same time, right? So, notice that it says threads of control, right? These threads could be actual hardware threads, could be kernel threads or not. It doesn't matter. It's just a concept, right? So, the important thing to notice here is that they all execute at the same time. That means that the effects of their execution is interleaved, right? You would see them a part of thread happening from this thread and apart from other thread and such, right? And that obviously means that concurrency is non-deterministic. You do not know what part of your code or your thread is going to execute at what time, right? So, it's a program structuring technique in the sense that it allows your programs to be modular, right? So, imagine writing a HTTP server, right? So, you can, if you're using, you have a concept of threads over there, some kind of threads, then you can write the interaction of each request response as a single thread which is isolated in its scope and you don't have to care about what other requests are going on and such, right? If you don't have threaded concurrency, like you don't have a node, then you have evented concurrency. We have to think about all the events across all the threads, all the requests which are going on at the same time and then your code is not that modular. It's a well-known callback, hell problem in JavaScript, right? So, what is a thread, right? A thread is nothing but a sequence of instructions with a context, right? That is the, in concept, that's all a thread is, right? So, in actual, in the real world, the thread is run by processors and they are managed by schedulers, some kind of schedulers. So, in the old times, you used to have single processor systems, then even those had multi-threading with time slicing. Now, you have multi-processor systems or multi-core systems which can execute multiple threads in parallel where each core can execute a separate thread, right? So, the Kerner scheduler is what schedules these threads which are called kernel threads on the processors, right? But what is the problem with the threads we have as the kernel threads? The problem is that the threads are pretty heavy, right? The context switch is expensive when you're switching from one thread to another thread and if they are difficult to start, they are slow to start, right? So, if you imagine if you have thousands of these threads then your kernel is doing the bookkeeping for running thousands or probably hundreds of thousands of these threads then that's all it's doing most of the time instead of doing your actual work, right? So, that is what is known as the C10K problem where the idea is to have 10K concurrent connections on a single machine, right? It's very hard to do that with only native threads, only kernel threads, right? So, in general, you can have some lower thousands of active threads on a single core. What is the solution to this? The solution to this is called green threads. I'm sure some of you may have heard about it. So, a green thread is scheduled by the runtime of your program or the virtual machine instead of the kernel, right? So, the control is in your process, in your code, so to say, or your code's libraries instead of kernel doing it, right? So, because of that, you can specialize and you can have green threads which are much cheaper to start and much faster to start. And the context switches are also way faster. I'm not sure, I mean, I'm sure that you have heard about this whole Alang being able to run hundreds and thousands and millions of green threads in a single VM, right? How? That's how, right? Because the Alang VM manages these threads and hence it's able to keep it in a very small space and switch it much faster, right? But there are problems with green threads. One major problem is that if you make a system call in the green thread, then it blocks the thread, right? So, if to use green threads effectively, you need to support, your runtime needs to support asynchronous IO, which Alang and Haskell and Go and such do. Some of them do not, like JVM does not support. It supports asynchronous IO, but it's a bit tricky to get working with asynchronous IO in JVM, right? And also, green threads, if you do a very simple implementation of green threads, you cannot exploit multiprocessor parallelism in green threads, right? What you need is a more intelligent runtime which can map N number of green threads to M number of kernel threads at runtime, intelligently. This is basically the scheduler which will do this. But if you don't have this, then you basically have N number of green threads running on only one kernel thread and if any one of them blocks, your whole program blocks, right? Fortunately, Haskell, Alang and Core Async support this kind of mapping. So, that's why we are gonna look at them. So, the main focus of this talk will be around green threads and synchronization of between those threads. Interestingly, where does the name green thread come from? Does anybody know? Why is it called a green thread? Is it more eco-friendly or something? So, the name comes from Java. I may not believe it, but one of the first implementations of Java has contained a green thread implementation was Java 1.1 and was implemented by a team called green team, where green again is not something to do with ecology. It was just like green team, red team, blue team and the green team just happened to implement green threads and the name stuck. So, now it's called green threads everywhere. So, when you have threads, I don't know if you can see the image behind, but this is Outer Ring Road right here. So, when you have multiple threads, you sometimes need to synchronize between them. If you'd never need to synchronize your well and good, but that's not how real world is, right? You need synchronization very often. So, what is synchronization? Synchronization is the process by which threads agree on something, on something, something or other, right? Agreement as in conquer and hence the name of the talk, right? So, that something could be like, you know, if you have a thread starting another thread or a thread waiting for another thread, which basically called forking and joining of threads, then you have a concurrency problem there. If you have a variable which is accessed by multiple threads at the same time, right? Then you have a concurrency problem because you do not want concurrent access to the threads, to the variable, right? You may have a sequence of steps which you want to execute in one particular order, but if you let multiple threads execute them together, then you may have interleaving and it all gets corrupted. So, you need to protect these steps with some sort of synchronization. You may have shared resources like a database handle, HTTP connection or TCP connection or whatever, to which you do not want to write concurrently because it'll corrupt the downstream resource. So, that's also a synchronization problem. So, how do we solve synchronization problems between thread, right? So, all threads in a particular process share memories, share memory addresses. So, it's very easy to use memory itself, shared memory to implement some sort of locking mechanism, right? So, locks are used to prevent concurrent access to the critical sections like a sequence of steps or critical memory like a variable or a resource, shared resource, right? But the problem with locks is that they do not compose. When you have n number of resources where n may or may not be known beforehand, then locks are difficult to work with. I'll show an example later. But the problem is that you have to lock, if you have multiple, you're working with multiple locks, then you have to get the locks in the right order at the right time and also you have to unlock them in the right order at the right time to make sure everything works perfectly. Otherwise, you'll have issues like deadlock where you have two resources waiting for each other's lock or starvation, right? And a bunch of other kind of concurrency issues, right? So, it's very easy to get them when you're working with multiple resources with locks, right? So, what is the solution? The solution is, well, you ditch locks, lock-free but still shared state concurrency as implemented by software transactional memory and communicating sequential processes or you ditch shared state itself, no shared state as implemented by actor model, right? So, with that, let's jump into the first one, actor model, right? I'll take questions after the presentation. So, I'll just have a lot to talk about. What is an actor? So, an actor is actually pretty simple in its definition. An actor is a thing which can create new actors, can send messages to actors, other actors or maybe itself, and can receive messages and do something, that's it. That is all an actor is in essence, right? How is it implemented by our line? So, you see here the circles are actors, right? And the envelopes are messages. So, actors are sending messages to other actors, right? So, there is something called mailbox over there, right? So, first of all, what is an actor? An actor is nothing but just a green thread, as we talked about. Our line calls them processes for some reason, but they're nothing but just a green thread implemented by the online VM. Each actor has its own mutable state, but only that particular actor has access to that state. No other actor can access that state, right? That's what is the isolated state over there at the bottom, right? The only thing that actors can do is send messages to each other, right? And messages are immutable. You cannot change a message after creation, creating a message. And when you send a message, when one actor sends a message to another actor, then it lands up in the other actor's mailbox. It is very similar to how email behaves, right? You send emails to each other, it lands up in the other person's mailbox. They go and check their email and then they reply to you or not, or spam, market spam or something. So, that's how it works with actors also, right? So, all you do is send messages to each other and it lands up into other actor's mailbox. And then the other actor can decide to read it whenever it wants, right? In whatever order they want, right? And then when they read a message, they can update their own state, like, you know, I read an email, I'm like, you have a meeting at 5 p.m. I put it in my head or something like that, right? So, I update my state to reflect the message I got. I can also send another message saying, hey, I'm not coming to this meeting or something like that, right? And that's pretty much all I can do. That's all an actor can do, right? Of course, it can do IO and other stuff, but I'm not gonna talk about that part here. So, the only way to communicate between actors is messaging. There is no other way. There are no locks. There is no shared memory, nothing. You just send messages to actors and in our line, everything is an actor. Literally, everything is an actor. If you open a file to, if you open a file to read, then an actor is created for that also, which will read the file and send you chunks of file as messages in a simplistic way, right? So, everything is an actor in our line. So, let's see some code examples, right? Tell me if it's not visible from the back. Yeah, I was big reveal. So, this is a very simple actor, right? The actor is an echo actor. You just send it a number and you just print it or you can send it a quit message and it'll just die, right? So, it's implemented as a recursive function. You see the loop function calls itself at the end in the num line, right? So, what it does is a receive call. So, when you say receive, the actor picks up a message from its mailbox. That's how you get mails or get messages, right? Then you can pattern match. This is pattern matching over here. It's matching it against two possible patterns. I am sure you've seen pattern matching in some FP language or other. So, if it's a quit message, it just says buy and does not call itself and in effect, it dies. If it gets something else, then it says receive buy, prints it there and then calls itself and then goes on a loop forever till you send it a quit message. So, this is the part where actors receive messages. How do you start an actor and send the message? That's also very simple. You call the spawn function with this function, right? Along with the functional language, you have functions as first class entities. So, you say spawn loop and you get back what is called a process ID, PID, right? That is how you reference actors or processes in our line, right? So, then you can send messages to the process ID. See, sending messages just bang. It's very simple in our line. You just say process bang whatever X number and it prints that number because it received it in the receive section over there, right? Very simple. Then you send it quit and it says buy and it dies. That's it. That's actor 101 in seven, eight, 10 lines, right? So, actors are nothing but simple functions, right? And they just keep repeating themselves, keep people printing, picking messages from mailbox and doing something, right? And when a receive is called, actor calls a receive and the mailbox is empty, then what the Alang VM does is that it just like sidelines that actor saying, hey, you don't have anything to do now, go sleep. And when a message arrives again, then the Alang VM knows that a message has arrived for this actor. It'll wake it up saying hey, you have a message now and then your receive function will finish and you'll get the message at that line, right? So, that's how an actor works with the help of the scheduler, right? Let's look at a more complicated example. That actor did not have a state, right? I was talking about a state, stateful actor. How does state implemented? So, let's write a counter actor. Very simple loop over there, but now this time it has a parameter and that's it. That is the state of the actor. You start an actor with an initial parameter, which is the initial state, and then it calls itself with a new state. So, if I send it a ink message, right? Which is the increment, then my cursor is somewhere. So, if I send it a ink message, then what it does is just calls itself with n plus one and you have done an increment in the state. That's it. So, there is no shared state and state is nothing but just the parameter of the function, right? The other function is the get function, which is how you get the counter value of a counter. If you have a counter, you can't get its value, it's pointless, right? Or countless. So, you start the counter as earlier, right? This time with, sorry. This time with an initial value, which is zero. You send it ink, that code activates, it increments the state to one, and then you say get self. What is self? Self is how a process gets its own ID. Remember when I said everything's a process? Even the shell in which I'm writing this code is a process. So, if I call self in the shell, I get the process ID of the shell itself. So, the shell sends its own process ID to the counter and the counter will message it back. As you see over there, it's sending the sender a message with the value and then recur. That's it. So, that is how you get the values out of actors. Actors cannot send you messages unless they know your process ID. Actors are anonymous in that sense. There are ways to name them also, but I'll not talk about that, right? So, apparently, it seems like the shell should have received a message, but where is it? I call receive in the shell itself to print the message and of course it works, it prints one. The shell did receive a message. But what if the shell sends a wrong message? I send a wrong message to the actor, get X. There is no handler for get X. What'll happen now? If I call a receive in the shell, it'll wait forever because that's how actors work, if the mailbox is empty, they receive blocks. Obviously, they thought about it and you can call receive with a timeout. You can say, I'm willing to wait for a thousand milliseconds. If I do not receive a message, I'm gonna do something else. That's it. So, a timeout happens in this case and the shell resumes, right? So, that is how you implement a stateful actor. So, this is very simplistic view of actors. There are obviously more things. There are supervision hierarchies and actor monitors and our line gives you a lot more than just this much, right? But I'm not gonna talk about that. Talk a little bit about what is an actor in our line, right? How is it implemented? Turns out an actor is nothing but just a piece of memory, a block of memory and looks very similar to a kernel thread, so to say, right? You have a process control block. You have a stack, you have a heap. The stack has parameters and return address and such. Very similar to how it's implemented in kernel. And then you have a heap which has a special thing which is mailbox, right? So, a mailbox is nothing but a piece in a part of memory in an actor's heap. And when one actor sends message to another actor, they just literally copy the message data from their heap to the other heap. That's it, right? So, actor A copies its message into actor B's memory and then marks its own messages deleted which gets collected by GC. So, the only one copy of message exists it's at one time in just one actor, right? Unless you explicitly copy and send it across and stuff. So, that ensures that the messages are not corrupted when they are being sent from one actor to another. You cannot alter a message while after sending it, for example, right? When the copying happens, you have a small piece where a lock is acquired to put the copy of the message in the mailbox. But there are more complicated algorithms also implemented in a line which can do this without locks, right? Which involves GC doing a part of work for cleanup and such. So, that's how an actor is implemented in a line. So, that's it for the a line part. Let's talk about what is good about actors, right? So, communication itself is synchronization. If you say a call receive, you block. You don't need any special locks or any sort of stuff for that, right? No need, you don't need to worry about mutual exclusion because the states are isolated in each actor they cannot access each other states, right? And it models distributed systems very well. And it also distributes very well our line has built-in transparent distribution of actors and such, right? The bad side is it can be slower compared to shared state concurrency with locks and such. It is difficult to do multi entity consensus as in distributed transactions or even two actor transactions, right? If you wanna do, you wanna synchronize there is no easy way to say that this a bunch of actors should do same thing at one particular time. There is no easy way to do that. You have to model distributed transactions using complicated algorithms like propossibly raft or something like that, right? Distributed consensus inside actors which has already been done, but sure, right? And if you do not consume messages if the actors do not consume messages fast enough their mailbox overflows and they die, right? Hard of memory. So these are a bunch of problems with the actors. Let's move on to the next section, software transactional memory. I'm gonna run a bit faster now. So what is software transactional memory, right? So a software transactional memory or STM allows you to do, as I just said, actors cannot do distributed transactions well. That's exactly what STM gives you, distributed transactions. It lets you change multiple mutable variables together in a single atomic operation, right? That gives you atomicity. As in like, all of the changes to these n variables become visible to every other thread at the same time or they don't, right? Which is isolation, right? So one operation is completely unaffected by other operations. It's as if you take a snapshot at the beginning and then just work on the snapshot and it's independent of all of the snapshots, all of the threads doing some other transaction on those variables at the same time, right? So let's see how Haskell implements STM. There's a data type called STM, parameterized type A, which also happens to implement monad type class, which gives you a lot of convenience functions. And this is the core of STM, a T var, T for transactional. This is a transactional mutable variable, right? You can create a new T var, you can read a T var, you can write a T var, but you can, should notice that all of these things happen in the STM type. It's not IO, right? Mutable things in Haskell are generally associated with IO. In this case, it's not IO. It's a special type called STM, right? And so you can do multiple of these things together, but how do you actually do that? For that, you have a function called atomically. It takes an STM block, which can have multiple STM operations and does them atomically. That's what it does, right? So it either gets done at the same time or not done at the same time, right? There are a bunch of other things, which I'll explain later. So to explain this, let's look at an example. This is a classic concurrency example where you have two accounts, you want to transfer money between them. It could be happening in reverse order at the same time concurrently. And how do you make sure that it's all working fine? Let's look at some Java code with locks, right? This is an account, which has an ID and an amount. And if you want to do a withdrawal, obviously you'll put a lock around it so that no other withdrawal is done at the same time, right? Otherwise you'll end up with beard amounts. And deposit is very simple, you just withdraw with a negative amount, right? And then you would think, okay, now I can write transfer very simply, I just withdraw from this and deposit into the other one and I'm done, wrong. It's, it'll not work because you are not locking the accounts in a deterministic order, right? So it can happen that if the two concurrent transactions are happening in reverse directions, then account A gets locked by the withdrawal function and the account B gets locked with the withdrawal function at the same time. And then they wait on each other to be unlocked and it never happens and it's a deadlock. To fix this deadlock, the very simple solution is to lock, acquire the locks in deterministic order, right? So how do you determine, how do you order them? You use some sort of, you know, characteristic of the account, in this case the ID. So you check, you compare IDs and then you lock in the order of IDs in the, first the smaller ID and then the larger ID, right? Otherwise you lock it in the other order, right? So there's the locking part and then you have to unlock in the same order. But even then you cannot reuse the withdrawal or deposit function because they release the lock internally. You cannot remove locking from there also because you may have just a withdrawal instead of a transfer, right? So you need to rewrite that code here to do this thing. And this is how it ends up looking. And this is just code for writing, transferring amount between two accounts. What if you have three accounts? What if you have unknown number of accounts, right? What if you are eBay or PayPal, whatever. And what if you wanna have like overdraft accounts? Like if you don't have this money in this account and get money from that account. Cannot do those things with Java, with locks like this. How does it look like in Haskell? You have an account which is just a transactional variable with an integer amount. Withdraw is very simple. Notice that everything is happening in STM monad. So read the balance, then reduce the balance, write down the new balance, right? That's it. Deposit is still just the opposite of withdrawal. And in this case, transfer is actually that code that we wanted to write in Java. Deposit, withdraw. That's it. Because it's running in STM with the atomically function. It'll make sure that it happens without any errors. It'll not cause any deadlocks. It'll never leave a corrupt state ever, right? That may seem like magic, but we'll come to the internals in a minute. And then a final function to get the amount in the account and which is also used atomically, right? So let's see how this is actually used. Oh, by the way, this is the whole Haskell code. It's smaller than the whole half the size of Java code. So you create two accounts, right? Again, using atomically. This is happening in IO. You do a transfer and then when you do, when you print these amounts, it'll always say 150, 150. You transferred 50 from 200 to 100. And it'll always be like that, right? There will never be a case when it's different. So how does it work, right? So is it visible? It's a bit unreadable from here, maybe. I'll just explain. So you have, say if you have a transaction running, right? So when a transaction starts, right? So the first time a T var is read within a transaction, it will cache or it'll copy its value to a local cache known as transaction log, right? So you read a variable A, X in this case. Some of these say A and X should all be X. That's my mistake. So if you read a variable X, right? It gets cached into the transaction log. And then the writes to the variable X will go to the same cache. And at the end, when you're finishing the transaction, you would do a commit, when you do a commit, right? Do, then at that time, STM will compare the value of X that it has in the log, in that cache with the actual value, which is there right now. So if it's the same, it just commits and everything is fine, right? If another transaction, a shorter transaction has happened between which has changed the value of X in between, then your commit fails at that point. And then what STM does is just restarts the transaction. That's it. And this may happen multiple times if you have a really long transaction and a lot of short transactions breaking it, but it'll work fine. It'll all eventually give you the right answers, right? If you have multiple variables which you read and write, even then it'll work fine because it'll match the value of every variable before committing. And if anything is wrong, it'll abort the commit, right? So at the time of commit, there's a short lock which is held, but that is only place where a lock is held and then the values are changed by a keyword cast or something which may be architecture-specific mechanism, right? So this is safe to do. You may think that, oh, what if it's retried many, many, many times? That's okay, because of the type system, the strong type system that Haskell has, it makes sure that there are no side effects, right? You cannot do IO inside STM. There are two different data types. There are two different monads. So you cannot write code which sends an SMS when this transfer is happening and that SMS gets sent twice or thrice. It will not happen because you cannot do that by like, you know, enforced by the type system, right? So let's, I'm gonna actually skip over these slides because I'm running out of time. We can see them later. These are, so the transaction variables can be used to implement a transactional channel, right? A transactional channel can be, again, has the same features of transactional variable except it is multi-valued, right? So like a queue, instead of just one variable, you have a queue of variables which you can, again, read, write atomically and such, right? You can read a tech channel, you can write to a channel and everything is atomic. And because of transactions, well, I did not talk about this previous thing over there which is or else. Or else is a way to merge two STM transactions. He says that do this if it fails as in failure as I showed in the previous slide, the commit fails, then do that. So this lets you read from multiple channels at the same time atomically. You say read from this channel or else read from that channel and only one of these channels will be read. That is ensured by a STM, right? So these are channels in STM and they're actually very simple to implement, right? You can write implementation yourself. An STM channel is nothing but a link list of STM T-vars. That's it. So if you have a T-vars which points to an item, the item points to the next T-vars, the next T-vars points to the next item and you keep a head and a tail T-vars. That's it. This is all for implementing transactional channels in Haskell. So to conclude, pros are you have composable atomicity and blocking, right? You can compose them in any order and it'll all work fine. It does no need to worry about locking order and such, right? The type system of Haskell will prevent all sorts of side effects. It's not possible to do that. And with T-chance, you can create a data flow programming sort of like assembly lines and such to do, say ETL or similar sort of stuff, right? I'll not go into details over there, but yeah. The cons are that the transactions which touch many, many, many variables, many T-vars are expensive. That's because at the time of commit, you actually have to go over all the T-vars and make sure that they are, you know, correct, right? So if you read 100 T-vars, then the transaction will be slower. If you read an unbounded number of T-vars, your transaction may never finish, right? So you should never read unbounded number of T-vars in a transaction. A long-running transaction may re-execute again and again because some short-running transaction may cause it to be aborted, right? As we saw in that slide. So if you have a transaction which is very long-running, very probable that it'll get aborted again and again. So you should not do very long-running transactions either, right? So that is it for SQL STM. I have 10 minutes. I'm gonna run through the last slide. Last part, which is communicating sequential processes as implemented by co-ressing in closure, right? So what is CSP, communicating sequential processes? You have independent threads of activity. Notice there, I'm called the threads of activity. In case of closure, they are not green threads. Closure does not have green threads because it's on JVM and JVM does not support green threads anymore, right? But they are independent. And communication between the threads is done through channels, as we talked about earlier. But the communication in this case is synchronous, right? I'll explain how it works. And you can multiplex channels as in you can combine them using alteration. So let's see what CSP is. So you have three processes here, one, two, and three. So one and two are using the same channel to communicate. So one reads the value from channel, some channel, right? Process one reads the value from a channel. But the channel is empty, so it blocks at that point. And at later time, process two writes the value to the same channel. And then the process one unblocks and gets the value. So the channel itself is used for synchronization between these two threads. Similarly, if you write before the channel is... So if you write before, then as process two writes first and then process three reads later in between process two blocks again. So both read and write to these channels are synchronization points. And that is how you synchronize between threads. You don't need locks or anything. It just reads weight on the channel, so to say, right? When you're reading or writing. Let's look at some quick code in closure. So go is how you start one of these threads in closure, right? So go, print, I'll just start a new thread of activity. Closure calls them IOC threads for some reason. So I will print it, right? Creating a channel is very easy. You create a channel like this, give it a name. And then you say go, print, you read it inside out. So you take a value from the channel, print it. And this part you are running in a go thread, right? But this will block as we just saw in the previous diagram because the channel is empty. There is nothing in there. So this go thread will block there. But at a later time, you put some value in the go thread, sorry, in the channel, in another go thread. And the first, the previous go thread will resume and print hello, right? There's also a way to create buffered channels where you give some buffer there. You don't have to block immediately when you're doing a read or write, right? But you will eventually block after the buffer expires, right? How do we do multiplexing of channels? As in like I talked about STM, you can read from multiple channels atomically. How do you do that? So if you have three channels, right? I start one go thread and each one of them, I'm trying to read from all the channels at once using the alts function over there, right? So what alts does is that it will read value, a value from a channel, one of the channels, right? It'll give you the value on the channel as well which channel it read it from. But read it only from one channel and not any other. This is again something like Clojure STM where it's done atomically, right? So if you have three values in each channel, only one particular channels, one particular value will be read and other channels will stay untouched, right? So again, this will not print anything because the channels are empty, but some later time I put some values in the channel, then it'll print those values. But this may come out in any order because there is no guarantee of, you know, which order the alts run in, right? It's non-deterministic, right? So that is a very, very short introduction to channels in Clojure. Let's see how it works, right? I have five minutes. You have to exchange, let's see. So this is a very simple code there. You have a go block in which you say if x equal to true, x equal to one, you'd run two alts false. That's it. And then you macro expand it because go is a macro and then you run this pass to state machine function which actually comes with Kora sync. This is how Kora sync works. So what it does is that it takes this small bit of code and it converts it into this. Completely unreadable, but it's all generated code using macros, right? So how does it work? So Kora sync is based on what are called deep walking macros. Deep walking macros recursively walk through your entire code and transform them. They are like compilers, right? They'll walk through your whole code and then transform it into some other form, right? In this case, in case of Kora sync, this form happens to be a state machine, right? So it takes this very simple looking code and makes a state machine out of it. If I zoom, which I can't, you would see there are functions that are named like state machine and such, right? So what it does is transforms it to a state machine. So the state machines, so now you have the state machine and what do you do? So you run the state machine, which is what the go threads are. So go threads actually run on kernel threads. They run on the kernel threads which Java provides to the closure runtime. And when they encounter an blocking operation like putting into a channel or getting from a channel, those are the only blocking operations, a basic blocking operations in Kora sync, right? So when you put it into a channel or you get into a channel as described as shown in the previous slide, you're supposed to block there. The thread is supposed to block and yield so that some other thread can run, right? But how does it work in Kora sync? This is a slide stolen from the Ritchie, from Ritchie's Kora sync internal stock. So this is the internals of the channel. Interestingly, in Kora sync, the threads are not the interesting part. The channels are the interesting parts. So what happens is that when you do a blocking operation on a channel, the entire state of your go thread, the stack with the local variables, the function reference, the current state in the state machine, the state to resume at, everything is put into an array, a Java array. It's wrapped into a callback and the callback is pushed into the channel's internal queue, you see? This is the last example over there. When you're doing a put and your channel is full, you can't see the colors over there, but the last case is where the channel is full, then it gets put into a queue inside the channel, right? And then when the corresponding resume event happens for put it's take and for take it's put, then the last case you see, so as soon as the take happens, there's a space created in the queue and the handler runs and the code resumes because the whole of your code state was put into an array and stuffed into that other array, right? So all of core and async internals are nothing but arrays, lots of Java arrays, right? So that's how it works and it gives you an illusion of green threads, but they're not green threads, they run on kernel threads, they just pack and pack state into arrays again and again and again and it's written in a very, very clever way so that it can work with green threads as in go threads or kernel threads and it can even work with in JavaScript in Clojus script. Clojus script can compile core async to work on JavaScript as well where there are no threads at all. So it's a very fabulous implementation of CSP but I don't have time to get into the details, right? Yes, this is the last slide. So the pros are that core async is just a library. There's no special runtime needed like Alang or Haskell or Go or something. You can do dataflow programming very easily in core async because the library comes with lots and lots of combinators. They have map, filter, join, fork, pipeline, blah, blah. Huge amount of convenience functions to create dataflow programming and it works on JVM and on browser, right? And it's especially very nice on browser. The cons are if you do IO, blocking IO in green threads, it blocks them, right? As I talked about like green threads will block if you do blocking IO in them and Clojus does not have non-blocking IO by default. You can do non-blocking IO, but it's a bit tricky, right? And error handling is complicated. Java, you have exceptions and if you throw exceptions inside a Go thread, you don't know what will happen. It's just all like gets corrupted, right? So that's it I have for the very short talk. The learnings are green threads are really, really nice, right? You can run huge amount of load using green threads. If you use these higher level concurrency models then it decomplets your code. It makes it easier to reason about and such and different techniques are suitable for different situations, right? These are some references, mostly books and talks and that's it. Thank you. I can talk, catch up with me and talk about.