Skip to main content

Locking and Concurrency Control

An important feature of any multi-process system is concurrency control, which prevents multiple processes from modifying a single data element simultaneously, thereby preventing data corruption. Consequently, InterSystems IRIS® data platform provides a lock management system. This page provides an overview.

Locking Overview

Locking prevents different processes from changing the same element of data at the same time. The basic locking mechanism is a lock command, LOCK in ObjectScript and iris.lock() in Python, which delays activity in one process until another process signals that it is permitted to proceed. InterSystems SQL also includes locking behavior and controls for data access; see Locking in SQL and Objects.

In InterSystems IRIS, locking requires that mutually competing processes use the same lock names. Take this scenario:

  1. Process A issues a lock command on a global, and InterSystems IRIS creates an exclusive lock on that global. Process A then makes changes to nodes in a global.

  2. Process B issues a lock command on the same global with the same lock name. When process B finds out that an exclusive lock exists, it pauses. The lock call does not return, and no additional lines of code can be executed.

  3. When process A releases the lock, process B's lock command finally returns, and process B continues. Process B can now modify the nodes in the global.

This example illustrates the importance of lock names. Processes must use the same names to coordinate access to shared data. For details on choosing and structuring lock names, see Naming Locks.

How Locks Work: Anatomy & Lifecycle

This section defines the components that make up a lock in InterSystems IRIS. Every lock has the following elements, established at the time of acquisition, that determine how it functions and at what scope:

Lock name

The identifier of the resource being protected. By convention, names mirror the global or node (for example, ^MyGlobal(1)). Competing processes must use the same name to coordinate. See Naming Locks.

Mode (shared vs. exclusive)

Controls contention: shared allows concurrent readers; exclusive blocks all other locks of that name. Exclusive is the default. See Exclusive and Shared Locks.

Acquisition style (incremental vs. simple)

Determines how existing locks are handled when acquiring new ones. Incremental adds to what you already hold (reference-counted); simple replaces the current set. Python locks are always incremental; in ObjectScript, LOCK +name is incremental and LOCK name is simple. See Incremental and Simple Locks.

Timeout

The duration a process should wait for a lock request to succeed before timing out, with 0 indicating non-blocking. See Locks with Timeouts.

When your process requests a lock, the following occurs:

  1. InterSystems IRIS enqueues it for the name.

  2. If a conflicting lock exists (based on the requested mode), your process waits up to the timeout; otherwise, it acquires immediately.

  3. On success, your code proceeds; on timeout, handle it per language: in ObjectScript, check $TEST; in Python, catch the IRISTimeoutError exception.

  4. Your process releases the lock, or it is released automatically when the process ends (visible in the lock table).

Core Locking Concepts

Naming Locks

One of the arguments for a lock command is the lock name. Lock names are arbitrary, but by universal convention, programmers use names identical to the items they lock. Usually, the item to be locked is a global or a node of a global. Lock names typically follow the same naming conventions as local and global variables, including case sensitivity and the use of subscripts. Find out more in Variables.

Caution:

Do not use process-private global names as lock names (you would not need such a lock anyway because, by definition, only one process can access such a global).

Competing processes need to use the same lock name when accessing shared data. In InterSystems IRIS, locking is not enforced at the data level; it is enforced by agreement between processes that use the same lock name. If two processes use different lock names for the same global or data structure, they can both acquire locks independently and modify the same data without coordination. To prevent conflicts, establish clear naming conventions for locks and apply them consistently across all code that accesses shared data. Lock names should be meaningful and clearly identify the global or node being locked.

Tip:

Since lock naming is a matter of convention and lock names are arbitrary, it is not necessary to define a variable before creating a lock with the same name.

Lock Names and Performance

The form of the lock name affects performance because of how InterSystems IRIS allocates and manages memory. Locking is optimized for lock names that use subscripts. An example is ^name("ABC").

In contrast, InterSystems IRIS is not optimized for lock names such as ^nameABC or ^nameDEF. Non-subscripted lock names can also cause ECP-related performance problems.

For a visual walk-through of how locking a subscripted node implicitly affects its ancestors and descendants, see Example: Locking Arrays and Subnodes.

Locks with Timeouts

Timeouts specify the duration a process should wait for a lock request to succeed before timing out. If a lock cannot be applied within the specified timeout period, the process will stop waiting, and the lock request will fail. Timeouts are especially relevant to incremental locks to avoid deadlocks.

A timeout does the following:

  1. Attempts to add the given lock to the lock table. The lock is added to the lock queue, if one exists.

  2. Pauses execution until the lock is acquired or the timeout period ends, whichever comes first.

  3. For ObjectScript LOCK: Sets the value of the $TEST special variable. If the lock is acquired, InterSystems IRIS sets $TEST equal to 1. Otherwise, InterSystems IRIS sets $TEST equal to 0.

    For Python iris.lock(): Raises IRISTimeoutError if the timeout period elapses before the lock is acquired; otherwise, returns normally.

See Creating Locks with Timeouts for examples and more.

Incremental and Simple Locks

A lock can be acquired in two basic modes: incremental or simple. These modes control how a process manages existing locks when acquiring new ones.

Incremental Locks

An incremental lock adds a new entry for the specified name without releasing any existing locks. Each acquisition increments a reference count. The lock is only released when the count decrements to zero, preventing other processes from acquiring it until then.

Note:

By default, all Python locks are incremental.

# Exclusive, non-escalating lock, no wait
iris.lock("", 0, "^MyGlobal", 1)

LOCK +^MyGlobal(1)

Incremental locks are the standard way to protect critical sections because they let you hold multiple locks at once, and acquire them at different times during execution. They are best combined with timeouts to avoid deadlocks.

Simple Locks

In ObjectScript, simple locks are atomic replacements of the held set; they're rarely used outside small critical sections or legacy code. A simple lock replaces all existing locks held by the process with the new one(s). Unlike incremental locks, it does not maintain a reference count across acquisitions.

LOCK (^MyVar1,^MyVar2,^MyVar3)

Simple locks are less common because applications usually need to acquire multiple locks at different stages of processing. However, you may specify lock types and timeouts with simple locks.

Note:

Simple locks are ObjectScript only. They cannot be used in Python.

Managing the Lock Table

InterSystems IRIS maintains a system-wide, in-memory table that records all current locks and the processes that own them. This table, the lock table, is accessible via the Management Portal (System Operation > Locks > Manage Locks), where you can view the locks and (in rare cases, if needed) remove them. Note that a given process can own multiple locks with different names (or even multiple locks with the same name).

When a process ends, the system automatically releases all locks it owns. Thus, it is not generally necessary to remove locks via the Management Portal, except in cases of an application errors.

Note:

When viewing locks in the Management Portal, the Directory column shows the database path to which the lock applies. This is particularly useful for understanding locks acquired across namespaces or through global mappings.

Lock Table Limits and Tuning

The lock table cannot exceed a fixed size, which you can specify using the locksiz setting. For information, see Monitoring Locks. Consequently, the lock table may fill up, preventing further locks. See Lock Table Full for more information.

Note:

Implicit locks are not included in the lock table and thus do not affect its size. For an example of implicit locks created by locking array nodes, see Example: Locking Arrays and Subnodes.

Lock Table Full

If the lock table reaches capacity, InterSystems IRIS writes the following message to the messages.log file:

LOCK TABLE FULL

Filling the lock table is not generally considered to be an application error; InterSystems IRIS also provides a lock queue, and processes wait until there is space to add their locks to the lock table.

Lock Types and Variations

Lock type codes modify a lock's behavior at the moment you acquire it. There are four lock type codes, shown below; they are not case-sensitive.

This documentation gives an overview of lock type behavior. See ObjectScript reference for further detail on lock types.

These lock type codes can be combined (as is the case with EI, produces the effect of both E, escalating and I, immediate unlock, thus creating an exclusive escalating lock with immediate unlock).

Lock Type Summary
  Exclusive Locks Shared Locks (#"S" locks)
Non-escalating Locks
  • locktype omitted - Default lock type

  • "I" - Exclusive lock with immediate unlock

  • "D" - Exclusive lock with deferred unlock

  • "S" - Shared lock

  • "SI" - Shared lock with immediate unlock

  • "SD" - Shared lock with deferred unlock

Escalating Locks (#"E" locks)
  • "E" - Exclusive escalating lock

  • "EI" - Exclusive escalating lock with immediate unlock

  • "ED" - Exclusive escalating lock with deferred unlock

  • "SE" - Shared escalating lock

  • "SEI" - Shared escalating lock with immediate unlock

  • "SED" - Shared escalating lock with deferred unlock

Assigning Lock Types

To assign a lock type:

 iris.lock(lock_mode, timeout, ^+lockReference, subscripts)
 LOCK +lockname#locktype

Where lock_mode or Locktype is one or more lock type codes, in any order, enclosed in double quotes for addition (or removal). In ObjectScript, a pound character (#) must separate the lock name from the lock type.

Exclusive and Shared Locks

Any lock is either exclusive (the default) or shared (S). These types have the following significance:

  • While one process owns an exclusive lock (with a given lock name), no other process can acquire any lock with that lock name.

  • While one process owns a shared lock (with a given lock name), other processes can acquire shared locks with that name, but no process can acquire an exclusive lock with that name.

The typical purpose of an exclusive lock is to indicate that you intend to modify a value and that other processes should not attempt to read or modify that value until you are done. The typical purpose of a shared lock is to indicate that you intend to read a value and that other processes should not attempt to modify that value; they can, however, read the value.

For a detailed example, see Example: Protecting Application Data.

Non-Escalating and Escalating Locks

Any lock is either non-escalating (the default) or escalating (E). These types determine how InterSystems IRIS manages memory and lock tracking when your application holds many locks on related nodes. They have the following significance:

  • For non-escalating locks, each node you lock is tracked individually in the lock table. This gives precise control, but can consume memory if you lock many nodes.

  • For escalating locks, when a process locks more than a specific number (by default, 1,000) of parallel nodes at the same subscript level, InterSystems IRIS automatically “escalates” them. It replaces the individual node locks with a single lock on the parent node, implicitly locking the entire branch. Releasing child node locks decrements the count. When enough locks are removed, InterSystems IRIS automatically removes the parent-level lock.

Note:

Escalation applies only to subscripted lock names. Attempting to escalate a flat name results in a <COMMAND> error. The escalation threshold is configurable via LockThreshold.

The typical purpose of an escalating lock is to manage large numbers of locks without overwhelming the lock table. By contrast, use non-escalating locks when fine-grained concurrency and memory usage are not a concern.

For a detailed example, see Example: Escalating Lock.

Deferred and Immediate Unlocks

The lock type codes I (immediate) and D (deferred) control when a lock is released relative to transactions. These options adjust unlock timing only; they do not change the lock name or mode. They are not case-sensitive and cannot be used together for the same lock operation.

Note:

These lock types are only available in ObjectScript, not Python.

  • Immediate unlock releases the lock as soon as the unlock is issued, regardless of whether a transaction is active. Use when exclusive access is no longer required, but the transaction continues for other work.

  • Deferred unlock schedules the unlock to occur at the end of the current transaction (commit or rollback). Use to keep the resource protected until the transaction reaches a durability boundary.

For syntax, see Assigning Lock Types. For end-to-end demonstrations, see Example: Immediate Unlock and Example: Deferred Unlock. Related timing behavior is shown in Example: Timed Lock.

Locking Syntax and Examples

Adding and Removing Locks

To add a lock, use a lock command as follows:

iris.lock(lockMode, timeout, lockReference, *subscripts)

 LOCK +lockname#locktype :timeout

There are different types of locks, each with distinct behaviors. With lock_mode or #locktype, you can specify the lock variation. Learn more about the lock types available in Lock Types.

In the above example, you can specify a timeout, where timeout is the timeout period in seconds. If you specify a timeout, this lock becomes an Incremental Lock with a Timeout. A timeout is an effective way to reduce the risk of deadlock. If you specify timeout as 0, InterSystems IRIS makes a single attempt to add the lock. In Python, a lock attempt that times out raises IRISTimeoutError; in ObjectScript, $TEST is set to 0.

Tip:

Hold locks only as long as necessary. Keeping them too long increases the risk of contention and performance bottlenecks.

Unlocking All Locks

To remove all locks held by the current process:

 iris.releaseAllLocks() 
 LOCK 

For both Python and ObjectScript, these contain no arguments.

Unlocking all locks is not common practice. It is best to release specific locks as soon as possible. Locks are automatically released when their related process ends. Avoid using this in shared processes or servers, as it can remove unrelated locks held by helpers within the same process.

Removing Typed Locks

To remove a lock of a specific type:

iris.unlock(lock_list, timeout_value=None, locktype=None)

 LOCK -lockname#locktype 

Examples:

# Remove one shared lock on ^G(1)
iris.unlock(['^G(1)'], None, "S")

# Remove one escalating lock on ^G(1)
iris.unlock(['^G(1)'], None, "E")

# Remove one exclusive, non-escalating lock on ^G(1)
iris.unlock(['^G(1)'])

 LOCK -^G(1)#"S"   ; removes one shared lock
 LOCK -^G(1)#"E"   ; removes one exclusive escalating lock
 LOCK -^G(1)#"SD"  ; removes one shared lock with deferred unlock

Creating Incremental Locks

An incremental lock allows you to apply the same lock multiple times, effectively incrementing the lock count. By default, all locks made in Python are incremental.

To add an incremental lock:

 # Python locks are always incremental; here with 0-second timeout (nonblocking)
iris.lock("", 0, "^Customer", 1234)

 LOCK +lockname

A process can add multiple incremental locks with the same name; these locks can be of different types or the same type. Learn more about lock types in Lock Types.

To learn more about incremental locks, see Incremental Locks.

Creating Locks with Timeouts

Timeouts specify the duration a process should wait for a lock request to succeed before timing out.

The following are examples of incremental locks with timeouts:

iris.lock(lock_mode, timeout, ^+lockReference, subscripts)

 LOCK +lockname#locktype :timeout

Where timeout or :timeout is the timeout period in seconds. In ObjectScript, the space before the colon is optional. If you specify a timeout as 0, InterSystems IRIS will attempt to add a lock.

Note:

If you try to take a lock on a parent node with a 0 timeout and already have a lock on a child node, the zero timeout is ignored, and an internal 1–second timeout is used instead.

If you're using a timeout argument, you may be advised to build in a check for the value of the $TEST; in Python, wrap iris.lock() in a “try/except” block and catch IRISTimeoutError if the lock cannot be acquired in time. The following shows an example:

try:
    # Try up to 2 seconds to acquire an exclusive, non-escalating lock
    iris.lock("", 2, "^ROUTINE", routinename)
    # ... protected work ...
finally:
    # Release using the same name
    iris.unlock([f'^ROUTINE("{routinename}")'])
 Lock +^ROUTINE(routinename):0 
 If '$TEST {  Return $$$ERROR("Cannot lock the routine: ",routinename)}

Namespaces and Locking

Locks are typically used to control access to globals. Because a global can be accessed from multiple namespaces, InterSystems IRIS provides automatic cross-namespace locking support. The behavior is automatic and needs no intervention. There are several scenarios to consider when understanding the implications of this:

  • Every namespace has one or more default databases, which contain data for persistent classes and any additional globals; this is the globals database for this namespace. When you access data (in any manner), InterSystems IRIS retrieves it from this database unless other considerations apply. A given database can serve as the globals database for more than one namespace. See Example: Multiple Namespaces with the Same Globals Database.

  • A namespace can include mappings that provide access to globals stored in other databases. See Example: Namespace Uses a Mapped Global.

  • A namespace can include subscript level global mappings that provide access to globals partly stored in other databases. See Example: Namespace Uses a Mapped Global Subscript.

  • Code running in one namespace can use an extended reference to access a global that is not otherwise available in that namespace. See Example: Extended Global References.

Although lock names are intrinsically arbitrary, when you use a lock name that starts with a caret (^), InterSystems IRIS provides special behavior appropriate for these scenarios. The following subsections give the details. For simplicity, only exclusive locks are discussed; the logic is similar for shared locks.

Example: Multiple Namespaces with the Same Globals Database

While one process holds an exclusive lock with a given lock name, no other process can acquire a lock with that name.

If the lock name starts with a caret, this rule applies to all namespaces that use the same globals database as the locked process.

For example, suppose the namespaces ALPHA and BETA are both configured to use database GAMMA as their globals database. The following shows a sketch:

Then consider the following scenario:

  1. In namespace ALPHA, process A acquires an exclusive lock named ^MyGlobal(15).

  2. In namespace BETA, process B tries to acquire a lock with the name ^MyGlobal(15). This lock command does not return; the process is blocked until process A releases the lock.

In this scenario, the lock table contains only the entry for the lock owned by process A. If you examine the lock table, you will notice that the Directory column indicates the database to which this lock applies. For example:

Example: Namespace Uses a Mapped Global

If one or more namespaces have global mappings, InterSystems IRIS automatically enforces the lock mechanism across all applicable namespaces. The system automatically creates additional lock table entries when locks are acquired in the non-default namespace.

For example, suppose that namespace ALPHA is configured to use database ALPHADB as its globals database. Suppose that namespace BETA is configured to use a different database (BETADB) as its globals database. Namespace BETA also includes a global mapping that specifies that ^MyGlobal is stored in the ALPHADB database. The following shows a sketch:

Then consider the following scenario:

  1. In namespace ALPHA, process A acquires an exclusive lock with the name ^MyGlobal(15).

    The lock table contains only the entry for the lock owned by process A. This lock applies to the ALPHADB database:

  2. In namespace BETA, process B tries to acquire a lock with the name ^MyGlobal(15). The lock command does not return; the process is blocked until process A releases the lock.

Example: Namespace Uses a Mapped Global Subscript

If one or more namespaces have global mappings that use subscript level mappings, InterSystems IRIS automatically enforces the lock mechanism across all applicable namespaces. The system automatically creates additional lock table entries when locks are acquired in a non-default namespace.

For example, suppose that namespace ALPHA is configured to use the database ALPHADB as its globals database. Namespace BETA uses the BETADB database as its globals database.

Also suppose that the namespace BETA also includes a subscript-level global mapping so that ^MyGlobal(15) is stored in the ALPHADB database (while the rest of this global is stored in the namespace's default location). The following shows a sketch:

Then consider the following scenario:

  1. In namespace ALPHA, process A acquires an exclusive lock with the name ^MyGlobal(15).

    As with the previous scenario, the lock table contains only the entry for the lock owned by Process A. This lock applies to the ALPHADB database (for example, c:\InterSystems\IRIS\mgr\alphadb).

  2. In namespace BETA, process B tries to acquire a lock named ^MyGlobal(15). This lock command does not return; the process is blocked until process A releases the lock.

When a non-default namespace acquires a lock, the overall behavior remains the same; however, InterSystems IRIS handles the details slightly differently. Suppose that in namespace BETA, a process acquires a lock with the name ^MyGlobal(15). In this case, the lock table contains two entries, one for the ALPHADB database and one for the BETADB database. The process in the BETA namespace owns both locks. Releasing the name in BETA removes both entries automatically.

When this process releases the lock name ^MyGlobal(15), the system automatically removes both locks.

Example: Extended Global References

Code running in one namespace can use an extended reference to access a global that is not otherwise available in that namespace. In this case, InterSystems IRIS adds an entry to the lock table that affects the relevant database. The lock is owned by the process that created it. For example, consider the following scenario. For simplicity, there are no global mappings in this scenario.

  1. Process A is running in the ALPHA namespace, and this process uses the following command to acquire a lock on a global that is available in the BETA namespace:

    # Lock ^["beta"]MyGlobal(15) from the current namespace
    iris.lock("", 0, '^["beta"]MyGlobal', 15)
    
    
     lock ^["beta"]MyGlobal(15)
  2. Now the lock table includes the following entry:

    Note that this shows only the global name (rather than the reference used to access it). Also, in this scenario, BETADB is the default database for the BETA namespace.

  3. In namespace BETA, process B tries to acquire a lock with the name ^MyGlobal(15). This lock command does not return; the process is blocked until process A releases the lock.

A process-private global is technically an extended reference, but InterSystems IRIS does not support using a process-private global name as a lock name; you would not need such a lock anyway because, by definition, only one process can access such a global.

Avoiding Deadlocks

Incremental locking is potentially dangerous because it can lead to a situation known as deadlock. This situation occurs when two processes each assert an incremental lock on a variable already locked by the other process. Because the attempted locks are incremental, the existing locks are not released. As a result, each process hangs while waiting for the other process to release the existing lock.

As an example:

  1. Process A issues this command:

     iris.lock("", 0, "^MyGlobal", 15)
    
     lock +^MyGlobal(15)
  2. Process B issues this command:

     iris.lock("", 0, "^MyOtherGlobal", 15)
    
     lock +^MyOtherGlobal(15)
  3. Process A issues this command:

     iris.lock("", 0, "^MyOtherGlobal", 15)
    
     lock +^MyOtherGlobal(15)

    This lock command does not return; the process is blocked until process B releases this lock.

  4. Process B issues this command:

     iris.lock("", 0, "^MyGlobal", 15)
    
     lock +^MyGlobal(15)

    This lock command does not return; the process is blocked until process A releases this lock. Process A, however, is blocked and cannot release the lock. Now, these processes are waiting for each other.

Deadlock is considered an application programming error and should be prevented. There are several ways to prevent deadlocks:

  • Always include the timeout argument.

  • Follow a strict protocol for the order in which you issue incremental lock commands. Deadlocks cannot occur as long as all processes follow the same order for lock names. A simple protocol is to add locks in collating sequence order.

  • In ObjectScript, use simple locking rather than incremental locking; that is, do not use the + operator. As noted earlier, with simple locking, the LOCK command first releases all previous locks held by the process. (In practice, however, simple locking is not often used.)

If a deadlock occurs, you can resolve it by using the Management Portal or the ^LOCKTAB routine. See Monitoring Locks.

Locking in SQL and Objects

When you work with InterSystems SQL or persistent classes, you do not need to use the ObjectScript LOCK or Python iris.lock() command directly because there are alternatives suitable for your use cases. (Internally, these alternatives all use an ObjectScript LOCK command.)

See Also

FeedbackOpens in a new tab