Applying C - Locking |
Written by Harry Fairhead | ||||
Tuesday, 27 May 2025 | ||||
Page 1 of 3 Threads are difficult to work with and not understanding locking is a big source of many difficult to find bugs. This extract is from my book on using C in an IoT context. Now available as a paperback or ebook from Amazon.Applying C For The IoT With Linux
Also see the companion book: Fundamental C <ASIN:1871962609> <ASIN:1871962617> In Chapter but not in this extract:
Atomic Operations and LocksAs we know a problem with multi-threaded programs is the possibility that two or more threads will try to change a shared resource in non-coordinated way. For example, if two threads are writing strings to the same file there is a good chance the strings will be written to the file all mixed together as they take turns at writing to the file in a disorganized way. Clearly what matters is whether an operation can be interrupted by another thread or not. Operations that cannot be interrupted are called “atomic”. For example storing a single int to memory is atomic and the action will be completed in one step and another thread cannot change the state of the memory location in the middle of it being changed. The big problem is that different architectures result in different operations being atomic. Consider again the operation of adding one to a memory location as in the counter example in the previous section. On some machines it might take a transfer to a register, add one and then transfer back to memory. As this is three operations it isn’t an atomic operation as another thread could modify the memory location by adding one while the first thread was in the middle of doing the same thing. That is, thread1 could add 1 while thread2 was adding 1 and the outcome would be that the memory location was incremented by 1. If adding 1 was an atomic operation the result would be 2. So far we have seen that non-atomic operations can result in the wrong answer i.e. the count isn't what you expect it to be, but it can also give rise to inconsistent results. For example, this is a risk whenever you update a complex data structure such as a struct. If the struct is shared then one thread might be updating all of the fields and this clearly isn’t a naturally atomic operation as it involves any number of writes to the struct. If another thread starts to read data from the struct then it is very possible that it will read a partially updated record and process it as if it was valid. For example, if we used two threads to update the record: struct person { char name[25]; int age; } me; then it is very easy to detect an invalid record. The thread function is designed to repeatedly “erase” the record’s data: void * resetRec(void *p) { for (;;) { strcpy(me.name, "xxxxx"); me.age = 0; } } The main program simply starts the thread: pthread_t pthread; int id = pthread_create(&pthread,NULL, resetRec, NULL); and then starts a similar loop to the thread, but in this case setting the struct to some reasonable data: for (int i = 0;; i++) { strcpy(me.name, "Harry"); me.age = 18; if (strcmp(me.name, "Harry") == 0 && } The if in the loop checks that the data in the struct is logical. If everything is working it can only be xxxxx and 0 or Harry and 18. If the if statement detects a Harry and anything other than 18 it prints the error message and stops. If you put all of this together and try it out you will find that it fails after at most a few thousand loops. Surprisingly, you will also discover that the struct is set to xxxxx and 0. What this means is that the thread changed the struct halfway through the if statement’s condition – after it has tested for Harry and before it tests for age. This is a failure mode you might not have expected. What is the solution to this problem? The most well known answer is locking but placing a lock around a set of operations can also be thought of as converting that operation into an atomic operation. |
||||
Last Updated ( Tuesday, 27 May 2025 ) |