Throttling events based on previous occurrence - c#

I have this test method
[Fact]
public async Task MethodName(){
var strings = new BufferBlock<string>();
var ints = new BufferBlock<int>();
ConcurrentStack<Tuple<string,int> > concurrentStack=new ConcurrentStack<Tuple<string, int>>();
var transformBlock = new TransformBlock<Tuple<string,int>,string>(async tuple => {
var formattableString = $"{tuple.Item2}-{tuple.Item1}-{Environment.CurrentManagedThreadId}";
if (concurrentStack.TryPeek(out var result) && Equals(result, tuple)){
WriteLine($"Await -> {formattableString}");
await Task.Delay(1000);
}
concurrentStack.Push(tuple);
await DoAsync(tuple);
WriteLine(formattableString);
return formattableString;
});
var joinBlock = new JoinBlock<string,int>();
var dataflowLinkOptions = new DataflowLinkOptions(){PropagateCompletion = true};
joinBlock.LinkTo(transformBlock,dataflowLinkOptions);
strings.LinkTo(joinBlock.Target1,dataflowLinkOptions);
ints.LinkTo(joinBlock.Target2,dataflowLinkOptions);
strings.Post("a");
strings.Post("a");
strings.Post("b");
strings.Post("b");
ints.Post(1);
ints.Post(1);
ints.Post(2);
ints.Post(1);
strings.Complete();
transformBlock.LinkTo(DataflowBlock.NullTarget<string>());
await transformBlock.Completion;
}
which outputs
15:36:53.2369|1-a-28
15:36:53.2369|Await -> 1-a-28
15:36:54.2479|1-a-28
15:36:54.2479|2-b-38
15:36:54.2479|1-b-38
I am looking to throttle this operation in a way that if the previous item is the same as the current to wait for a certain amount of time. I would like to use this flow in a parallel scenario and my guess is that RX extensions could be the solution here as I do not feel my approach is correct although it seems to produce the correct results.
UPDATE - CONCRETE EXAMPLE
In comments #Enigmativity requested to provide examples of my sources and transforms, so I think the real implementation is the best fit to this.
_requestHandlerDatas = new BufferBlock<IRequestHandlerData>(executionOption);
_webProxies = new TransformBlock<WebProxy, PingedWebProxy>(async webProxy => {
var isOnline =await requestDataflowData.Pinger.IsOnlineAsync(webProxy.Address).ConfigureAwait(false);
return new PingedWebProxy(webProxy.Address, isOnline);
}, executionOption);
var joinBlock = new JoinBlock<IRequestHandlerData, WebProxy>(new GroupingDataflowBlockOptions { Greedy = false });
_transformBlock = new TransformBlock<Tuple<IRequestHandlerData, WebProxy>, TOut>(async tuple => {
await requestDataflowData.LimitRateAsync(tuple.Item1, tuple.Item2).ConfigureAwait(false);
var #out = await requestDataflowData.GetResponseAsync<TOut>(tuple.Item1, tuple.Item2).ConfigureAwait(false);
_webProxies.Post(tuple.Item2);
_requestHandlerDatas.Post(tuple.Item1);
return #out;
}, executionOption);
var dataflowLinkOptions = new DataflowLinkOptions(){PropagateCompletion = true};
joinBlock.LinkTo(_transformBlock,dataflowLinkOptions,tuple => ((PingedWebProxy)tuple.Item2).IsOnline);
joinBlock.LinkTo(DataflowBlock.NullTarget<Tuple<IRequestHandlerData, WebProxy>>(),dataflowLinkOptions,tuple =>
!((PingedWebProxy) tuple.Item2).IsOnline);
_requestHandlerDatas.LinkTo(joinBlock.Target1,dataflowLinkOptions);
_webProxies.LinkTo(joinBlock.Target2,dataflowLinkOptions);
So, first I am posting data to _webProxies which verifies that I have a working proxy and if so it links it together with my _requestHandlerDatas in a _transformBlock that will use them to call another service. When that call returns I am releasing both the used parameters (proxy, handlerData) back to their pools and output the TOut for further processing from another block. My goal is to delay the webservice calls from the same proxy,handlerData combination.

Related

Getting error 409 "confict" because of Task.WhenAll() involving SqlDataReader

I'm asking this more because I have no idea of why the way I solved the issue works.
Here's the method that gives me the error (notice the return line):
public async Task<IEnumerable<ContractServiceResponse>> GetContractServices(Guid contractId)
{
var services = await _translationManager.GetCachedResource<Service>();
var rates = await _translationManager.GetCachedResource<Rate>();
var contractServices = await _dbContext.ContractService
.Where(cs => cs.ContractId == contractId)
.ToListAsync();
var serviceCenterBillableConcepts = await _billableConceptService.GetServiceCenterBillableConcepts(services.Select(s => s.Id).Distinct());
var contractBillableConcepts = await _billableConceptService.GetContractBillableConcepts(contractId);
var servicesResponse = contractServices.Select(async cs => new ContractServiceResponse
{
Id = cs.ServiceId,
Service = services.FirstOrDefault(s => s.Id == cs.ServiceId).Translation,
Enabled = cs.Billable,
ExceptionReason = cs.BillableExceptionReason,
BillableConcepts = await AddContractBillableConceptsToServices(cs.ServiceId, contractBillableConcepts, serviceCenterBillableConcepts, rates)
});
return await Task.WhenAll(servicesResponse);
}
And here's the code that works (notice the return line).
public async Task<IEnumerable<ContractServiceResponse>> GetContractServices(Guid contractId)
{
var services = await _translationManager.GetCachedResource<Service>();
var rates = await _translationManager.GetCachedResource<Rate>();
var contractServices = await _dbContext.ContractService
.Where(cs => cs.ContractId == contractId)
.ToListAsync();
var serviceCenterBillableConcepts = await _billableConceptService.GetServiceCenterBillableConcepts(services.Select(s => s.Id).Distinct());
var contractBillableConcepts = await _billableConceptService.GetContractBillableConcepts(contractId);
var servicesResponse = contractServices.Select(async cs => new ContractServiceResponse
{
Id = cs.ServiceId,
Service = services.FirstOrDefault(s => s.Id == cs.ServiceId).Translation,
Enabled = cs.Billable,
ExceptionReason = cs.BillableExceptionReason,
BillableConcepts = await AddContractBillableConceptsToServices(cs.ServiceId, contractBillableConcepts, serviceCenterBillableConcepts, rates)
});
return servicesResponse.Select(t => t.Result);
}
Why the second one works, but the first one doesn't?
Thanks in advance.
First, this has nothing to do with SqlDataReader or any database. Databases don't return HTTP status codes. The 409 is returned by whatever is called by await AddContractBillableConceptsToServices
The first example executes as many calls as there are items in contractServices at the exact same time. If there are 100 items, it will execute 100 requests. All those requests would try to allocate and use memory, access databases or limited other resources at the same time, causing blocks, queueing and possibly deadlocks. Making 100 concurrent calls could easily result in 100x degradation if not outright crashing the service.
That's why production services always implement throttling and queuing, and, if a client is ill-behaved, will throw a 409 and block it for a while.
The second code runs sequentially. t.Result blocks until the task t completes, so the second code is no different than:
foreach(var cs in contractServices)
{
var t=AddContractBillableConceptsToServices(cs.ServiceId, contractBillableConcepts, serviceCenterBillableConcepts, rates);
t.Wait();
yield return new ContractServiceResponse
{
BillableConcepts = t.Result
}
}
Execute only N requests at a time
A real solution is to execute only a limited number of requests at a time, using, eg Parallel.ForEachAsync :
var results=new ConcurrentQueue<ContractServiceResponse> ();
Parallel.ForEachAsync(contractServices, async cs=>{
var concepts=AddContractBillableConceptsToServices(cs.ServiceId, contractBillableConcepts, serviceCenterBillableConcepts, rates);
var response=new ContractServiceResponse
{
...
BillableConcepts = concepts
}
results.Enqueue(response);
});
By default, ForEachAsync will make as many concurrent calls as there are cores. This can be changed through the ParallelOptions.MaxDegreeOfParallelism property.
Youre executing everything in sequence. servicesResponse.Select(t => t.Result) won't do anything until the IEnumerable is enumerated by the consumer.
Task.WhenAll() will run the tasks in parallel. Your issue is that the code you're calling, does not play nice with parallelism.
To solve this, don't use WhenAll, but just use await.
Alternatively you can pinpoint the code which breaks when it is run in parallel, and implement some kind of locking mechanism using a semaphoreslim

Why doesn't my function await the end of ForEachAsync?

I'm trying to fill my model with data that I get from an asynchronous operation to my database. The problem is that the function returns the View (without the completed model), despite my await call.
I have tried to put a timer (I know that is not the solution), to be sure that the problem come from the asynchronous, I have also tried to put on comment some part of code inside my ForEachAsync, but it doesn't seem to help.
I get a list of project, that I fill with some additional information, finally, I assign my object to my model then return the View
public async Task<IActionResult> newProjetList(int GestionaireId, int VilleId)
{
ProjetListModel model = new ProjetListModel();
ProjetService projetService = new ProjetService(connectionString);
UserServices userServices = new UserServices(connectionString);
AvancementService avancementService = new AvancementService(connectionString);
VilleService villeService = new VilleService(connectionString);
List<Projet> projets = await projetService.NewProjetLine(GestionaireId, VilleId);
await projets.ToAsyncEnumerable().ForEachAsync(async p =>
{
int villeId = await villeService.getVilleIdByProjetId(p.ProjetId);
Ville ville = await villeService.GetById(villeId);
p.Ville = ville.VilleLabel;
p.GestionnaireProjet = await userServices.GetNameById(p.GestionnaireProjetId ?? 0);
await p.SousProjet.ToAsyncEnumerable().ForEachAsync(async sp =>
{
sp.Avancement = await avancementService.GetLabelById(sp.AvancementId);
});
});
model.projets = projets;
//System.Threading.Thread.Sleep(5000);
return View("ProjetList", model);
}
I expected an output with the missing information (here are the 'ville', 'gestionnairesProjet' and 'Avancement'
ForEachAsync only takes an Action<...>, not a Func<..., Task>, so the async lambda your code is passing to ForEachAsync is becoming an async void method. One of the primary reasons async void should be avoided is that it's not easy to determine when the method completes - and in fact, in this case, there is nothing ensuring that it will complete before sending the response.
I recommend doing what Marc suggested and just using foreach:
List<Projet> projets = await projetService.NewProjetLine(GestionaireId, VilleId);
foreach (var p in projects)
{
int villeId = await villeService.getVilleIdByProjetId(p.ProjetId);
Ville ville = await villeService.GetById(villeId);
p.Ville = ville.VilleLabel;
p.GestionnaireProjet = await userServices.GetNameById(p.GestionnaireProjetId ?? 0);
foreach (var sp in p.SousProject)
{
sp.Avancement = await avancementService.GetLabelById(sp.AvancementId);
}
}
model.projets = projets;
Or, if you want to use asynchronous concurrency, you can make use of Task.WhenAll:
List<Projet> projets = await projetService.NewProjetLine(GestionaireId, VilleId);
await Task.WhenAll(projects.Select(async p =>
{
int villeId = await villeService.getVilleIdByProjetId(p.ProjetId);
Ville ville = await villeService.GetById(villeId);
p.Ville = ville.VilleLabel;
p.GestionnaireProjet = await userServices.GetNameById(p.GestionnaireProjetId ?? 0);
await Task.WhenAll(p.SousProject.Select(async sp =>
{
sp.Avancement = await avancementService.GetLabelById(sp.AvancementId);
});
});
model.projets = projets;
Use ForEachAwaitAsync instead of ForEachAsync
Explanation: ForEachAsync can't wait since it is simply a multi-threaded execution of your loop (accepts Action). In fact you might have received a compiler warning by using async for your lambda return, because you intend to assign a Task to a void (unused)
ForEachAwaitAsync will wait because it accepts a Func<Task> and internally also awaits / asyncs accordingly.
For curious minds, you can see the source code here: https://github.com/dotnet/reactive/blob/main/Ix.NET/Source/System.Linq.Async/System/Linq/Operators/ForEach.cs mentioned as ForEachAwaitAsyncCore and ForEachAsync

TPL Dataflow LinkTo TransformBlock is very slow

I have two TransformBlocks which are arranged in a loop. They link their data to each other. TransformBlock 1 is an I/O block reading data and is limited to a maximum of 50 tasks. It reads the data and some meta data. Then they are passed to the second block. The second block decides on the meta data if the message goes again to the first block. So after the meta data matches the criteria and a short wait the data should go again back again to the I/O block. The second blocks MaxDegreeOfParallelism can be unlimited.
Now I have noticed when I send a lot of data to the I/O block it takes a long time till the messages are linked to the second block. It takes like 10 minutes to link the data and they are all sent in a bunch. Like 1000 entries in a few seconds.
Normally I would implement it like so:
public void Start()
{
_ioBlock = new TransformBlock<Data,Tuple<Data, MetaData>>(async data =>
{
var metaData = await ReadAsync(data).ConfigureAwait(false);
return new Tuple<Data, MetaData>(data, metaData);
}, new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = 50 });
_waitBlock = new TransformBlock<Tuple<Data, MetaData>,Data>(async dataMetaData =>
{
var data = dataMetaData.Item1;
var metaData = dataMetaData.Item2;
if (!metaData.Repost)
{
return null;
}
await Task.Delay(TimeSpan.FromMinutes(1)).ConfigureAwait(false);
return data;
}, new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = DataflowBlockOptions.Unbounded });
_ioBlock.LinkTo(_waitBlock);
_waitBlock.LinkTo(_ioBlock, data => data != null);
_waitBlock.LinkTo(DataflowBlock.NullTarget<Data>());
foreach (var data in Enumerable.Range(0, 2000).Select(i => new Data(i)))
{
_ioBlock.Post(data);
}
}
But because of the described problem I have to implement it like so:
public void Start()
{
_ioBlock = new ActionBlock<Data>(async data =>
{
var metaData = await ReadAsync(data).ConfigureAwait(false);
var dataMetaData= new Tuple<Data, MetaData>(data, metaData);
_waitBlock.Post(dataMetaData);
}, new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = 50 });
_waitBlock = new ActionBlock<Tuple<Data, MetaData>>(async dataMetaData =>
{
var data = dataMetaData.Item1;
var metaData = dataMetaData.Item2;
if (metaData.Repost)
{
await Task.Delay(TimeSpan.FromMinutes(1)).ConfigureAwait(false);
_ioBlock.Post(data);
}
}, new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = DataflowBlockOptions.Unbounded });
foreach (var data in Enumerable.Range(0, 2000).Select(i => new Data(i)))
{
_ioBlock.Post(data);
}
}
When I use the second approach the data get linked/posted faster (one by one). But it feels more like a hack to me. Anybody know how to fix the problem? Some friends recommended me to use TPL Pipeline but it seems much more complicated to me.
Problem solved. You need to set
ExecutionDataflowBlockOptions.EnsureOrdered
to forward the data immediately to the next/wait block.
Further information:
Why do blocks run in this order?

Applying async and await in lengthy while loops

We're having to call data from a hideously limited Web API provided to us by means beyond our control. The logic flow is as follows:
For each product represented by a list of ID values
Get each batch of sub-categories of type FOO (100 records max. per call)
Keep calling the above until no records remain
Get each batch of sub-categories of type BAR (100 records max. per call)
Keep calling the above until no records remain
This at present is generating nearly 100 web API calls (we've asked the provider, and there's no improving the web API to mitigate this).
I'm worried that performance will suffer drastically because of this, so am trying to get my head around the asynchronous alternative, in the hope that this will help. One major issue is that the data can only be called once. After that, it becomes locked and isn't resent, which is a major limitation to our testing.
I've read here, here and here, but am struggling to adapt to my code as I think I need two await calls rather than one, and am worried things will get messed up.
Can anyone please apply the await and async logic into this pseudo code so that I can then read up and try follow the flow of what's happening?
public class DefaultController : Controller
{
public ActionResult Index()
{
var idlist = new List<String>() {"123", "massive list of strings....", "789"};
var xdoc = new XDocument();
xdoc.Declaration = new XDeclaration("1.0", Encoding.Unicode.WebName, "yes");
var xroot = new XElement("records");
xdoc.Add(xroot);
foreach (string id in idlist)
{
// Get types FOO -----------------------------------
Boolean keepGoingFOO = true;
while (keepGoingFOO)
{
// 100 records max per call
var w = new WebServiceClient();
request.enumType = enumType.FOO;
var response = w.response();
foreach (ResultItem cr in response.ResultList)
{
var xe = new XElement("r");
// create XML
xroot.Add(xe);
}
keepGoingFOO = response.moreRecordsExist;
}
// Get types BAR -----------------------------------
Boolean keepGoingBAR = true;
while (keepGoingBAR)
{
// 100 records max per call
var w = new WebServiceClient();
request.enumType = enumType.BAR;
var response = w.response();
foreach (ResultItem cr in response.ResultList)
{
var xe = new XElement("r");
// create XML
xroot.Add(xe);
}
keepGoingBAR = response.moreRecordsExist;
}
}
return View(xdoc);
}
}
Should get you started:
public async ActionResult Index()
{
var idlist = new List<string>() { "123", "massive list of strings....", "789" };
IEnumerable<XElement> list = await ProcessList(idlist);
//sort the list as it will be completely out of order
return View(xdoc);
}
public async Task<IEnumerable<XElement>> ProcessList(IEnumerable<string> idlist)
{
IEnumerable<XElement>[] processList = await Task.WhenAll(idlist.Select(FooBar));
return processList.Select(x => x.ToList()).SelectMany(x => x);
}
private async Task<IEnumerable<XElement>> FooBar(string id)
{
Task<IEnumerable<XElement>> foo = Foo(id);
Task<IEnumerable<XElement>> bar = Bar(id);
return ((await bar).Concat(await foo));
}
private async Task<IEnumerable<XElement>> Bar(string id)
{
var localListOfElements = new List<XElement>();
var keepGoingFoo = true;
while (keepGoingFoo)
{
var response = await ServiceCallAsync(); //make sure you use the async version
localListOfElements.Add(new XElement("r"));
keepGoingFoo = response.moreRecordsExist;
}
return localListOfElements;
}
private async Task<IEnumerable<XElement>> Foo(string id)
{
var localListOfElements = new List<XElement>();
var keepGoingFoo = true;
while (keepGoingFoo)
{
var response = await ServiceCallAsync(); //make sure you use the async version
localListOfElements.Add(new XElement("r"));
keepGoingFoo = response.moreRecordsExist;
}
return localListOfElements;
}
private async Task<Response> ServiceCallAsync()
{
await Task.Delay(1000);//simulation
return new Response();
}
There are multiple issues with your code, it can be greatly improved by refactoring as the only items changing in between for loops is the request.EnumType. And performance could be greatly improved with proper use of async awaits - as long as the ids are independent the question is not parallelizing two - but rather parallelizing as much as possible.
The portion slowing your time is not xml access - it is web api calls.
I would refactor it as
async Task<Tuple<string, enumType, XElement>> SendRequest(string id, enumType input){
..
}
And replace the for loop with
List<Tuple<string, enumType>> tupleList = idList.Select(id => Tuple.Create(id, enumType.BAR)).ToList();
tupleList.Concat(idList.Select(id => Tuple.Create(id, enumType.FOO)).ToList());
Task<Tuple<string, enumType, XElement>>[] all = tupleList
.Select(c => SendRequest(c.Item1, c.Item2))
.ToArray();
var res = await Task.WhenAll(tasks);
The res variable will contain all the XElement values you want to add which should be fast. You can instead use a Key Value pair with Tuple of id-enumType being the key as well yet the idea is the same.
To make the solution more elegant, I would hide the batching behind an enumerable object, like weston proposed, but instead of putting all items on one list - consume them as soon as they are available (to minimize memory utilization).
With AsyncEnumerator NuGet Package you can write the code like this:
public class DefaultController : Controller
{
public async ActionResult Index()
{
var idlist = new List<String>() { "123", "massive list of strings....", "789" };
var xdoc = new XDocument();
xdoc.Declaration = new XDeclaration("1.0", Encoding.Unicode.WebName, "yes");
var xroot = new XElement("records");
xdoc.Add(xroot);
foreach (string id in idlist) {
// Get types FOO -----------------------------------
var foos = EnumerateItems(enumType.FOO);
await foos.ForEachAsync(cr => {
var xe = new XElement("r");
// create XML
xroot.Add(xe);
});
// Get types BAR -----------------------------------
var bars = EnumerateItems(enumType.BAR);
await foos.ForEachAsync(cr => {
var xe = new XElement("r");
// create XML
xroot.Add(xe);
});
}
return View(xdoc);
}
public IAsyncEnumerable<ResultItem> EnumerateItems(enumType itemType)
{
return new AsyncEnumerable<ResultItem>(async yield => {
Boolean keepGoing = true;
while (keepGoing) {
// 100 records max per call
var w = new WebServiceClient();
request.enumType = itemType;
// MUST BE ASYNC CALL
var response = await w.responseAsync();
foreach (ResultItem cr in response.ResultList)
await yield.ReturnAsync(cr);
keepGoing = response.moreRecordsExist;
}
});
}
}
Note 1: by making everything async actually does not improve the performance of one single routine, and in fact makes it little bit slower (due to the overhead of async state machines and TPL tasks). However, it helps to better utilize the worker threads in your app overall.
Note 2: your client request must be Async, otherwise the optimization does not make sense - if you do it synchronously it blocks the thread and just waits when the server respond.
Note 3: please pass a CancellationToken to every Async method - that's a good practice.
Note 4: judging from the code, you have a web server, so I do not recommend to run all batches for all items in parallel and do Task.WhenAll to wait for them - if your service client is synchronous (the w.response call) then it will just block a lot of threads and your whole web service might get unresponsive.
In the proposed solution you can do one trick to do thing in parallel - your can read ahead the next batch while processing the current one like this:
public IAsyncEnumerable<ResultItem> EnumerateItemsWithReadAhead(enumType itemType)
{
return new AsyncEnumerable<ResultItem>(async yield => {
Task<Response> nextBatchTask = FetchNextBatch(itemType);
Boolean keepGoing = true;
while (keepGoing) {
var response = await nextBatchTask;
// Kick off the next batch request (read ahead)
keepGoing = response.moreRecordsExist;
if (keepGoing)
nextBatchTask = FetchNextBatch(itemType);
foreach (ResultItem cr in response.ResultList)
await yield.ReturnAsync(cr);
}
});
}
private Task<Response> FetchNextBatch(enumType itemType)
{
// 100 records max per call
var w = new WebServiceClient();
request.enumType = itemType;
// MUST BE ASYNC
return w.responseAsync();
}

Deferring tasks execution

I'm playing with Tasks and I would like to defer my task's execution.
I've a sample method like this:
private async Task<bool> DoSomething(string name, int delayInSeconds)
{
Debug.WriteLine($"Inside task named: {name}");
await Task.Delay(TimeSpan.FromSeconds(delayInSeconds));
Debug.WriteLine($"Finishing task named: {name}");
return true;
}
I would like to create few tasks first, then perform some job and after this run those tasks. As the line Task<bool> myTask = DoSomething("Name", 4); fires the task right away, I've figured something like this:
string[] taskNames = new string[2];
Task<Task<bool>>[] myTasks = new Task<Task<bool>>[2];
myTasks[0] = new Task<Task<bool>>(async () => await DoSomething(taskNames[0], taskNames[0].Length));
myTasks[1] = new Task<Task<bool>>(async () => await DoSomething(taskNames[1], taskNames[1].Length));
// I think I can declare it also like this, but this will create tasks later
//IEnumerable<Task<Task<bool>>> myTasks = taskNames.Select(x => new Task<Task<bool>>(async () => await DoSomething(x, x.Length)));
taskNames[0] = "First";
taskNames[1] = "Second";
Debug.WriteLine($"Tasks created");
var results = await Task.WhenAll(myTasks.Select(x => { x.Start(); return x.Unwrap(); }));
Debug.WriteLine($"Finishing: {results.Select(x => x.ToString()).Aggregate((a,b) => a + "," + b) }");
Can this be done different way, without wrapping task?
You can just use Task-producing delegates to simplify things a bit:
string[] taskNames = new string[2];
Func<Task<bool>>[] myTasks = new Func<Task<bool>>[2];
myTasks[0] = new Func<Task<bool>>(async () => await DoSomething(taskNames[0], taskNames[0].Length));
myTasks[1] = new Func<Task<bool>>(() => DoSomething(taskNames[1], taskNames[1].Length)); // Shorter version, near-identical functionally.
// I think I can declare it also like this, but this will create tasks later
//IEnumerable<Task<Task<bool>>> myTasks = taskNames.Select(x => new Task<Task<bool>>(async () => await DoSomething(x, x.Length)));
taskNames[0] = "First";
taskNames[1] = "Second";
Debug.WriteLine($"Tasks created");
var results = await Task.WhenAll(myTasks.Select(x => x()));
Debug.WriteLine($"Finishing: {results.Select(x => x.ToString()).Aggregate((a, b) => a + "," + b) }");
Caveat: DoSomething will execute synchronously up to the first await when you invoke those delegates, so the behaviour is similar, but not exactly identical.
Alternatively, your IEnumerable-based solution will work fine too. Just write an iterator method and yield return tasks as you start them.
Personally though I'd just do this:
string[] taskNames = new string[2];
taskNames[0] = "First";
taskNames[1] = "Second";
var results = await Task.WhenAll(taskNames.Select(n => DoSomething(n, n.Length)));
Debug.WriteLine($"Finishing: {results.Select(x => x.ToString()).Aggregate((a, b) => a + "," + b) }");
You're not just "creating a Task" in your example. You're invoking a method, DoSomething, that returns a Task. Assuming these are async methods, the Task creation and starting takes place behind the scenes in compiler-generated code.
The solution to this problem is easy: Don't invoke the method until you're ready for the method to be running. Imagine how confusing the behavior you're asking for would be in any other context.

Categories

Resources