en-US/PSParallelPipeline-help.xml
|
<?xml version="1.0" encoding="utf-8"?>
<helpItems schema="maml" xmlns="http://msh"> <command:command xmlns:maml="http://schemas.microsoft.com/maml/2004/10" xmlns:command="http://schemas.microsoft.com/maml/dev/command/2004/10" xmlns:dev="http://schemas.microsoft.com/maml/dev/2004/10" xmlns:MSHelp="http://msdn.microsoft.com/mshelp"> <command:details> <command:name>Invoke-Parallel</command:name> <command:verb>Invoke</command:verb> <command:noun>Parallel</command:noun> <maml:description> <maml:para>Executes parallel processing of pipeline input objects using multithreading.</maml:para> </maml:description> </command:details> <maml:description> <maml:para>The `Invoke-Parallel` cmdlet enables parallel processing of input objects in PowerShell, including Windows PowerShell 5.1 and PowerShell 7+, offering functionality similar to `ForEach-Object -Parallel` introduced in PowerShell 7.0. It processes pipeline input across multiple threads, improving performance for tasks that benefit from parallel execution.</maml:para> </maml:description> <command:syntax> <command:syntaxItem> <maml:name>Invoke-Parallel</maml:name> <command:parameter required="true" variableLength="true" globbing="false" pipelineInput="False" position="0" aliases="none"> <maml:name>ScriptBlock</maml:name> <maml:description> <maml:para>Defines the script block executed for each input object in parallel.</maml:para> </maml:description> <command:parameterValue required="true" variableLength="false">ScriptBlock</command:parameterValue> <dev:type> <maml:name>ScriptBlock</maml:name> <maml:uri /> </dev:type> <dev:defaultValue>None</dev:defaultValue> </command:parameter> <command:parameter required="false" variableLength="true" globbing="true" pipelineInput="False" position="named" aliases="funcs"> <maml:name>Functions</maml:name> <maml:description> <maml:para>Specifies an array of function names or wildcard patterns from the local session to include in the runspaces’ Initial Session State (https://learn.microsoft.com/en-us/dotnet/api/system.management.automation.runspaces.initialsessionstate). This enables their use within the parallel script block.</maml:para> <maml:para>> [!NOTE] > > If a specified name or wildcard pattern fails to match any function in the caller's scope, a terminating error is thrown.</maml:para> <maml:para>> [!TIP] > > This parameter is the recommended way to make local functions available in the parallel scope. Alternatively, you can retrieve the function definition as a string (e.g., `$def = ${function:Greet}.ToString()`) and use `$using:` to pass it into the script block, defining it there (e.g., `${function:Greet} = $using:def`).</maml:para> </maml:description> <command:parameterValue required="true" variableLength="false">String[]</command:parameterValue> <dev:type> <maml:name>String[]</maml:name> <maml:uri /> </dev:type> <dev:defaultValue>None</dev:defaultValue> </command:parameter> <command:parameter required="true" variableLength="true" globbing="false" pipelineInput="True (ByValue)" position="named" aliases="none"> <maml:name>InputObject</maml:name> <maml:description> <maml:para>Specifies the objects to process in the script block. This parameter accepts pipeline input.</maml:para> </maml:description> <command:parameterValue required="true" variableLength="false">Object</command:parameterValue> <dev:type> <maml:name>Object</maml:name> <maml:uri /> </dev:type> <dev:defaultValue>None</dev:defaultValue> </command:parameter> <command:parameter required="false" variableLength="true" globbing="false" pipelineInput="False" position="named" aliases="tl"> <maml:name>ThrottleLimit</maml:name> <maml:description> <maml:para>Sets the maximum number of script blocks executed in parallel across multiple threads. Additional input objects wait until the number of running script blocks falls below this limit. The default value is `5`.</maml:para> </maml:description> <command:parameterValue required="true" variableLength="false">Int32</command:parameterValue> <dev:type> <maml:name>Int32</maml:name> <maml:uri /> </dev:type> <dev:defaultValue>5</dev:defaultValue> </command:parameter> <command:parameter required="false" variableLength="true" globbing="false" pipelineInput="False" position="named" aliases="to"> <maml:name>TimeoutSeconds</maml:name> <maml:description> <maml:para>Specifies the maximum time (in seconds) to process all input objects. When the timeout is reached, running script blocks are terminated, and remaining input is discarded.</maml:para> <maml:para>> [!NOTE] > > A value of `0` (default) disables the timeout, allowing processing to continue until completion.</maml:para> </maml:description> <command:parameterValue required="true" variableLength="false">Int32</command:parameterValue> <dev:type> <maml:name>Int32</maml:name> <maml:uri /> </dev:type> <dev:defaultValue>0</dev:defaultValue> </command:parameter> <command:parameter required="false" variableLength="true" globbing="false" pipelineInput="False" position="named" aliases="unr"> <maml:name>UseNewRunspace</maml:name> <maml:description> <maml:para>Uses a new runspace for each parallel invocation instead of reusing existing runspaces in the runspace pool.</maml:para> </maml:description> <dev:type> <maml:name>SwitchParameter</maml:name> <maml:uri /> </dev:type> <dev:defaultValue>False</dev:defaultValue> </command:parameter> <command:parameter required="false" variableLength="true" globbing="true" pipelineInput="False" position="named" aliases="vars"> <maml:name>Variables</maml:name> <maml:description> <maml:para>Provides dictionaries (`IDictionary`), variable names, or wildcard patterns to make matching caller variables available in the parallel scope.</maml:para> <maml:para>You can supply arguments in multiple forms:</maml:para> <maml:para>- Dictionaries : Pass one or more dictionaries (e.g., hashtables). Keys specify variable names in the parallel scope. Duplicate keys across multiple dictionaries are ignored. - Variable Names or Wildcard Patterns : Pass strings containing variable names or wildcard expressions (e.g., ` `, `foo `, `[ab]*`). Non-built-in variables in the caller scope matching the pattern will be imported automatically.</maml:para> <maml:para>> [!NOTE] > > - When resolving variables by string/wildcard pattern, variables holding a `ScriptBlock` value are silently skipped. > - If a string or wildcard pattern does not match any variable in the caller's scope, a terminating error is thrown.</maml:para> <maml:para>> [!TIP] > > Use this parameter as an alternative to the `$using:` scope modifier (https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_scopes#scope-modifiers).</maml:para> </maml:description> <command:parameterValue required="true" variableLength="false">Hashtable</command:parameterValue> <dev:type> <maml:name>Hashtable</maml:name> <maml:uri /> </dev:type> <dev:defaultValue>None</dev:defaultValue> </command:parameter> <command:parameter required="false" variableLength="true" globbing="false" pipelineInput="False" position="named" aliases="mn"> <maml:name>ModuleNames</maml:name> <maml:description> <maml:para>Specifies an array of module names to import into the runspaces’ Initial Session State (https://learn.microsoft.com/en-us/dotnet/api/system.management.automation.runspaces.initialsessionstate). This allows the script block to use cmdlets and functions from the specified modules.</maml:para> <maml:para>> [!TIP] > > Use this parameter to ensure required modules are available in the parallel scope. Module names must be discoverable via the `$env:PSModulePath` (https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_psmodulepath)environment variable, which lists installed module locations.</maml:para> </maml:description> <command:parameterValue required="true" variableLength="false">String[]</command:parameterValue> <dev:type> <maml:name>String[]</maml:name> <maml:uri /> </dev:type> <dev:defaultValue>None</dev:defaultValue> </command:parameter> <command:parameter required="false" variableLength="true" globbing="false" pipelineInput="False" position="named" aliases="mp"> <maml:name>ModulePaths</maml:name> <maml:description> <maml:para>Specifies an array of file paths to directories containing PowerShell modules (e.g., `.psm1` or `.psd1` files) to import into the runspaces’ Initial Session State (https://learn.microsoft.com/en-us/dotnet/api/system.management.automation.runspaces.initialsessionstate). This enables the script block to use cmdlets and functions from custom or local modules.</maml:para> <maml:para>> [!NOTE] > > Paths must be absolute or relative to the current working directory and must point to valid directories containing PowerShell modules. If an invalid path (e.g., a file or non-existent directory) is provided, a terminating error is thrown.</maml:para> </maml:description> <command:parameterValue required="true" variableLength="false">String[]</command:parameterValue> <dev:type> <maml:name>String[]</maml:name> <maml:uri /> </dev:type> <dev:defaultValue>None</dev:defaultValue> </command:parameter> </command:syntaxItem> </command:syntax> <command:parameters> <command:parameter required="false" variableLength="true" globbing="true" pipelineInput="False" position="named" aliases="funcs"> <maml:name>Functions</maml:name> <maml:description> <maml:para>Specifies an array of function names or wildcard patterns from the local session to include in the runspaces’ Initial Session State (https://learn.microsoft.com/en-us/dotnet/api/system.management.automation.runspaces.initialsessionstate). This enables their use within the parallel script block.</maml:para> <maml:para>> [!NOTE] > > If a specified name or wildcard pattern fails to match any function in the caller's scope, a terminating error is thrown.</maml:para> <maml:para>> [!TIP] > > This parameter is the recommended way to make local functions available in the parallel scope. Alternatively, you can retrieve the function definition as a string (e.g., `$def = ${function:Greet}.ToString()`) and use `$using:` to pass it into the script block, defining it there (e.g., `${function:Greet} = $using:def`).</maml:para> </maml:description> <command:parameterValue required="true" variableLength="false">String[]</command:parameterValue> <dev:type> <maml:name>String[]</maml:name> <maml:uri /> </dev:type> <dev:defaultValue>None</dev:defaultValue> </command:parameter> <command:parameter required="true" variableLength="true" globbing="false" pipelineInput="True (ByValue)" position="named" aliases="none"> <maml:name>InputObject</maml:name> <maml:description> <maml:para>Specifies the objects to process in the script block. This parameter accepts pipeline input.</maml:para> </maml:description> <command:parameterValue required="true" variableLength="false">Object</command:parameterValue> <dev:type> <maml:name>Object</maml:name> <maml:uri /> </dev:type> <dev:defaultValue>None</dev:defaultValue> </command:parameter> <command:parameter required="true" variableLength="true" globbing="false" pipelineInput="False" position="0" aliases="none"> <maml:name>ScriptBlock</maml:name> <maml:description> <maml:para>Defines the script block executed for each input object in parallel.</maml:para> </maml:description> <command:parameterValue required="true" variableLength="false">ScriptBlock</command:parameterValue> <dev:type> <maml:name>ScriptBlock</maml:name> <maml:uri /> </dev:type> <dev:defaultValue>None</dev:defaultValue> </command:parameter> <command:parameter required="false" variableLength="true" globbing="false" pipelineInput="False" position="named" aliases="tl"> <maml:name>ThrottleLimit</maml:name> <maml:description> <maml:para>Sets the maximum number of script blocks executed in parallel across multiple threads. Additional input objects wait until the number of running script blocks falls below this limit. The default value is `5`.</maml:para> </maml:description> <command:parameterValue required="true" variableLength="false">Int32</command:parameterValue> <dev:type> <maml:name>Int32</maml:name> <maml:uri /> </dev:type> <dev:defaultValue>5</dev:defaultValue> </command:parameter> <command:parameter required="false" variableLength="true" globbing="false" pipelineInput="False" position="named" aliases="to"> <maml:name>TimeoutSeconds</maml:name> <maml:description> <maml:para>Specifies the maximum time (in seconds) to process all input objects. When the timeout is reached, running script blocks are terminated, and remaining input is discarded.</maml:para> <maml:para>> [!NOTE] > > A value of `0` (default) disables the timeout, allowing processing to continue until completion.</maml:para> </maml:description> <command:parameterValue required="true" variableLength="false">Int32</command:parameterValue> <dev:type> <maml:name>Int32</maml:name> <maml:uri /> </dev:type> <dev:defaultValue>0</dev:defaultValue> </command:parameter> <command:parameter required="false" variableLength="true" globbing="false" pipelineInput="False" position="named" aliases="unr"> <maml:name>UseNewRunspace</maml:name> <maml:description> <maml:para>Uses a new runspace for each parallel invocation instead of reusing existing runspaces in the runspace pool.</maml:para> </maml:description> <command:parameterValue required="false" variableLength="false">SwitchParameter</command:parameterValue> <dev:type> <maml:name>SwitchParameter</maml:name> <maml:uri /> </dev:type> <dev:defaultValue>False</dev:defaultValue> </command:parameter> <command:parameter required="false" variableLength="true" globbing="true" pipelineInput="False" position="named" aliases="vars"> <maml:name>Variables</maml:name> <maml:description> <maml:para>Provides dictionaries (`IDictionary`), variable names, or wildcard patterns to make matching caller variables available in the parallel scope.</maml:para> <maml:para>You can supply arguments in multiple forms:</maml:para> <maml:para>- Dictionaries : Pass one or more dictionaries (e.g., hashtables). Keys specify variable names in the parallel scope. Duplicate keys across multiple dictionaries are ignored. - Variable Names or Wildcard Patterns : Pass strings containing variable names or wildcard expressions (e.g., ` `, `foo `, `[ab]*`). Non-built-in variables in the caller scope matching the pattern will be imported automatically.</maml:para> <maml:para>> [!NOTE] > > - When resolving variables by string/wildcard pattern, variables holding a `ScriptBlock` value are silently skipped. > - If a string or wildcard pattern does not match any variable in the caller's scope, a terminating error is thrown.</maml:para> <maml:para>> [!TIP] > > Use this parameter as an alternative to the `$using:` scope modifier (https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_scopes#scope-modifiers).</maml:para> </maml:description> <command:parameterValue required="true" variableLength="false">Hashtable</command:parameterValue> <dev:type> <maml:name>Hashtable</maml:name> <maml:uri /> </dev:type> <dev:defaultValue>None</dev:defaultValue> </command:parameter> <command:parameter required="false" variableLength="true" globbing="false" pipelineInput="False" position="named" aliases="mn"> <maml:name>ModuleNames</maml:name> <maml:description> <maml:para>Specifies an array of module names to import into the runspaces’ Initial Session State (https://learn.microsoft.com/en-us/dotnet/api/system.management.automation.runspaces.initialsessionstate). This allows the script block to use cmdlets and functions from the specified modules.</maml:para> <maml:para>> [!TIP] > > Use this parameter to ensure required modules are available in the parallel scope. Module names must be discoverable via the `$env:PSModulePath` (https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_psmodulepath)environment variable, which lists installed module locations.</maml:para> </maml:description> <command:parameterValue required="true" variableLength="false">String[]</command:parameterValue> <dev:type> <maml:name>String[]</maml:name> <maml:uri /> </dev:type> <dev:defaultValue>None</dev:defaultValue> </command:parameter> <command:parameter required="false" variableLength="true" globbing="false" pipelineInput="False" position="named" aliases="mp"> <maml:name>ModulePaths</maml:name> <maml:description> <maml:para>Specifies an array of file paths to directories containing PowerShell modules (e.g., `.psm1` or `.psd1` files) to import into the runspaces’ Initial Session State (https://learn.microsoft.com/en-us/dotnet/api/system.management.automation.runspaces.initialsessionstate). This enables the script block to use cmdlets and functions from custom or local modules.</maml:para> <maml:para>> [!NOTE] > > Paths must be absolute or relative to the current working directory and must point to valid directories containing PowerShell modules. If an invalid path (e.g., a file or non-existent directory) is provided, a terminating error is thrown.</maml:para> </maml:description> <command:parameterValue required="true" variableLength="false">String[]</command:parameterValue> <dev:type> <maml:name>String[]</maml:name> <maml:uri /> </dev:type> <dev:defaultValue>None</dev:defaultValue> </command:parameter> </command:parameters> <command:inputTypes> <command:inputType> <dev:type> <maml:name>System.Object</maml:name> </dev:type> <maml:description> <maml:para>You can pipe any object to this cmdlet.</maml:para> </maml:description> </command:inputType> </command:inputTypes> <command:returnValues> <command:returnValue> <dev:type> <maml:name>System.Object</maml:name> </dev:type> <maml:description> <maml:para>Returns objects produced by the script block.</maml:para> </maml:description> </command:returnValue> </command:returnValues> <maml:alertSet> <maml:alert> <maml:para>- `Invoke-Parallel` uses multithreading, which may introduce overhead. For small datasets, sequential processing might be faster.</maml:para> <maml:para>- Ensure variables or collections passed to the parallel scope are thread-safe (e.g., use `ConcurrentDictionary<TKey, TValue>` (https://learn.microsoft.com/en-us/dotnet/api/system.collections.concurrent.concurrentdictionary-2) or similar), as shown in __Example #3__ (#example-3-adding-to-a-thread-safe-collection-with-using)and __Example #4__ (#example-4-adding-to-a-thread-safe-collection-with--variables), to avoid race conditions. - By default, runspaces are reused from a pool to optimize resource usage. Using `-UseNewRunspace` (#-usenewrunspace)increases memory and startup time but ensures isolation.</maml:para> </maml:alert> </maml:alertSet> <command:examples> <command:example> <maml:title>------- Example 1: Run a slow script in parallel batches -------</maml:title> <dev:code>$message = 'Hello world from ' 0..10 | Invoke-Parallel { $using:message + [runspace]::DefaultRunspace.InstanceId Start-Sleep 3 }</dev:code> <dev:remarks> <maml:para>This example demonstrates parallel execution of a script block with a 3-second delay, appending a unique runspace ID to a message. The `$using:` scope modifier (https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_scopes#the-using-scope-modifier)is used to pass the local variable `$message` into the parallel scope, a supported method for accessing external variables in `Invoke-Parallel`.</maml:para> </dev:remarks> </command:example> <command:example> <maml:title>-------- Example 2: Demonstrates `-Variables` Parameter --------</maml:title> <dev:code>$message = 'Hello world from ' 0..10 | Invoke-Parallel { $message + [runspace]::DefaultRunspace.InstanceId Start-Sleep 3 } -Variables @{ message = $message }</dev:code> <dev:remarks> <maml:para>This example demonstrates the `-Variables` parameter (#-variables), which passes the local variable `$message` into the parallel scope using a hashtable. The key `message` in the hashtable defines the variable name available within the script block, serving as an alternative to the `$using:` scope modifier.</maml:para> </dev:remarks> </command:example> <command:example> <maml:title>--------- Example 3: Import variables using wildcards ---------</maml:title> <dev:code>$appUser = 'John Doe' $appRole = 'Admin' 0..5 | Invoke-Parallel { "Processing $_ for $appUser ($appRole)" } -Variables app*</dev:code> <dev:remarks> <maml:para>This example uses wildcards with `-Variables` to dynamically import all caller variables starting with `app` (in this case `$appUser` and `$appRole`) into the parallel scope.</maml:para> </dev:remarks> </command:example> <command:example> <maml:title>--- Example 4: Passing multiple dictionaries to `-Variables` ---</maml:title> <dev:code>$dict1 = @{ Server = 'localhost' } $dict2 = @{ Port = 8080 } 0..5 | Invoke-Parallel { "Connecting to $Server:$Port" } -Variables $dict1, $dict2</dev:code> <dev:remarks> <maml:para>This example demonstrates passing an array of dictionaries to `-Variables`. Key/value pairs from both dictionaries are merged into the parallel scope.</maml:para> </dev:remarks> </command:example> <command:example> <maml:title>- Example 5: Adding to a thread-safe collection with `$using:` -</maml:title> <dev:code>$dict = [System.Collections.Concurrent.ConcurrentDictionary[int, object]]::new() Get-Process | Invoke-Parallel { ($using:dict)[$_.Id] = $_ } $dict[$PID]</dev:code> <dev:remarks> <maml:para>This example uses a thread-safe dictionary to store process objects by ID, leveraging the `$using:` modifier for variable access.</maml:para> </dev:remarks> </command:example> <command:example> <maml:title>Example 6: Adding to a thread-safe collection with `-Variables`</maml:title> <dev:code>$dict = [System.Collections.Concurrent.ConcurrentDictionary[int, object]]::new() Get-Process | Invoke-Parallel { $dict[$_.Id] = $_ } -Variables @{ dict = $dict } $dict[$PID]</dev:code> <dev:remarks> <maml:para>Similar to Example 3, this demonstrates the same functionality using `-Variables` instead of `$using:`.</maml:para> </dev:remarks> </command:example> <command:example> <maml:title>-- Example 7: Using the `-Functions` parameter with wildcards --</maml:title> <dev:code>function Get-Greeting { param($s) "Hello $s" } function Get-Farewell { param($s) "Goodbye $s" } 0..5 | Invoke-Parallel { Get-Greeting $_; Get-Farewell $_ } -Functions Get-*</dev:code> <dev:remarks> <maml:para>This example imports all functions matching the wildcard pattern `Get-*` from the local session into the parallel scope using the `-Functions` parameter (#-functions).</maml:para> </dev:remarks> </command:example> <command:example> <maml:title>----- Example 8: Setting a timeout with `-TimeoutSeconds` -----</maml:title> <dev:code>0..10 | Invoke-Parallel { Start-Sleep 1 } -TimeoutSeconds 3</dev:code> <dev:remarks> <maml:para>This example limits execution to 3 seconds, stopping all running script blocks and ignoring unprocessed input once the timeout is reached.</maml:para> </dev:remarks> </command:example> <command:example> <maml:title>--- Example 9: Creating new runspaces with `-UseNewRunspace` ---</maml:title> <dev:code>0..3 | Invoke-Parallel { [runspace]::DefaultRunspace.InstanceId } -ThrottleLimit 2 # Guid # ---- # c945ae1f-4e66-4312-b23c-f3994965308e # 1c6af45c-8727-4488-937a-4dfc1d259e9e # c945ae1f-4e66-4312-b23c-f3994965308e # 1c6af45c-8727-4488-937a-4dfc1d259e9e 0..3 | Invoke-Parallel { [runspace]::DefaultRunspace.InstanceId } -ThrottleLimit 2 -UseNewRunspace # Guid # ---- # 7a1c3871-6ce2-4b7f-ae90-fb1e92cd9678 # 2488be9e-15fe-4be2-882d-7d98b068c913 # d3dd7b5d-e7e3-457f-b6fb-def35fe837d7 # 9af7c222-061d-4c89-b073-375ee925e538</dev:code> <dev:remarks> <maml:para>This example contrasts default runspace reuse with the `-UseNewRunspace` switch, showing unique runspace IDs for each invocation in the latter case.</maml:para> </dev:remarks> </command:example> <command:example> <maml:title>-------- Example 10: Using the `-ModuleNames` parameter --------</maml:title> <dev:code>Import-Csv users.csv | Invoke-Parallel { Get-ADUser $_.UserPrincipalName } -ModuleNames ActiveDirectory</dev:code> <dev:remarks> <maml:para>This example imports the `ActiveDirectory` module into the parallel scope using `-ModuleNames`, enabling the `Get-ADUser` cmdlet within the script block.</maml:para> </dev:remarks> </command:example> <command:example> <maml:title>-------- Example 11: Using the `-ModulePaths` parameter --------</maml:title> <dev:code>$moduleDir = Join-Path $PSScriptRoot "CustomModule" 0..10 | Invoke-Parallel { Get-CustomCmdlet } -ModulePaths $moduleDir</dev:code> <dev:remarks> <maml:para>This example imports a custom module from the specified directory using `-ModulePaths`, allowing the `Get-CustomCmdlet` function to be used in the parallel script block.</maml:para> <maml:para>> [!NOTE] > > The path must point to a directory containing a valid PowerShell module.</maml:para> </dev:remarks> </command:example> </command:examples> <command:relatedLinks> <maml:navigationLink> <maml:linkText>Online Version:</maml:linkText> <maml:uri>https://github.com/santisq/PSParallelPipeline</maml:uri> </maml:navigationLink> <maml:navigationLink> <maml:linkText>__ForEach-Object -Parallel__</maml:linkText> <maml:uri>https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/foreach-object</maml:uri> </maml:navigationLink> <maml:navigationLink> <maml:linkText>__Runspaces Overview__</maml:linkText> <maml:uri>https://learn.microsoft.com/en-us/dotnet/api/system.management.automation.runspaces.runspace</maml:uri> </maml:navigationLink> <maml:navigationLink> <maml:linkText>__Managed threading best practices__</maml:linkText> <maml:uri>https://learn.microsoft.com/en-us/dotnet/standard/threading/managed-threading-best-practices</maml:uri> </maml:navigationLink> <maml:navigationLink> <maml:linkText>__Thread-safe collections__</maml:linkText> <maml:uri>https://learn.microsoft.com/en-us/dotnet/standard/collections/thread-safe/</maml:uri> </maml:navigationLink> </command:relatedLinks> </command:command> </helpItems> |