CMutex: A cooperative, cancellable, cross-process mutex for VB6
TL;DR — A small VB6 class that wraps the Windows mutex APIs correctly, adds a responsive (message-pumped) wait option with cooperative cancel, supports “short-circuit” names to bail early, distinguishes expected vs unexpected acquisition failures, and exposes a bitmask error policy so you control what raises and what doesn’t. It’s designed as a single-use object: create ? acquire once ? inspect ? dispose.
Use cases: one-writer/many-readers coordination between processes, preventing concurrent updaters, keeping UI responsive while waiting, or aborting a wait when some other system signal is present.
Features at a glance
Quick start
API surface (selected)
Acquire methods
Enums
State & events
Short-circuits (CMutexSC)
Give CMutex a list of mutex names. If any exists (even if you don’t own it), acquisition is stopped quickly and state is mxs_Shorted. You can inspect which names were found.
Cancellation model
In mxwb_Cancellable mode, the wait loop uses MsgWaitForMultipleObjects with small slices (~50ms) and raises Acquiring(ByRef Cancel). Set Cancel = True to abort; state becomes mxs_Cancelled. UI stays responsive.
Error policy (bitmask) examples
Design notes
Gotchas / best practices
Lightly tested so far, feedback and bug reports are very welcome!
Changelog
TL;DR — A small VB6 class that wraps the Windows mutex APIs correctly, adds a responsive (message-pumped) wait option with cooperative cancel, supports “short-circuit” names to bail early, distinguishes expected vs unexpected acquisition failures, and exposes a bitmask error policy so you control what raises and what doesn’t. It’s designed as a single-use object: create ? acquire once ? inspect ? dispose.
Quote:
Use cases: one-writer/many-readers coordination between processes, preventing concurrent updaters, keeping UI responsive while waiting, or aborting a wait when some other system signal is present.
Features at a glance
- Global\ / Local\ namespaces (machine-wide or session-local).
- Blocking or Cancellable wait (message-pumped with DoEvents) — great for keeping UI alive so user can cancel.
- Short-circuit list: if any named mutex exists, acquisition stops. For example, you could detect a mutex that your installer creates and abort waiting for another long running process and shutdown.
- Abandoned detection: surfaces WAIT_ABANDONED_0 so you can react accordingly.
- Bitmask error policy: choose what raises: Usage, AcquireExpected, AcquireUnexpected, None.
- Single-shot lifecycle (no re-use quirks) and automatic cleanup on terminate.
- Optional SDDL so cross-user coordination works, or use the built-in default.
Quick start
Code:
Dim mx As New CMutex
' Acquire in the Global\ namespace, Infinite timeout
mx.AcquireGlobal "MyMutex"
If mx.IsAcquired Then
' ... critical section ...
ElseIf mx.IsTimedOut Then
' handle timeout
ElseIf mx.IsCancelled Then
' user cancelled via event
ElseIf mx.IsShortCircuited Then
' another named mutex was present, see mx.ShortCircuits
ElseIf mx.IsFailed Then
Debug.Print "Unexpected failure: "; mx.LastErrorNumber; mx.LastErrorDescription
End If
Set mx = Nothing ' Mutex releasedAPI surface (selected)
Acquire methods
Code:
' Core
Function Acquire( ByVal p_MutexName As String, _
Optional ByVal p_TimeoutMs As Long = INFINITE, _
Optional ByVal p_WaitBehaviour As e_MutexWaitBehavior = mxwb_Blocking, _
Optional ByVal p_ErrorPolicy As e_MutexErrorPolicy = mxep_Usage, _
Optional ByVal p_ShortCircuitMutexNameOrArray As Variant) As CMutex
' Convenience wrappers
Function AcquireGlobal( ByVal p_MutexName As String, _
Optional ByVal p_TimeoutMs As Long = INFINITE, _
Optional ByVal p_WaitBehaviour As e_MutexWaitBehavior = mxwb_Blocking, _
Optional ByVal p_ErrorPolicy As e_MutexErrorPolicy = mxep_Usage, _
Optional ByVal p_ShortCircuitMutexNameOrArray As Variant) As CMutex
Function AcquireLocal( ByVal p_MutexName As String, _
Optional ByVal p_TimeoutMs As Long = INFINITE, _
Optional ByVal p_WaitBehaviour As e_MutexWaitBehavior = mxwb_Blocking, _
Optional ByVal p_ErrorPolicy As e_MutexErrorPolicy = mxep_Usage, _
Optional ByVal p_ShortCircuitMutexNameOrArray As Variant) As CMutexCode:
Enum e_MutexWaitBehavior
mxwb_Blocking = 0 ' WaitForSingleObject only; no cancel
mxwb_Cancellable = -1 ' Message-pumped wait; cooperative cancel
End Enum
Enum e_MutexErrorPolicy ' bitmask
mxep_None = 0
mxep_Usage = 1 ' bad arguments / illegal sequence
mxep_AcquireUnexpected = 2 ' mxs_Failed (system/logic error)
mxep_AcquireExpected = 4 ' mxs_TimedOut / mxs_Cancelled / mxs_Shorted
mxep_AcquireAll = mxep_AcquireUnexpected Or mxep_AcquireExpected
mxep_All = mxep_Usage Or mxep_AcquireAll
End EnumCode:
' States you can query after Acquire()
Enum e_MutexState
mxs_Disposed = &H80000000
mxs_Failed = -4
mxs_Shorted = -3
mxs_TimedOut = -2
mxs_Cancelled = -1
mxs_Initializing = 0
mxs_Acquiring = 1
mxs_Acquired = 2
End Enum
' Events
Event Acquiring(ByRef p_Cancel As Boolean) ' fire periodically in cancellable mode
Event Acquired(ByVal p_IsAbandonedByPreviousOwner As Boolean)
Event Failed(ByVal p_IsExpectedFailure As Boolean)Short-circuits (CMutexSC)
Give CMutex a list of mutex names. If any exists (even if you don’t own it), acquisition is stopped quickly and state is mxs_Shorted. You can inspect which names were found.
Code:
' Provide one or many names (String or String())
mx.Acquire "JobQueueWriter", , mxwb_Cancellable, mxep_AcquireExpected, _
Array("BackupJobRunning", "ReindexJobRunning")
If mx.IsShortCircuited Then
Dim i As Long
For i = 0 To mx.ShortCircuits.FoundCount - 1
Debug.Print "Found: "; mx.ShortCircuits.FoundByIndex(i)
Next
End IfCancellation model
In mxwb_Cancellable mode, the wait loop uses MsgWaitForMultipleObjects with small slices (~50ms) and raises Acquiring(ByRef Cancel). Set Cancel = True to abort; state becomes mxs_Cancelled. UI stays responsive.
Code:
Private Sub mx_Acquiring(ByRef Cancel As Boolean)
DoEvents ' let UI breathe
Cancel = UserRequestedStop() ' your own flag/button
End SubError policy (bitmask) examples
Code:
mx.AcquireLocal "PrintQueue", 0, mxwb_Blocking, mxep_Usage
' Only usage errors raise. Timeout (0ms) returns IsTimedOut = True.
mx.AcquireGlobal "DB_Write", 10000, mxwb_Cancellable, mxep_AcquireUnexpected
' Only unexpected run-time failures raise. Timeouts/cancel/shorted do not.
mx.ErrorPolicy = mxep_All
' Everywhere: raise usage + all acquisition outcomes.Design notes
- Single-use class: call Acquire once per instance. After it completes (success or any failure), consider the instance “spent”.
- Abandoned ownership: if acquired with WAIT_ABANDONED_0, Acquired(True) is raised so callers can repair state.
- High-precision timing: QueryPerformanceCounter is cached on first use; fallback logic is included if QPC isn’t available.
- Security descriptor: SDDL grants FA to owner/admins and SYNCHRONIZE to authenticated users, enabling cross-user coordination without unsafe DACLs.
- Namespaces are case-sensitive in the Windows object manager. The class emits "Global" / "Local" with proper casing.
Gotchas / best practices
- Don’t reuse an instance for multiple acquires—by design.
- If you expose a “Cancel” button, wire it to set a flag the Acquiring handler reads.
- Prefer AcquireGlobal when coordinating across interactive services/sessions.
- Avoid passing a backslash in p_MutexName; the class prepends the namespace itself.
Lightly tested so far, feedback and bug reports are very welcome!
Changelog
- v1.0 — Initial public release. Single-use lifecycle, cancellable waits, short-circuits, abandoned detection, bitmask error policy, Global/Local helpers, SDDL support.