Concurrency: "Simultaneous" Execution
- Josh Donais
- Oct 8, 2020
- 6 min read
We know about multiple cores in processors. When following new tech, we see the ongoing war between AMD and Intel to create processors with more cores at a higher CPU frequency. What more cores boils down to is: more load distribution. We can run multiple programs that want to hog the processor, and software developers can utilize these extra cores to offload tasks from the main process. In most cases, this is accomplished with parallel processing. What if a user had a single CPU without cores? How are they allowed to run multiple programs that seem to all be running at the same time?
Concurrent Processing
The CPU will switch between tasks quickly to make it appear to the user, as though all of the processes are running at the same time. This is known as concurrent processing. In order for the CPU to accomplish this, it must store all the registers, stack pointer and heap and then switch to another process, loading its stored data. There are two forms of concurrent processing: with processes and with threads.
Concurrent Processing with Processes
When a new child process is created (in POSIX, it would be with the Fork() system call) all of the process's data is copied to the child process. Both the parent and child process have isolated variables and do not share memory. Data sharing can be done with pipes but that is for another day.
Concurrent Processing with Threads
If a new thread is created (in POSIX, it would be with the pthread_create() system call) the heap is shared but other data is copied to the new thread. This allows for multiple threads to share data, which can save in process time and overhead.
Sharing data presents its own problems. If two threads enter the critical section of a program (e.g. a write operation), without proper handling, the thread that reaches this critical area first will enjoy the benefits of the write and a second thread can come in and write something on top of it, thus corrupting the data and desired function of the program. This is known as a race condition. Race conditions can be handled various ways, notably by mutex locks. As soon as one thread is allowed into the critical section of the program, in an atomic operation, it checks if the section is locked, if not, it then locks it. This prevents another thread from also entering the critical section until the first one is done. An atomic operation is one that cannot be reduced further, it has two states: unchanged and its final state. An atomic operation is required in a mutex lock. If the thread checked if the critical section was locked and then entered it on a separate operation, a different thread could also have checked and saw that the critical section was unlocked, then they would both enter.
Resolving race conditions with a mutex lock can present its own problems. One that comes to mind is a deadlock. An example I like to use is the Dining Philosophers Problem.

In the Dining Philosopher's problem, each philosopher represents a thread. Eating the noodles in the middle represent the task each thread wants to accomplish. The chopsticks are the resources that the threads need to accomplish the task. In order for a philosopher to eat the noodles, he needs two chopsticks. Assume that when a philosopher goes to pick up the chopstick, they always pick up the left one first. If every philosopher picks up their left chopstick before they pick up their right one, none of them will be able to eat. There are ways to program around this but I just wanted to demonstrate that working with shared memory can be difficult.
Concurrent processing with threads can be very useful for a server because you can handle all the processing of a client on a single thread. The server would share some resource, such as a database that would hold user information. Every time a new client connects and a new thread is created, the database would not have to be loaded.
What I've learned
I don't believe that I found anything surprising in the Network Programming class that reviewed this material. I was pretty familiar with it after it was covered in a different class from a previous year.
The assignments helped visualize how threaded concurrency is different than process concurrency. Sometimes I forget that only threaded concurrency shares the heap.
In the final project, I believe that I will utilize threaded processing because the chat server can handle each chat client on a different thread and share the communication space among the users.
I also feel that Dr. Frye adequately covered the material. However, in the pthread program, I believe students were confused by their output when it would print the same four times then change on them seemingly out of nowhere. This may have exacerbated someone who was already frustrated with the assignment, and cause them to question some changes they made. If sleeps were included in the base program but commented out in the final submission, hopefully students would remember that their output is supposed to be weird because the forced context switches from sleep would result in a (hopefully) visibly different output each time.
Observation from work
While going over this material, and while making a feature addition to a program at my internship, I encountered other programmers' implementation of a lock. I believe the following is an example of threaded concurrency on a server because each user's program can be thought of as a thread and the database files that are accessed/modified is the shared memory space between the threads. Below is my observation(with company permission).
The system is IBMi 7.xx.
The languages are SQL, RPGLE, and CL
Note: A job is basically a process.
Suppose this application will create a file which will have a variable name, svmatrix. The user can create several different files based on an enumerated list of names.

Check Lock
The snippet of code on the right is inside a function called "checkLock". It is embedded SQL that retrieves the user name, the job number, the locking event, and more information, from a database file called pdmtxctl. This file is used as a "lock" file.
If the user tries to perform operations on a file listed in the "lock file" they will be rejected.

Then a program is called, using the parameters found in the database file.

This program's sole purpose is to check all the jobs running and see if there is a job active that matches the input parameters.
If it exists, it returns i (inactive).

Lock
If anything is returned besides inactive, the program uses the chain() function and search the database file for a matching entry a second time. If it isn't found, a new record is created with the current session information. Anyone else wishing to run this program will not be able to modify our database files.

Unlock
When the user is done performing operations on svmatrix, the record created for it in the lock file pdmtxctl is deleted.
Analysis
IBM implements their own way to lock objects, their preferred method is to lock a single record, only, to prevent corruption of data. This implementation is interesting as it essentially creates a list of users who've locked the file for this particular program. The author of this implementation found it be necessary, mostly likely in the event that several files are created.
Multiple users could use this application to modify different tables at the same time.
The problem with this implementation lies in the way that it locks the files. The check and the lock should be done at the same time, as an atomic operation. If one user creates a file, then wants to edit it, in the time after the check but before the lock, another user could also try to edit the file this could be disastrous not at the record-level due to IBM's built-in locks, but at the file level, where new records could be created that one user or the other didn't want.
I believe that the developer was aware of this case, some clues lie in the number of times they check for the lock. They check for it in the "checkLock" function and then they check for it again in the "lock" function, right before they add the entry to the "lock file". I imagine the second check was to try and reduce the amount of time between the check and the lock to be as small as they could manage. There are various ways to ensure mutual exclusion, one way is by using a test-and-set-lock.

It is an atomic operation because it never actually checks whether or not the lock is set, it just repeatedly tries to set the lock (spin-wait) until it finally does.
Introducing spin-wait in an application like this would most likely end up with the user waiting for an unknown amount of time, unable to perform other tasks. This is known as resource starvation.
A test-and-set lock that would outright deny the user of the ability to modify the file (and include a warning) would be acceptable as is the current mode of denial. I believe that this would be implemented by setting an amount of time that the process would sit in spin-wait before sending an error to the user, denying them access. Perhaps they can try again later?





Comments