 Hi, hello, and welcome to program analysis. In this lecture of the course, we will look at two concurrent programs, so programs in which multiple things are happening at the same time. As in real life, when many things are happening at the same time, things might go wrong. And in particular, we will look for that reason into bugs that may happen in concurrent programs and how program analysis can help find these bugs. Here's an overview of the four topics that we will cover in this lecture. Each of them, as usual, has a separate video. So in this first part of the lecture, where we are right now, I will just give a brief introduction into what concurrency is and what kinds of problems one might see in concurrent programs. Then we'll have a look at one of these problems, namely data races, and we'll look at a dynamic analysis technique to detect data races. Next we look at thread safe classes, which are a very common way of implementing concurrent data structures in object oriented languages. But unfortunately there, of course, may also be bugs in these implementations, and we will see how to find some of those bugs. And finally, we look at the general problem of exploring different interleavings that a concurrent program may have, which is a problem that is relevant for all of these approaches, because only if you can actually trigger the right interleaving that triggers a bug, you may be able to detect that bug. Before looking into what can go wrong when people write concurrent programs and how program analysis can help find these problems, let's have a look at why people actually bother with concurrency. So there are basically two reasons. One of them is that you need concurrency to make best use of the computers that we have nowadays. There was a time in computing, which ended roughly around 2005, when the CPU clock speed was increasing automatically every year. And basically all people had to do was to wait for next year's processor to get more performance out of the programs that they had already written. Now these nice times are over, and instead we now have multi-core processes, which are basically now in every laptop and even phone that you can buy. And in order to make use of these multiple cores that are processing operations in one computer, you basically need concurrent programs that are running multiple threads of executions or multiple things concurrently. The other reason is that some problems that occur in the real world are just inherently concurrent. So for example, if you think of a server that is receiving requests from different clients, then obviously the server must be able to handle multiple concurrent requests because these requests may just come in more or less at the same time. And in order to do this, you obviously have a concurrent program. And then of course, there are some problems that people would like to compute, which are what some people call embarrassingly parallel, which basically means it's very easy to parallelize the problem and work on different chunks of this problem at the same time. And if you do this, then you obviously also have a concurrent program, which may be good for performance, but may not be so good for the correctness of the programs. When people implement concurrent programs, there are basically two styles that are used in practice. The first one is the so-called message passing style. Here the basic idea is that you have different instances that are computing in parallel and they exchange information with each other by sending messages from one to another. In an ideal instantiation of this idea, you do not have any shared memory. So basically all the communication between these different computation instances is happening through messages. Just to give you two examples of instances of this message passing paradigm. So one of them is MPI, the message passing interface, which is very heavily used in large-scale scientific computing, where computations are run on many, many different computers and they exchange intermediate results with each other by basically sending messages from one node or computer to another. The second example is the actor concurrency model, which has been made popular a while ago in Erlang and is now, for example, also very popular in Scala, where you have different actors. So these are the concurrently computing entities that exchange messages by sending each other messages while the program is executing. The second concurrency style, and that's the one that we will actually focus on here in this lecture, is thread-based shared memory concurrency. So in this style there are multiple concurrently executing threads, so you again have these entities that are running at the same time. But the main difference is that all these threads access the same shared memory. So there's one big global memory that different threads are accessing and in order to make sure that while different threads are accessing the same memory, you need to synchronize these memory accesses through things like logs or barriers and the big problem is how to actually make sure that these logs and barriers and other mechanisms are used at the right place so that you do not get any concurrency bugs. So this second style of concurrency, thread-based shared memory concurrency is the focus of this lecture and we will see how to use program analysis to find problems in programs that use this concurrency style. To illustrate the challenges that a program analysis has when reasoning about the behavior of a concurrent program, let's have a look at a very simple example. So here in this example we start by a main thread of execution and this main thread is initializing some variables a and b and then also some other variables r and t. This is Java like syntax but in principle it could be any language where you have multiple threads that can access shared memory. And then after this initialization we have two threads down here, thread one and threads two, that both are writing to these shared variables a and b and r and t. And now the question is what does this program actually mean? So what is the value of say variable a or b after executing this program? And the answer is well the behavior depends on the thread interleaving so it depends on which of these instructions that are executed in thread one and thread two are executed at what point in time because they may influence each other. So let's have a look at what behaviors this little program may have by looking at the different thread interleavings that may happen while this program is executing. So one possible thread interleaving is the following where the program after the initialization code starts by first executing the statement at the beginning of thread one followed by the second statement in thread one. So we are setting a to one and r to true and then we're executing the two statements in thread two. So next we are setting t to the value of r so t will now be true and then b to the value of a so that at the end b will also be one. So at the end of this program we see that for example t is true and b has value one. Now of course this is not the only way this program may execute. A second thread interleaving looks as follows. So here again we start after the initialization with the first statement of thread one but then the control switches to thread two and we next execute this first statement of thread two followed by say the second statement of thread two. So in this case b will again be one but t will have the old value of r which was false and then at the end we execute the remaining statement from thread one which is setting r to true. So at the end of this execution we will have a different state than in the first execution because now here we see that t will have value false at the end and b just as in the first execution will have value one. So of course these two executions aren't all that may happen so another one would be this one where we again start with the first statement of thread one then move over to thread two and execute its first statement but then immediately afterwards go back to thread one and execute this r equal true statement and then go back to threads two again and have this statement where we assign the value of a to our variable b and now again we will have some final state here and let's just look at t and b. So in this case it will have the same final state as execution two so t will be false and b will be equal to one even though actually a different interleaving has happened. So the statements have executed in a different order than in the second execution but we still see the same behavior or the same state at the end of this little program. Now in all the executions that we've seen so far we've always started with the first statement of thread one and now we can basically do the same again but now starting with threads two all the time and this will give us three more interleavings. One of these three new interleavings is this one where we start with the two instructions or statements of thread two followed by the two statements of thread one and as you'll see this actually gives a different final state than all the three others that we've seen so far because here at the end t is false but b is zero which is something we haven't seen before. Here's yet another interleaving where we again start with the first statement of thread two and then move over to thread one and executes all its two statements but then move back to thread two to execute b equal a so the last statement in thread two and at the end the state that we'll get is that t is false and b is one so something that we have already seen before but now with a different interleaving and this is the sixth and final interleaving that this program may have so here we are starting in thread two and then basically switch the thread after each statement which at the end gives us the state where t is false and b is one so now you might say well okay there's so many different behaviors is there anything we can say at all about the behavior of this program and actually there is so even in a concurrent program where you may get different behaviors depending on the interleaving there are some things you can say about the behavior of the program of course and one thing you can say about all possible executions of this little program is that whenever t is true at the end then this implies that b is equal to one so this is one of the post conditions if you want of this program that we could state while explaining this example i've made an implicit assumption that i would like to make more explicit now and this assumption is that our programs execute under what is called sequential consistency so sequential consistency essentially means two things one of them is that the program order is preserved which means that in each threads execution the instructions happen in the order in which they are actually in the code of this thread so there's no reordering of instructions within a single thread the second part of sequential consistency is that the shared memory that different threads are accessing concurrently essentially behaves like a global array which means that whenever you read something you read the most recent value and whenever you write something then this write happens immediately so that if another instruction maybe in a different thread is reading that value afterwards it will see this most recently written value now we will make this assumption of sequential consistency for the rest of this lecture just keep in mind that actually many real world platforms have even more complex semantics so if you just search for memory models you'll find out for example that this yeah the java language has a more complex memory model that allows some kind of reordering of some instructions and that allows some writes to be seen only later by some threads but we're not really getting into this here but instead assume that we have sequential consistency in all the programs that we are analyzing so now if you have a concurrent program um you may ask well what could go wrong so what what could happen that does not um yeah match what the developer is expecting when writing the program the by far most common problem in concurrent programs are so-called data races and this is what we will focus on in most of this lecture so let me just define what a data race is so there are basically three conditions one is that you have two accesses to the same shared memory location then the second condition is that at least one of these two accesses is a write and the third one is that the ordering of these accesses is non-deterministic which basically means there may be an execution where access one happens before access two and another execution where access two happens before access one and whenever these three conditions are true then your program indeed has a data race so as an example let's have a look at a very simple program written in a java-like language so here we again have some initialization code that happens before the concurrent threads are executing which is initializing the balance of for example a bank account to 10 and then we have two concurrently executing threads where one is trying to deposit some money and the other one is trying to withdraw some money and now the way these two threads are executed um or sorry implemented is such that um thread one will first read the balance into a temporary variable and then add something to this temporary variable and then write it back to our shared variable balance and on the other side the thread that is withdrawing money um will also read the current balance into its local temporary variable then subtract some value from this uh read balance and then write the result back into balance now what could go wrong here well um let's look at the terminology that we've just used to define what a data race is first um so this balance variable here is our shared memory location and then we have some concurrent accesses here namely this read of balance here and also here and two writes of balance here and also here now if you look at the definition of data race again we need to have um two concurrent accesses to a shared memory location where at least one is a write and the accesses happen in a non-deterministic order well then we'll have three races in this program um namely the ones that you can see here so for example we have a race between this read of balance and this write of balance in the other thread then maybe we have a race um between this read of balance and this write of balance and finally of course there's also a race between this right and this right and for each of these races the question is basically which of these accesses will happen first so they are both racing to be first and depending on which one happens first we may get um a different overall behavior of this program to make you think a little bit more about these different possible behaviors that this program may have because of these data races I have a little quiz which is basically about the values that balance may have after executing all the code that you see here so basically after thread one and thread two each have executed all their statements and the program has finished so what are the possible values that balance may have so I just invite you to stop the video here for a second think about it for whatever time you need and then continue the video all right so continuing with the answers so the possible outcomes that this program may have is that balance maybe three or eight or 15 at the end of executing these two threads but of course only the value eight is correct because if you have 10 let's say euros and then add five and subtract seven you want to have eight at the end and not three and not 15 so without proper synchronization on these concurrent accesses of balance we have data races which lead to incorrect behavior in this program so now of course this incorrect behavior that we've just seen is not what you want to have if you're for example running a bank because you want to make sure that people always have to write balance on their bank accounts and the typical way to avoid data races is to use logs that ensure that shared memory accesses do not interfere with each other and conceptually you can here see how this is typically done so we are using these two instructions to acquire and release a particular lock and the idea is that these instructions when they're referred to L all refer to the same log which basically means that these two sections of code this one here and that one here are mutually exclusive because only one of them can have the lock at a given point in time which means if one of them is executing these two statements then the other one has to wait until the first one is done and if you do this then you get rid of these data races that you've seen and the only behavior that this program can have is that balance indeed has the correct value at the end just to show you how this looks like in a more real programming language so if i would use java syntax instead of this acquire and release then it would basically look like this where we would surround these statements with a synchronized block and the synchronization would happen on the same log l so that these two yeah blocks are again mutually exclusive and we get no data races but the intended behavior all right and this is already all i have for the introduction into this lecture on analyzing concurrent programs so you hopefully now have a rough idea what concurrency is what kinds of problems exist in particular data races and in the remaining parts of this lecture we will look into program analyses to find some of these problems automatically thanks for listening and see you next time