Thread Synchronization (in PowerShell?)

Up until now, I had been under the impression that there’s no need to worry about synchronized access to objects in PowerShell, even when using runspaces. (See Jason Shirk’s comment on this stackoverflow post.) Reading today’s Hey, Scripting Guy! blog post by Boe Prox on using WPF with PowerShell, it would appear I was mistaken. He’s using the [hashtable]::Synchronize() method to obtain a thread-safe wrapper to a hashtable, and passing that object to other runspaces by way of the Runspace.SessionStateProxy.SetVariable() method.

Since this would imply that it’s possible to access objects from different threads in a PowerShell session, I thought it might be useful to have a “Lock” statement in PowerShell as well. http://msdn.microsoft.com/en-us/library/system.collections.icollection.issynchronized.aspx mentions that you must explicitly lock the SyncRoot property of an Collection to perform a thread-safe enumeration of its contents, even if you’re dealing with a Synchronized collection. We’d like to be able to do this:

$myHashTable = @{
    One   = 1
    Two   = 2
    Three = 3
}

lock ($myHashTable.SyncRoot) {
    foreach ($keyValuePair in $myHashTable.GetEnumerator())
    {
        # Do something
    }
}

Edit: I realized today that while the source code was displaying quite well on WordPress, line breaks weren’t being preserved when copying and pasting code off the page. I’ve uploaded the function to the TechNet Gallery at http://gallery.technet.microsoft.com/scriptcenter/Lock-Object-Synchronize-725ef5e7 . Also, I removed pipeline support from the function for now, after running into some problems getting it working properly.

Here’s the function:

function Lock-Object
{
    <#
    .Synopsis
       Locks an object to prevent simultaneous access from another thread.
    .DESCRIPTION
       PowerShell implementation of C#'s "lock" statement.  Code executed in the script block does not have to worry about simultaneous modification of the object by code in another thread.
    .PARAMETER InputObject
       The object which is to be locked.  This does not necessarily need to be the actual object you want to access; it's common for an object to expose a property which is used for this purpose, such as the ICollection.SyncRoot property.
    .PARAMETER ScriptBlock
       The script block that is to be executed while you have a lock on the object.
       Note:  This script block is "dot-sourced" to run in the same scope as the caller.  This allows you to assign variables inside the script block and have them be available to your script or function after the end of the lock block, if desired.
    .EXAMPLE
       $hashTable = @{}
       lock $hashTable.SyncRoot {
           $hashTable.Add("Key", "Value")
       }

       This is an example of using the "lock" alias to Lock-Object, in a manner that most closely resembles the similar C# syntax with positional parameters.
    .EXAMPLE
       $hashTable = @{}
       Lock-Object -InputObject $hashTable.SyncRoot -ScriptBlock {
           $hashTable.Add("Key", "Value")
       }

       This is the same as Example 1, but using the full PowerShell command and parameter names.
    .INPUTS
       None.  This command does not accept pipeline input.
    .OUTPUTS
       System.Object (depends on what's in the script block.)
    .NOTES
        Most of the time, PowerShell code runs in a single thread.  You have to go through several steps to create a situation in which multiple threads can try to access the same .NET object.  In the Links section of this help topic, there is a blog post by Boe Prox which demonstrates this.
    .LINK
       http://learn-powershell.net/2013/04/19/sharing-variables-and-live-objects-between-powershell-runspaces/
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, Position = 0)]
        [AllowEmptyString()]
        [AllowEmptyCollection()]
        [object]
        $InputObject,

        [Parameter(Mandatory = $true, Position = 1)]
        [scriptblock]
        $ScriptBlock
    )

    if ($InputObject.GetType().IsValueType)
    {
        $params = @{
            Message      = "Lock object cannot be a value type."
            TargetObject = $InputObject
            Category     = [System.Management.Automation.ErrorCategory]::InvalidArgument
            ErrorId      = 'CannotLockValueType'
        }

        Write-Error @params
        return
    }

    $lockTaken = $false

    try
    {
        [System.Threading.Monitor]::Enter($InputObject)
        $lockTaken = $true
        . $ScriptBlock
    }
    catch
    {
        $params = @{
            Exception    = $_.Exception
            Category     = [System.Management.Automation.ErrorCategory]::OperationStopped
            ErrorId      = 'InvokeWithLockError'
            TargetObject = New-Object psobject -Property @{
                ScriptBlock  = $ScriptBlock
                ArgumentList = $ArgumentList
                InputObject  = $InputObject
                LockProperty = $LockProperty
            }
        }

        Write-Error @params
        return
    }
    finally
    {
        if ($lockTaken)
        {
            [System.Threading.Monitor]::Exit($InputObject)
        }
    }
}

Set-Alias -Name Lock -Value Lock-Object

# Export-ModuleMember -Function Lock-Object -Alias Lock

And some demonstration code (which requires the Lock-Object function to be in a file named PSThreading.psm1 in the current working directory):

$arrayList = New-Object System.Collections.ArrayList
$arrayList.AddRange(('a','b','c','d','e'))

$sessionstate = [system.management.automation.runspaces.initialsessionstate]::CreateDefault()
$sessionstate.ImportPSModule("$pwd\PSThreading.psm1")
$sessionstate.Variables.Add(
    (New-Object System.Management.Automation.Runspaces.SessionStateVariableEntry('arrayList', $arrayList, $null))
)

$runspacepool = [runspacefactory]::CreateRunspacePool(1, 2, $sessionstate, $Host)
$runspacepool.Open()

$ps1 = [powershell]::Create()
$ps1.RunspacePool = $runspacepool

$null = $ps1.AddScript({
    Lock-Object $arrayList.SyncRoot {
        for ($i = 1; $i -le 5; $i++)
        {
            Start-Sleep -Seconds 1
            $null = $arrayList.Add($i)
        }
    }
})

$handle1 = $ps1.BeginInvoke()

$ps2 = [powershell]::Create()
$ps2.RunspacePool = $runspacepool

$null = $ps2.AddScript({
    Lock-Object $arrayList.SyncRoot {
        foreach ($i in $arrayList)
        {
            $i
        }
    }
})

$handle2 = $ps2.BeginInvoke()

if ([System.Threading.WaitHandle]::WaitAll($handle1.AsyncWaitHandle) -and
    [System.Threading.WaitHandle]::WaitAll($handle2.AsyncWaitHandle))
{
    $ps1.EndInvoke($handle1)
    $ps2.EndInvoke($handle2)
}
Advertisements

About Dave Wyatt

Microsoft MVP (PowerShell), IT professional.
This entry was posted in PowerShell. Bookmark the permalink.

One Response to Thread Synchronization (in PowerShell?)

  1. Pingback: Thread Synchronization (lock statement) in PowerShell | PowerShell.org

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s