| File: Graph\ParallelWorkSet.cs | Web Access |
| Project: src\msbuild\src\Build\Microsoft.Build.csproj (Microsoft.Build) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; #nullable disable namespace Microsoft.Build.Graph { /// <summary> /// Provides deduping of expensive work by a key, or modeling of a set of deduped work that /// can be awaited as a unit. Completed results are kept in the collection for reuse. /// </summary> internal class ParallelWorkSet<TKey, TResult> { private readonly CancellationToken _cancellationToken; /// <summary> /// Number of workers to process work items. /// </summary> private readonly int _degreeOfParallelism; private readonly ConcurrentDictionary<TKey, WorkItem> _inProgressOrCompletedWork; private bool _isSchedulingCompleted; private long _pendingCount; private readonly ConcurrentQueue<WorkItem> _queue = new ConcurrentQueue<WorkItem>(); private readonly SemaphoreSlim _semaphore; private readonly List<Task> _tasks; private readonly List<Exception> _exceptions = new List<Exception>(0); /// <summary> /// Retrieves all completed work items. /// </summary> internal Dictionary<TKey, TResult> CompletedWork { get { var completedWork = new Dictionary<TKey, TResult>(_inProgressOrCompletedWork.Count); foreach (KeyValuePair<TKey, WorkItem> kvp in _inProgressOrCompletedWork) { WorkItem workItem = kvp.Value; if (workItem.IsValueCreated) { completedWork[kvp.Key] = workItem.Value; } } if (_exceptions.Count > 0) { throw new AggregateException(_exceptions); } return completedWork; } } /// <summary> /// Checks if the work set has been marked as completed. /// </summary> internal bool IsCompleted { get => Volatile.Read(ref _isSchedulingCompleted); private set => Volatile.Write(ref _isSchedulingCompleted, value); } internal ParallelWorkSet(int degreeOfParallelism, IEqualityComparer<TKey> comparer, CancellationToken cancellationToken) { if (degreeOfParallelism < 0) { throw new ArgumentException("Degree of parallelism must be a positive integer."); } _cancellationToken = cancellationToken; _degreeOfParallelism = degreeOfParallelism; _inProgressOrCompletedWork = new ConcurrentDictionary<TKey, WorkItem>(comparer); // Semaphore count is 0 to ensure that all the tasks are blocked unless new data is scheduled. _semaphore = new SemaphoreSlim(0, int.MaxValue); _tasks = new List<Task>(degreeOfParallelism); for (int i = 0; i < degreeOfParallelism; i++) { _tasks.Add(CreateProcessorItemTask()); } } /// <summary> /// Enqueues a work item to the work set. /// </summary> /// <param name="key"></param> /// <param name="workFunc"></param> internal void AddWork(TKey key, Func<TResult> workFunc) { if (IsCompleted) { throw new InvalidOperationException("Cannot add new work after work set is marked as completed."); } WorkItem workItem = new(workFunc); if (!_inProgressOrCompletedWork.TryAdd(key, workItem)) { return; } Interlocked.Increment(ref _pendingCount); // NOTE: Enqueue MUST happen before releasing the semaphore // to ensure WaitAsync below never returns when there is not // a corresponding item in the queue to be dequeued. The only // exception is on completion of all items. _queue.Enqueue(workItem); _semaphore.Release(); } /// <summary> /// Assists processing items until all the items added to the queue are processed, completes the work set, and /// propagates any exceptions thrown by workers. /// </summary> internal void WaitForAllWorkAndComplete() { if (IsCompleted) { return; } while (!_cancellationToken.IsCancellationRequested && Interlocked.Read(ref _pendingCount) > 0) { ExecuteWorkItem(); } IsCompleted = true; // Release one thread that will release all the threads when all the elements are processed. _semaphore.Release(); Task.WaitAll( #if NET _tasks); #else _tasks.ToArray()); #endif if (_exceptions.Count > 0) { throw new AggregateException(_exceptions); } } private Task CreateProcessorItemTask() { return Task.Run( async () => { bool shouldStopAllWorkers = false; while (!shouldStopAllWorkers) { await _semaphore.WaitAsync(_cancellationToken); try { ExecuteWorkItem(); } finally { shouldStopAllWorkers = Interlocked.Read(ref _pendingCount) == 0 && IsCompleted; if (shouldStopAllWorkers) { // Ensure all tasks are unblocked and can gracefully // finish since there are at most degreeOfParallelism - 1 tasks // waiting at this point _semaphore.Release(_degreeOfParallelism); } } } }, _cancellationToken); } private void ExecuteWorkItem() { if (_queue.TryDequeue(out WorkItem workItem)) { try { _ = workItem.Value; } catch (Exception ex) { lock (_exceptions) { _exceptions.Add(ex); } } finally { Interlocked.Decrement(ref _pendingCount); } } } /// <summary> /// A lazily-evaluated work item. Unlike <see cref="Lazy{T}"/>, this does not place a /// <c>DynamicallyAccessedMembers</c> requirement on <typeparamref name="TResult"/> (<see cref="Lazy{T}"/> /// annotates its type argument for the parameterless constructor, which triggers IL2091 here), and it is /// only ever evaluated once by a single worker before the result is read after /// <see cref="WaitForAllWorkAndComplete"/> establishes a happens-before barrier. /// </summary> private sealed class WorkItem { private Func<TResult> _workFunc; private bool _isValueCreated; private TResult _value; internal WorkItem(Func<TResult> workFunc) { _workFunc = workFunc; } internal bool IsValueCreated => _isValueCreated; internal TResult Value { get { if (!_isValueCreated) { _value = _workFunc(); _isValueCreated = true; // Release the factory (and anything its closure captured) once the value is // computed. Completed work items are retained in the set for reuse, so holding // the delegate would keep captured graph state alive for the whole build. // Matches Lazy<T>, which also drops its value factory after evaluation. _workFunc = null; } return _value; } } } } }