Concurrency is one of the trickiest aspects of software development. Whenever you have multiple processes or threads manipulating the same data, you run into concurrency problems.
Concurrency is hard to test for. In general, automated tests are great foundational steps for software development but results of these tests are not sufficient enough to assure about security of the application in times of concurrency problems.
In day to day life of a software developer, transaction managers play an important role to avoid concurrency issues from bothering them. Transactions provide a framework, within which data manipulation is safe from different aspects of concurrency in an application. In database scope, Transactions pull together several requests that the client wants treated as if they were a single request.
However, in developmental process, scope of work is not limited to a single database transaction but there can be situations where data transcends transaction. We also encounter multi-threaded application servers where concurrency issues can be faced and usually are dealt with use of several platforms or framework.
Lets us categorize our concurrency problems:
· Lost updates — when two entities are working on same object and update of one’s work is lost or overwritten by the other.
· Inconsistent read — reading two things which are correct pieces of information however not correct at the same time.
Effect of the above concurrency problems can be seen on the correctness and liveness of the application. If we take measures for correctness, such as limiting access so that only one entity can work on an object at a time, it might affect liveness of the system, which would be the ability to do multiple things at a time.
Common Solutions for Concurrency: Isolation & Immutability
Isolation is partitioning the data to ensure any chunk of it can be accessed by one active agent. It is a vital technique which greatly reduces chances of error. It helps program enter an isolated zone where it does not have to worry about concurrency problems. For example: file locks.
Immutability means data can not be modified. However, this can not be the case all the time. However, by identifying some data as immutable, we can relax our concurrency concerns.
How to deal with Mutable date?
As we know, while working in enterprise application, majority of data cannot be immutable hence, we will discuss concurrency control measures to deal such scenarios.
It allows two entities to operate on an object by making a copy of it. First entity to commit the changes usually goes trouble free, however, second entity while committing changes will face conflict and will have to deal with it by modifying changes accordingly.
In this case, first entity to work on the object restricts other entity to work upon it until changes of first entity is committed.
A good way of thinking about this is that an optimistic lock is about conflict detection while a pessimistic lock is about conflict prevention.
Pessimistic approach might seem more effective while reducing concurrency concerns, but it affects system’s liveness to great effect. Hence, use of these solutions must be entirely situational usually based upon frequency and severity of conflicts.
Pessimistic approach often leads to deadlocks. Deadlocks usually occur when two entities working on two objects separately try to make changes on the object which other was working upon. It leads to a scenario where each entity is waiting for the lock to get over to progress. Problem gets worse when number of entities increases.
There can be different approaches to deal with deadlocks. One approach usually picks a victim after detecting a deadlock and discards the changes related to it so that others can make progress. Ensuring time limits on locks can also be a viable option but will affect liveness of system especially in the cases when deadlocks are not present, yet system makes entities wait to work on the respective objects.
Transaction is a bounded sequence of work where all participants are in consistent state before and after a transactional process. A Transaction must complete on all or nothing basis. A software transaction must adhere to ACID properties, coined in 1983 by Theo Härder and Andreas Reuter.
Atomicity ensures that while working in the scope of transaction, each step of the sequential action set must either successfully commit or else rollback. The ability to abort a transaction on error and have all writes from that transaction discarded is the defining feature of ACID atomicity.
Ensures that state of resources must be in consistent state before and after a transaction. Consistency depends on the application’s notion of invariants, and it’s the application’s responsibility to define its transactions correctly so that they preserve consistency. Atomicity, isolation, and durability are properties of the database, whereas consistency is application oriented.
Ensures concurrently executing transactions are isolated from each other which implies that result of an individual transaction must not be visible to any other open transactions until that transaction commits successfully. Serializability on database can be used to achieve isolation but it leads to bad performance.
Ensures that result of a successful transaction is committed and made permanent, even if there is a hardware fault or the database crashes.
Transactions and concerns
In software world, we usually run into transactions in terms of databases however, there can be other scenarios where transactions can come handy.
Note: In general, a transactional process is usually kept short to increase liveness of the application as well as increase throughput. A common approach is to open a transaction as late as possible ensuring reads are done outside transactional scope and transaction is opened only for an update. However, leaving reads outside transactional scope might also lead to situation of inconsistent reads.
Most transactional systems use the SQL standard which defines four levels of isolation with Serialization being the strongest one.
If the isolation level is serializable, system makes sure that in scenarios where two entities are working on an object where one is modifying it and other is reading it, then, the one reading will always get the correct state which would be either the state before modification or the state after the modification.
Serialization ensures system correctness but takes a good impact on liveness of the system.
In this article, we have discussed about concurrency and issues related to it. We also looked at some common techniques which can be used to avoid or solve concurrency problems. Each solution comes with its pros and cons and it is up to application developer to make sure the solution being implemented fits the situational requirement of the system.