Skip to content

Commit f2403ed

Browse files
PaulHiginTravisEz13
authored andcommitted
Fix for ForEach-Object -Parallel performance problem with many runspaces (PowerShell#10455)
1 parent 0fd0ff0 commit f2403ed

File tree

3 files changed

+146
-81
lines changed

3 files changed

+146
-81
lines changed

src/System.Management.Automation/engine/InternalCommands.cs

Lines changed: 122 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ public sealed class ForEachObjectCommand : PSCmdlet, IDisposable
7070

7171
#endregion
7272

73+
#region Common Parameters
74+
7375
/// <summary>
7476
/// This parameter specifies the current pipeline object.
7577
/// </summary>
@@ -85,6 +87,8 @@ public PSObject InputObject
8587

8688
private PSObject _inputObject = AutomationNull.Value;
8789

90+
#endregion
91+
8892
#region ScriptBlockSet
8993

9094
private List<ScriptBlock> _scripts = new List<ScriptBlock>();
@@ -355,6 +359,7 @@ public void Dispose()
355359
_taskTimer?.Dispose();
356360
_taskDataStreamWriter?.Dispose();
357361
_taskPool?.Dispose();
362+
_taskCollection?.Dispose();
358363
}
359364

360365
#endregion
@@ -368,6 +373,8 @@ public void Dispose()
368373
private Dictionary<string, object> _usingValuesMap;
369374
private Timer _taskTimer;
370375
private PSTaskJob _taskJob;
376+
private PSDataCollection<System.Management.Automation.PSTasks.PSTask> _taskCollection;
377+
private Exception _taskCollectionException;
371378

372379
private void InitParallelParameterSet()
373380
{
@@ -411,6 +418,7 @@ private void InitParallelParameterSet()
411418

412419
if (AsJob)
413420
{
421+
// Set up for returning a job object.
414422
if (MyInvocation.BoundParameters.ContainsKey(nameof(TimeoutSeconds)))
415423
{
416424
ThrowTerminatingError(
@@ -424,24 +432,78 @@ private void InitParallelParameterSet()
424432
_taskJob = new PSTaskJob(
425433
Parallel.ToString(),
426434
ThrottleLimit);
435+
436+
return;
427437
}
428-
else
438+
439+
// Set up for synchronous processing and data streaming.
440+
_taskCollection = new PSDataCollection<System.Management.Automation.PSTasks.PSTask>();
441+
_taskDataStreamWriter = new PSTaskDataStreamWriter(this);
442+
_taskPool = new PSTaskPool(ThrottleLimit);
443+
_taskPool.PoolComplete += (sender, args) =>
429444
{
430-
_taskDataStreamWriter = new PSTaskDataStreamWriter(this);
431-
_taskPool = new PSTaskPool(ThrottleLimit);
432-
_taskPool.PoolComplete += (sender, args) =>
433-
{
434-
_taskDataStreamWriter.Close();
435-
};
436-
if (TimeoutSeconds != 0)
437-
{
438-
_taskTimer = new Timer(
439-
(_) => _taskPool.StopAll(),
440-
null,
441-
TimeoutSeconds * 1000,
442-
Timeout.Infinite);
443-
}
445+
_taskDataStreamWriter.Close();
446+
};
447+
448+
// Create timeout timer if requested.
449+
if (TimeoutSeconds != 0)
450+
{
451+
_taskTimer = new Timer(
452+
callback: (_) => { _taskCollection.Complete(); _taskPool.StopAll(); },
453+
state: null,
454+
dueTime: TimeoutSeconds * 1000,
455+
period: Timeout.Infinite);
444456
}
457+
458+
// Task collection handler.
459+
System.Threading.ThreadPool.QueueUserWorkItem(
460+
(_) =>
461+
{
462+
// As piped input are converted to PSTasks and added to the _taskCollection,
463+
// transfer the task to the _taskPool on this dedicated thread.
464+
// The _taskPool will block this thread when it is full, and allow more tasks to
465+
// be added only when a currently running task completes and makes space in the pool.
466+
// Continue adding any tasks appearing in _taskCollection until the collection is closed.
467+
while (true)
468+
{
469+
// This handle will unblock the thread when a new task is available or the _taskCollection
470+
// is closed.
471+
_taskCollection.WaitHandle.WaitOne();
472+
473+
// Task collection open state is volatile.
474+
// Record current task collection open state here, to be checked after processing.
475+
bool isOpen = _taskCollection.IsOpen;
476+
477+
try
478+
{
479+
// Read all tasks in the collection.
480+
foreach (var task in _taskCollection.ReadAll())
481+
{
482+
// This _taskPool method will block if the pool is full and will unblock
483+
// only after a task completes making more space.
484+
_taskPool.Add(task);
485+
}
486+
}
487+
catch (Exception ex)
488+
{
489+
// Close the _taskCollection on an unexpected exception so the pool closes and
490+
// lets any running tasks complete.
491+
_taskCollection.Complete();
492+
_taskCollectionException = ex;
493+
break;
494+
}
495+
496+
// Loop is exited only when task collection is closed and all task
497+
// collection tasks are processed.
498+
if (!isOpen)
499+
{
500+
break;
501+
}
502+
}
503+
504+
// We are done adding tasks and can close the task pool.
505+
_taskPool.Close();
506+
});
445507
}
446508

447509
private void ProcessParallelParameterSet()
@@ -461,53 +523,77 @@ private void ProcessParallelParameterSet()
461523

462524
if (AsJob)
463525
{
526+
// Add child task job.
464527
var taskChildJob = new PSTaskChildJob(
465528
Parallel,
466529
_usingValuesMap,
467530
InputObject);
468531

469532
_taskJob.AddJob(taskChildJob);
533+
534+
return;
470535
}
471-
else
472-
{
473-
// Write any streaming data
474-
_taskDataStreamWriter.WriteImmediate();
475536

476-
var task = new System.Management.Automation.PSTasks.PSTask(
477-
Parallel,
478-
_usingValuesMap,
479-
InputObject,
480-
_taskDataStreamWriter);
537+
// Write any streaming data
538+
_taskDataStreamWriter.WriteImmediate();
481539

482-
// Add task to task pool.
483-
// Block if the pool is full and wait until task can be added.
484-
_taskPool.Add(task, _taskDataStreamWriter);
540+
// Add to task collection for processing.
541+
if (_taskCollection.IsOpen)
542+
{
543+
try
544+
{
545+
// Create a PSTask based on this piped input and add it to the task collection.
546+
// A dedicated thread will add it to the PSTask pool in a performant manner.
547+
_taskCollection.Add(
548+
new System.Management.Automation.PSTasks.PSTask(
549+
Parallel,
550+
_usingValuesMap,
551+
InputObject,
552+
_taskDataStreamWriter));
553+
}
554+
catch (InvalidOperationException)
555+
{
556+
// This exception is thrown if the task collection is closed, which should not happen.
557+
Dbg.Assert(false, "Should not add to a closed PSTask collection");
558+
}
485559
}
486560
}
487561

488562
private void EndParallelParameterSet()
489563
{
490564
if (AsJob)
491565
{
566+
// Start and return parent job object.
492567
_taskJob.Start();
493568
JobRepository.Add(_taskJob);
494569
WriteObject(_taskJob);
570+
571+
return;
495572
}
496-
else
497-
{
498-
_taskDataStreamWriter.WriteImmediate();
573+
574+
// Close task collection and wait for processing to complete while streaming data.
575+
_taskDataStreamWriter.WriteImmediate();
576+
_taskCollection.Complete();
577+
_taskDataStreamWriter.WaitAndWrite();
499578

500-
_taskPool.Close();
501-
_taskDataStreamWriter.WaitAndWrite();
579+
// Check for an unexpected error from the _taskCollection handler thread and report here.
580+
var ex = _taskCollectionException;
581+
if (ex != null)
582+
{
583+
var msg = string.Format(CultureInfo.InvariantCulture, InternalCommandStrings.ParallelPipedInputProcessingError, ex);
584+
WriteError(
585+
new ErrorRecord(
586+
exception: new InvalidOperationException(msg),
587+
errorId: "ParallelPipedInputProcessingError",
588+
errorCategory: ErrorCategory.InvalidOperation,
589+
targetObject: this));
502590
}
503591
}
504592

505593
private void StopParallelProcessing()
506594
{
507-
if (!AsJob)
508-
{
509-
_taskPool.StopAll();
510-
}
595+
_taskCollection?.Complete();
596+
_taskPool?.StopAll();
511597
}
512598

513599
#endregion

src/System.Management.Automation/engine/hostifaces/PSTask.cs

Lines changed: 21 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -602,12 +602,16 @@ internal sealed class PSTaskPool : IDisposable
602602
#region Members
603603

604604
private readonly ManualResetEvent _addAvailable;
605-
private readonly ManualResetEvent _stopAll;
606-
private readonly Dictionary<int, PSTaskBase> _taskPool;
607605
private readonly int _sizeLimit;
606+
private readonly ManualResetEvent _stopAll;
608607
private readonly object _syncObject;
608+
private readonly Dictionary<int, PSTaskBase> _taskPool;
609+
private readonly WaitHandle[] _waitHandles;
609610
private bool _isOpen;
610611

612+
private const int AddAvailable = 0;
613+
private const int Stop = 1;
614+
611615
#endregion
612616

613617
#region Constructor
@@ -625,6 +629,11 @@ public PSTaskPool(int size)
625629
_syncObject = new object();
626630
_addAvailable = new ManualResetEvent(true);
627631
_stopAll = new ManualResetEvent(false);
632+
_waitHandles = new WaitHandle[]
633+
{
634+
_addAvailable, // index 0
635+
_stopAll, // index 1
636+
};
628637
_taskPool = new Dictionary<int, PSTaskBase>(size);
629638
}
630639

@@ -672,46 +681,21 @@ public void Dispose()
672681
/// This method is not multi-thread safe and assumes only one thread waits and adds tasks.
673682
/// </summary>
674683
/// <param name="task">Task to be added to pool.</param>
675-
/// <param name="dataStreamWriter">Optional cmdlet data stream writer.</param>
676684
/// <returns>True when task is successfully added.</returns>
677-
public bool Add(
678-
PSTaskBase task,
679-
PSTaskDataStreamWriter dataStreamWriter = null)
685+
public bool Add(PSTaskBase task)
680686
{
681687
if (!_isOpen)
682688
{
683689
return false;
684690
}
685691

686-
WaitHandle[] waitHandles;
687-
if (dataStreamWriter != null)
688-
{
689-
waitHandles = new WaitHandle[]
690-
{
691-
_addAvailable, // index 0
692-
_stopAll, // index 1
693-
dataStreamWriter.DataAddedWaitHandle // index 2
694-
};
695-
}
696-
else
697-
{
698-
waitHandles = new WaitHandle[]
699-
{
700-
_addAvailable, // index 0
701-
_stopAll, // index 1
702-
};
703-
}
692+
// Block until either space is available, or a stop is commanded
693+
var index = WaitHandle.WaitAny(_waitHandles);
704694

705-
// Block until either room is available, data is ready for writing, or a stop command
706-
while (true)
695+
switch (index)
707696
{
708-
var index = WaitHandle.WaitAny(waitHandles);
709-
710-
// Add new task
711-
if (index == 0)
712-
{
697+
case AddAvailable:
713698
task.StateChanged += HandleTaskStateChangedDelegate;
714-
715699
lock (_syncObject)
716700
{
717701
if (!_isOpen)
@@ -727,21 +711,13 @@ public bool Add(
727711

728712
task.Start();
729713
}
730-
731714
return true;
732-
}
733715

734-
// Stop all
735-
if (index == 1)
736-
{
716+
case Stop:
717+
return false;
718+
719+
default:
737720
return false;
738-
}
739-
740-
// Data ready for writing
741-
if (index == 2)
742-
{
743-
dataStreamWriter.WriteImmediate();
744-
}
745721
}
746722
}
747723

@@ -977,7 +953,7 @@ public void Start()
977953
// This thread will end once all jobs reach a finished state by either running
978954
// to completion, terminating with error, or stopped.
979955
System.Threading.ThreadPool.QueueUserWorkItem(
980-
(state) =>
956+
(_) =>
981957
{
982958
foreach (var childJob in ChildJobs)
983959
{

src/System.Management.Automation/resources/InternalCommandStrings.resx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,4 +181,7 @@
181181
<value>The following common parameters are not currently supported in the Parallel parameter set:
182182
ErrorAction, WarningAction, InformationAction, PipelineVariable</value>
183183
</data>
184+
<data name="ParallelPipedInputProcessingError" xml:space="preserve">
185+
<value>An unexpected error has occurred while processing ForEach-Object -Parallel input. This may mean that some of the piped input did not get processed. Error: {0}.</value>
186+
</data>
184187
</root>

0 commit comments

Comments
 (0)