The Problem
I am developing a Visual Studio extension for importing code from a server.
Because of the way Visual Studio works, multiple threads could try to perform the same operation at the same time.
Thus, I wanted a class that would ensure that a) each Task
would only be performed when needed and b) each Task
would only be performed once.
The only exception to b) is if the requesters cancel and then later another request is made in which case if the operation was not finished, it should be retried.
Intended functionality:
- A
TaskManager
object is created. - At some point,
TaskManager.Value
is called on the object and theTask
is set to run. - If
TaskManger.Value
is called multiple times (possibly on other threads), the other executions are set waiting on theTask
that is already in progress. - If the
CancellationToken
indicates a cancellation request, only if all the requestors agree should theTask
be cancelled. - If the
TaskManager
object is cancelled before completion and later is requested again, theTask
should be reinitialized.
The Code
public class TaskManager<T>
{
readonly Func<CancellationToken, Task<T>> _initializer;
public TaskManager(Func<CancellationToken, Task<T>> initializer) => _initializer = initializer;
Task<T>? _task;
CancellationTokenSource _cancellationToken = new CancellationTokenSource();
readonly object _initializationLock = new();
readonly ConcurrentBag<CancellationToken> _cancellationTokens = new();
public Task<T> Value (CancellationToken cancellationToken)
{
if (cancellationToken != default)
{
if (cancellationToken.IsCancellationRequested) return Task.FromResult<T>(default);
_cancellationTokens.Add(cancellationToken);
cancellationToken.Register(() =>
{
if (_task is not { IsCompleted: true } && _cancellationTokens.All(c => c.IsCancellationRequested))
_cancellationToken.Cancel();
});
}
if (_task != null && !_cancellationToken.IsCancellationRequested) return _task;
lock (_initializationLock)
{
if (_task != null) return _task;
_cancellationToken = new CancellationTokenSource();
_task = _initializer(_cancellationToken.Token);
}
return _task;
}
}
Example Usage
public static class CodepediaApi
{
private static readonly RestClient client = new RestClient("https://localhost:5001"); // Test Server
private static readonly Dictionary<string, TaskManager<SearchResult[]>> responses = new Dictionary<string, TaskManager<SearchResult[]>>();
public static Task<SearchResult[]> Search(string query, CancellationToken cancellationToken) =>
responses.TryGetOrAdd(query,
() => new TaskManager<SearchResult[]>(async c =>
(await client.GetAsync<SearchResult[]>(new RestRequest("/api/search").AddQueryParameter("q", query), c)) ?? new SearchResult[0]
)
).Value(cancellationToken);
}
where TryGetOrAdd
is defined as follows:
public static class Extensions
{
public static TValue TryGetOrAdd<TKey, TValue>(this IDictionary<TKey, TValue> d, TKey key, Func<TValue> toAdd)
{
if (d.TryGetValue(key, out TValue value))
return value;
TValue adding = toAdd();
lock (d)
{
if (d.TryGetValue(key, out value))
return value;
d.Add(key, adding);
}
return adding;
}
}
Another Example Usage
Here is one of my classes:
public class CodepediaUtil
{
public readonly Workspace Workspace;
public readonly SnapshotSpan Range;
public readonly TaskManager<(Document document, SemanticModel semanticModel, SyntaxNode outerNode, UnresolvedMethodCallInfo unresolvedInvocation)?> CodeAnalysisInfo;
public readonly TaskManager<SearchResult[]> SearchResults;
public readonly TaskManager<Dictionary<SearchResult, WikiEntry>> SearchResultsInfo;
public async Task<UnresolvedMethodCallInfo?> UnresolvedInvocation (CancellationToken cancellationToken)
{
var unresolvedInvocationInfo = await CodeAnalysisInfo.Value(cancellationToken);
if (unresolvedInvocationInfo is not (_, _, _, UnresolvedMethodCallInfo unresolvedInvocation)) return null;
return unresolvedInvocation;
}
public CodepediaUtil (Workspace workspace, SnapshotSpan range)
{
Workspace = workspace;
Range = range;
CodeAnalysisInfo = new TaskManager<(Document document, SemanticModel semanticModel, SyntaxNode outerNode, UnresolvedMethodCallInfo unresolvedInvocation)?>
(
async (CancellationToken cancellationToken) =>
{
Document? document = Range.Snapshot.TextBuffer.GetRelatedDocuments().FirstOrDefault();
if (document == null) return null;
SemanticModel? semanticModel = await document.GetSemanticModelAsync(cancellationToken);
if (semanticModel == null) return null;
SyntaxNode? outerNode = await CodeAnalyzer.GetOuterNodeAsync(semanticModel, Range.Start, cancellationToken);
if (outerNode == null) return null;
UnresolvedMethodCallInfo? unresolvedInvocation = outerNode.FindUnresolvedInvocation(semanticModel);
if (unresolvedInvocation == null) return null;
return (document, semanticModel, outerNode, unresolvedInvocation);
}
);
SearchResults = new TaskManager<SearchResult[]>
(
async (CancellationToken cancellationToken) =>
{
UnresolvedMethodCallInfo? unresolvedInvocation = await UnresolvedInvocation(cancellationToken);
if (unresolvedInvocation == null) return new SearchResult[0];
return await CodepediaApi.Search(unresolvedInvocation.Name, cancellationToken);
}
);
SearchResultsInfo = new TaskManager<Dictionary<SearchResult, WikiEntry>>
(
async (CancellationToken cancellationToken) =>
{
// UnresolvedMethodCallInfo? unresolvedInvocation = await UnresolvedInvocation(cancellationToken);
// if (unresolvedInvocation == null) return new Dictionary<SearchResult, WikiEntry>();
// string invocationName = unresolvedInvocation.Name;
return await CodepediaApi.InterpretSearchResults(await SearchResults.Value(cancellationToken), cancellationToken);
}
);
}
}
This coding pattern should allow other classes to request exactly what they need on a per-need basis.
What I want reviewed
- Is my code useful? (Is there a better way to do this?)
- Could the performance/elegance of my code improved? (Any comments to make it better would be appreciated.)
- Did I make a mistake? (Is my code thread-safe? Are there any race conditions or ways it could get stuck?)
await
a Task as many times as you want but that will not trigger a new execution. each Task would only be performed when needed << what do mean by that? Do want to have a cold start on the Task? \$\endgroup\$Task
is that it is not started until theValue
function is called on it but if multiple calls are made to theValue
function, theTask
is only started once. \$\endgroup\$Task
if it is cancelled. Eg. a scenario where a user searches for something, cancels and then searches for the same thing again should retry. \$\endgroup\$