ALL > Computer and Education > courses > university courses > graduate courses > modern operating system > zstu 2018-2019-2 class > student homework directory > L20182E060116 >
mutual exclusion and atomic transactions Version 0
👤 Author: by kaamssabrygmailcom 2019-04-16 02:28:02

mutual exclusion


Overview



File to use

module-4/mandatory/src/mutex.c

Descr iption

In this program, a process creates a number of threads and waits for their termination. The threads are divided into two sets I and D. A shared variable which works as a counter is initialized to 0 and then incremented by the threads in set I and decremented by the threads in set D. The sum of all the increments is equal to the absolute value of the sum of all the decrements.

Example

There are five threads in set I, each incrementing the counter 20 times by 2. In total the counter will be incremented by 5*20*2 = 200. There are two threads in set D, each decrementing the counter 20 times by 5. In total the counter will be decremented by 2*20*5 = 200.

Desired behavior

When all threads are done executing the value of the shared counter should be 0 (the same as value it was initialized with).


Compile and run


In the terminal, navigate to the module-4/mandatory directory. Use make to compile the program.

$ make


The executable will be named mutex and placed in the bin sub directory. Run the program from the terminal.

$ ./bin/mutex


Questions


Run the program several times and study the provided source code.

  1. Does the program work according to the specification?

  2. Can you predict the output before execution? Why?


Identify the critical sections


Identify the critical sections in the program. The critical sections should be as small as possible.

Mutual exclusion


Your task is to make sure that at any time, at most one thread execute in a critical section, i.e., access to the critical sections should be mutual exclusive. You will use three different methods to ensure that updates to the shared counter variable appear to the system to occur instantaneously, i.e., make sure updates are atomic.

Method 1 - pthread mutex locks


The first option you will study is to use the mutex lock provided by the pthreads library to enforce mutual exclusion when accessing the shared counter variable. Add the needed synchronization in the functions inc_mutexand dec_mutex to make the program execute according to the desired behavior.

Reference



  • Pthreads Manual: Mutexes


Method 2 - test and set


An alternative to mutex locks is to use the atomic test-and-set built-in provided by the GCC compiler to implement a spinlock.

At the beginning of mutex.c you find the following declaration.

/* Shared variable used to implement a spinlock */
volatile int lock = false;



  • Use the shared lock variable to implement the spin_lock() and spin_unlock() operations.

  • Change the functions inc_tas_spinlock and dec_tas_spinlock to use use the test-and-set spinlock to enforce mutual exclusion.





Mutual exclusion controls how many threads can simultaneously run a region of code. In Intel® Threading Building Blocks (Intel® TBB), mutual exclusion is implemented by mutexes andlocks. A mutex is an object on which a thread can acquire a lock. Only one thread at a time can have a lock on a mutex; other threads have to wait their turn.

The simplest mutex is spin_mutex. A thread trying to acquire a lock on a spin_mutex busy waits until it can acquire the lock. A spin_mutex is appropriate when the lock is held for only a few instructions. For example, the following code uses a mutex FreeListMutex to protect a shared variable FreeList. It checks that only a single thread has access to FreeList at a time. The black font shows the usual sequential code. Insertions added to make the code thread-safe, are shown in bold font.
Node* FreeList;
typedef spin_mutex FreeListMutexType;
FreeListMutexType FreeListMutex;
 
Node* AllocateNode() {
Node* n;
{
FreeListMutexType::scoped_lock lock(FreeListMutex);
n = FreeList;
if( n )
FreeList = n->next;
}
if( !n )
n = new Node();
return n;
}
 
void FreeNode( Node* n ) {
FreeListMutexType::scoped_lock lock(FreeListMutex);
n->next = FreeList;
FreeList = n;
}

The constructor for scoped_lock waits until there are no other locks on FreeListMutex. The destructor releases the lock. The braces inside routine AllocateNode may look unusual. Their role is to keep the lifetime of the lock as short as possible, so that other waiting threads can get their chance as soon as possible.

CAUTION


Be sure to name the lock object, otherwise it will be destroyed too soon. For example, if the creation of the scoped_lock object in the example is changed to
FreeListMutexType::scoped_lock (FreeListMutex);

then the scoped_lock is destroyed when execution reaches the semicolon, which releases the lock before FreeList is accessed.


The following shows an alternative way to write AllocateNode:
Node* AllocateNode() {
Node* n;
FreeListMutexType::scoped_lock lock;
lock.acquire(FreeListMutex);
n = FreeList;
if( n )
FreeList = n->next;
lock.release();
if( !n )
n = new Node();
return n;
}

Method acquire waits until it can acquire a lock on the mutex; method release releases the lock.

It is recommended that you add extra braces where possible, to clarify to maintainers which code is protected by the lock.

If you are familiar with C interfaces for locks, you may be wondering why there are not simply acquire and release methods on the mutex object itself. The reason is that the C interface would not be exception safe, because if the protected region threw an exception, control would skip over the release. With the object-oriented interface, destruction of the scoped_lock object causes the lock to be released, no matter whether the protected region was exited by normal control flow or an exception. This is true even for our version of AllocateNode that used methods acquire and release – the explicit release causes the lock to be released earlier, and the destructor then sees that the lock was released and does nothing.

All mutexes in Intel TBB have a similar interface, which not only makes them easier to learn, but enables generic programming. For example, all of the mutexes have a nested scoped_locktype, so given a mutex of type M, the corresponding lock type is M::scoped_lock.

TIP


It is recommended that you always use a typedef for the mutex type, as shown in the previous examples. That way, you can change the type of the lock later without having to edit the rest of the code. In the examples, you could replace the typedef with typedef queuing_mutex FreeListMutexType, and the code would still be correct.






Atomic Transactions


BizTalk orchestrations can be designed to run discrete pieces of work, following the classic 'ACID' concept of a transaction. These discrete or atomic units of work, when performed, move the business process from one consistent state to a new, consistent and durable state that is isolated from other units of work. This is typically done by using the Scope construct that encapsulates the units of work with the transactional semantics. The entire orchestration can also be defined as an atomic transaction without the use of scopes. The scopes, however, cannot be marked as transactional unless the orchestration itself is marked as a long running or atomic transaction type. Atomic transactions guarantee that any partial updates are rolled back automatically in the event of a failure during the transactional update, and that the effects of the transaction are erased (except for the effects of any .NET calls that are made in the transaction). Atomic transactions in BizTalk orchestrations are similar to distributed transaction coordinator (DTC) transactions in that they are generally short-lived and have the four "ACID" attributes (atomicity, consistency, isolation, and durability):

  • Atomicity

    A transaction represents an atomic unit of work. Either all modifications within a transaction are performed, or none of the modifications are performed.

  • Consistency

    When committed, a transaction must preserve the integrity of the data within the system. If a transaction performs a data modification on a database that was internally consistent before the transaction started, the database must still be internally consistent when the transaction is committed. Ensuring this property is largely the responsibility of the application developer.

  • Isolation

    Modifications made by concurrent transactions must be isolated from the modifications made by other concurrent transactions. Isolated transactions that run concurrently perform modifications that preserve internal database consistency exactly as they would if the transactions were run serially.

  • Durability

    After a transaction is committed, all modifications are permanently in place in the system by default. The modifications persist even if a system failure occurs.

    The following isolation levels are supported by the atomic transactions used in BizTalk Orchestrations:

  • Read Committed

    Shared locks are held while the data is being read to avoid dirty reads, but the data can be changed before the end of the transaction, resulting in non-repeatable reads or phantom data.

  • Repeatable Read

    Locks are placed on all data that is used in a query, preventing other users from updating the data. This prevents non-repeatable reads, but phantom rows are still possible.

  • Serializable

    A range lock is placed preventing other users from updating or inserting rows into the database until the transaction is complete.

    BizTalk Server ensures that state changes within an atomic transaction—such as modifications to variables, messages, and objects—are visible outside the scope of the atomic transaction only upon commitment of the transaction. The intermediate state changes are isolated from other parts of an orchestration.



 Note


If an atomic transaction contains a Receive shape, a Send shape, or a Start Orchestrationshape, the corresponding actions will not take place until the transaction has committed.


If you require full ACID properties on the data—for example, when the data must be isolated from other transactions—you must use atomic transactions exclusively.

When an atomic transaction fails, all states are reset as if the orchestration instance never entered the scope. The rule BizTalk has for atomic transactions is that all variables (not just local to the scope) participate in the transaction. All non-serializable variables and messages used in an atomic transaction should be declared local to the scope; otherwise, the compiler will give the "variable….is not marked as serializable" error. All atomic scopes are assumed to be "synchronized" and the orchestration compiler will provide a warning for redundant usage, if indeed the synchronized keyword is used for atomic scopes. The synchronization for the shared data extends from the beginning of the scope until the successful completion of the scope (including the state persistence at the end of the scope) or to the completion of the exception handler (in case of errors). The synchronization domain does not extend to the compensation handler.

Atomic transactions can be associated with timeout values at which point the orchestration will stop the transaction and the instance will be suspended. If an atomic transaction contains a Receive shape, a Send shape, or a Start Orchestration shape, the corresponding actions will not take place until the transaction has committed.

An atomic transaction retries when the user deliberately throws a RetryTransactionException or if a PersistenceException is raised at the time that the atomic transaction tries to commit. The latter could happen if for instance, the atomic transaction was part of a distributed DTC transaction and some other participant in that transaction stopped the transaction. Likewise, if there were database connectivity problems at the time when the transaction was trying to commit, there would also be a PersistenceException raised. If this happens for an atomic scope that has Retry=True, then it will retry up to 21 times. The delay between each retry is two seconds by default (but that is modifiable). After 21 retries are exhausted, if the transaction is still unable to commit, the whole orchestration instance is suspended. It can be manually resumed and it will start over again, with a fresh counter, from the beginning of the offending atomic scope.

 Note


Only the RetryTransactionException and PersistenceException can cause an atomic scope to retry. Any other exception raised in an atomic scope cannot cause it to retry, even if the Retryproperty is set to True. The two-second default delay between each two consecutive retries can be overridden by using a public property on RetryTransactionException (if that is the exception that is being used to cause the retries).


The atomic scopes have restrictions on nesting other transactions that cannot nest other transactions or include Catch - Exception blocks.

DTC transactions


While atomic transactions behave like DTC transactions, they are not explicitly DTC transactions by default. You can explicitly make them DTC transactions, provided that any objects being used in the transaction are COM+ objects derived from System.EnterpriseServices.ServicedComponents, and that isolation levels agree between transaction components.

 Note


An atomic transaction cannot contain any other transactions within it, nor can it contain exception handlers.



 Note


An atomic transaction cannot contain matching send and receive actions—that is, a request-response pair or a send and receive that use the same correlation set.


Non-serializable objects


If you want to use a non-serializable object within an orchestration, you must use it only within an atomic transaction. Any use of such an object outside of an atomic transaction risks data loss whenever the orchestration is persisted by the engine.

 Caution


Each Atomic scope represents a persistence point and what that means is the state of the orchestration is saved to the database at each persistence point. Extensive use of Atomic scope will increase the latency in the orchestration. Instead of using Atomic scope for the purpose of creating non-serializable objects, you can use Static method calls which they don not require Atomic scopes, thus eliminating the need for the persistence points.





Please login to reply. Login

Reversion History

Loading...
No reversions found.