We have already talked about some of the complexities of implementing a functionally correct parallel application. One particular problem is that of data races. In a large pro-gram, it can be quite hard to truly eliminate all possible races. Even using data race detection tools does not guarantee that there are no further races hidden in the code.
The safe software solution is to place mutex locks around all potentially critical sections of code. The mutex locks ensure that only a single thread can access the critical variables and hence that the application will work correctly. However, mutex locks both impose a performance penalty on access to the shared variables and also work only if all accesses to the variables are protected by a common lock.
The idea of transactional memory is to attempt to address both issues. Transactional memory is really aimed at the correctness issue. If it can also make a difference to per-formance, that is an additional benefit rather than a critical benefit.
Transactional memory enables the developer to protect accesses to variables within a transaction. A transaction is a block of code that either completes successfully or fails and does not then result in any change in system state.
A common syntax for transactions has not yet been developed. One possibility is the use of the keyword atomic to wrap the entire transaction. Listing 10.25 shows an exam-ple of this. The transaction moves a value from one location in an array to another. The keyword atomic indicates that the specified amount must be moved atomically between the two locations in the array. Since transactions can fail, the atomic keyword must implicitly retry the transaction until it successfully completes. The critical points here are first that the transaction cannot leave the data in an unknown partially completed state and second that no other process can see a partially completed transaction. This makes the use of the keyword atomic appropriate.
Listing 10.25 Accessing Multiple Accounts in a Single Transaction
void move( int from, int to, int value )
accounts[ from ] -= value;
accounts[ to ] += value;
Transactions will fail if any of the variables used in the transaction are modified (or potentially read) by another thread. This is where transactions can improve the safety of parallel code. If there is another modification of a variable used in a transaction outside of that transaction, the transaction will fail. It is not possible for the transaction to com-plete in the presence of data races. This does not stop situations where multiple threads access a variable outside of a transaction, but it does eliminate problems with the granu-larity of locks or potential deadlock situations.
Transactional memory can be provided either in software using a library or at the hardware level. Hardware transactional memory is the ideal. This is where during a trans-action, hardware tracks any other accesses to variables used by the transaction and aborts the transaction if necessary.
Software transactional memory is provided by a library that ensures that the variables used in the transaction are accessed atomically only. As such, it tends to have a much higher implementation cost compared to hardware transactional memory. Providing transactions in software can help address the correctness issue; however, it is unlikely to do so in a way that also leads to performance gains over using any other approach.