-
Notifications
You must be signed in to change notification settings - Fork 4.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Proposal] Asynchronous switch #5187
Comments
@MadsTorgersen Is this too many curly braces? |
@gafter Neat. Am I interpreting the grammar correctly? select {
case string primaryContents = await client.DownloadStringTaskAsync(primaryUri): {
Console.WriteLine($"Downloaded contents from primary URI: {primaryContents}");
}
case string secondaryContents = await client.DownloadStringTaskAsync(secondaryUri): {
Console.WriteLine($"Downloaded contents from secondary URI: {secondaryContents}");
}
case await Task.Delay(1000): {
Console.WriteLine("Failed to download from either URI within a second.");
}
}; How does select-default work? If none of the tasks (or awaitables?) are complete it immediately executes that block? |
@HaloFour That's right. Adding a default clause makes it synchronous. Otherwise it is asynchronous. |
Is this really a common enough problem to warrant language support? Personally, the number of times I have to do something like this is so rare that it's really not that big of a deal to fall back to using |
Very nice feature; I've got a lot of code which does the same thing doing semantically. At the moment the code needs to check the result of |
@gafter What about the the rest of the tasks? Are they going to be (semi-)automatically cancelled? This seems to be a frequent use case. |
@vladd Would you prefer the other tasks are cancelled? If so, how would you suggest I modify the proposal? |
@gafter I feel that sometimes cancelling the remaining tasks seems to be the right way. Example: the application uses tasks that track some UI events (await button click, await text input etc.), and wants to cancel the observations as soon as any event arrives (say, because any of them triggers some flow in the program logic). Another example is the mentioned above tasks with download and timeout: after the timeout is reached, continuing the download doesn't make sense any more. But I am pretty sure there is valid use of non-cancelling semantics as well. |
I'm not sure about the
|
Regarding the whole feature, couldn't it be replaced pretty easily by a small library? Something like (though probably with different naming): Select
.Case(client.DownloadStringTaskAsync(primaryUri),
primaryContents => Console.WriteLine($"Downloaded contents from primary URI: {primaryContents}"))
.Case(client.DownloadStringTaskAsync(secondaryUri),
secondaryContents => Console.WriteLine($"Downloaded contents from secondary URI: {secondaryContents}"))
.Case(Task.Delay(1000),
() => Console.WriteLine("Failed to download from either URI within a second."))
.Done(); |
I like the idea. Parallel async fits in nicely with sequential async that C# already has, but it really needs cancellation support. Something like |
@svick I'd have to agree. The behavior of this proposed syntax could be easily accomplished through a library without a lot of additional keystrokes to consume. A fluent API could also make it easier to extend to customize cancellation behavior, etc. |
@svick Hmm, I think so. You probably don't need a .Done() at the end, because you need an await Select
.Case(client.DownloadStringTaskAsync(primaryUri),
primaryContents => Console.WriteLine($"Downloaded contents from primary URI: {primaryContents}"))
.Case(client.DownloadStringTaskAsync(secondaryUri),
secondaryContents => Console.WriteLine($"Downloaded contents from secondary URI: {secondaryContents}"))
.Case(Task.Delay(1000),
() => Console.WriteLine("Failed to download from either URI within a second.")); On the other hand, a statement form would not require creating all these lambdas. |
@orthoxerox Perhaps |
try
{
var tasks = { client.DownloadStringTaskAsync(primaryUri),
client.DownloadStringTaskAsync(secondaryUri),
Task.Delay(1000) }
var res = await TaskFactory.StartNew(()=> Task.WaitAny( tasks ))
switch(res)
{
case 0:
var primaryContents = tasks[res].Result;
Console.WriteLine($"Downloaded contents from primary URI: {primaryContents}");
case 1:
var secondaryContents = task[res].Result;
Console.WriteLine($"Downloaded contents from secondary URI: {secondaryContents}");
default:
Console.WriteLine("Failed to download from either URI within a second.");
}
}
catch( exception e)
{
} |
@svick Well, the old good But I think the discussed feature would add more expressive power to the language. And selecting between parallel tasks is a concept deep enough to be reflected in the language. (By the way, the |
@gafter You could retain the brevity of the statement form if one could write expressions as part of lambdas. Something like
Or basically
|
@AdamSpeight2008 Unfortunately |
@gafter I meant |
@vladd Agreed, and it's a game of balance. But there are so many variations on the logic that you'd want to do with something like this that building the syntax into the language doesn't make a lot of sense since it would have to expand into something more complex than probably originally intended. Cancellation by itself is a good example, both in the sense of how to handle canceling the entire operation as well as what to do with the other tasks, not to mention to do it right you should be using a Not to mention, making it a library feature makes it easier to extend via extension methods. |
Right now I'm apprehensive regarding this proposal for the following reasons:
On a side note, cancellation support could be added to this:
With the following semantics:
|
Why not just any awaitable? Is the example the same as:
If so, what happens to the unwaited tasks? |
@paulomorgado Yes, those are the semantics when a default is present. As currently specified, nothing happens to the unawaited tasks. Do you think that is a problem? If so, what do you think should be done about it? |
@gafter, I was not particularly mentioning the default part but the fact that possibly faulted tasks are left unchecked. I was under the impression one should not do that. |
@paulomorgado I'm open to suggestions. |
I'm confused, @gafter. Are unobserved task exceptions OK now? |
FWIW, I get nervous about a language feature that makes it easy to ignore tasks. I'll think about it some more. |
@paulomorgado I was not intending to recommend anything about the way tasks should be used. This proposal should be modified to follow the most useful pattern, or if that doesn't make sense it should be abandoned. |
Thus the discussion, @gafter! |
I'd think that a language way of doing it would be akin to promoting an idiomatic approach. But I'd think that with cancellation and exception handling alone there isn't necessarily an idiomatic approach, what should be done depends highly on the types of tasks being executed and what needs to happen to the result. Between those two alone you'll end up with some nasty additional syntax. |
I don't think this feature is worth the complication. I work with C# daily and have done so for 10 years, and I have needed to do this thing once or twice, and it is not that hard to implement with |
I can't tell when you would use this feature. Why would you care to know which task finished first and to execute code based explicitly on it, and forget all the other tasks? You could try to cancel them, but they could be already finished by then too. You couldn't guarantee cancellation in any transactional way. I supposed you could use it when trying to parallel execute multiple redundant computations/fetches of data, and just use the first one that comes back, if you've got the resources to burn. I can't imagine this being common enough to warrant language syntax. But what do I know? Maybe it will some day be the basis of a new-fangled language all of it own, where triple redundant execution is all the rage. |
@erik-kallen, how come you've been working with |
@mattwar, one scenario would be have several providers for the same calculation and only caring for the first one that responds. |
@paulomorgado Yes, I agree. That's why I considered that in my prior post and could not think of a reason why that would be common enough to become a regularly used pattern. |
@mattwar, specially with the ignored tasks. |
@mattwar My experience says this pattern would be very useful in coding the typical business logic flow. Off the top of my head: you run a game, where the program logic waits until your player finishes the quest, the enemy player finishes the quest (this comes from network), or the player closes the window, whichever happens first. After that, you don't need to track any of the remaining conditions. Each of the conditions is easily representable by a Another quick example: the program needs to look up for the user input in local database (and present the result), in the network database (and present the result), or handle user's request cancellation (waiting for the cancellation is a |
@vladd I'd say that this approach is anything but typical. Even if you wanted to hit multiple sources like that you probably wouldn't want to discard every other result. In your second example I'd want to combine the local and network results to present them both to the user as they are available. My experience is that each form of problem like this is very specific and that a single idiomatic approach simply does not exist. Modifying the proposal above to allow the flexibility of handling exceptions or cancellation does not make a lot of sense. You'd end up with something even more confusing and used in such limited circumstances that developers are never going to attain a level of familiarity with it. |
I agree with cancellation support is needed. I often use methods like below instead of using using System.Linq;
using System.Threading;
using System.Threading.Tasks;
public delegate Task AsyncAction(CancellationToken ct);
public delegate Task<T> AsyncFunc<T>(CancellationToken ct);
public class TaskEx
{
public static async Task First(params AsyncAction[] actions)
{
var cts = new CancellationTokenSource();
var ct = cts.Token;
await Task.WhenAny(actions.Select(a => a(ct)));
cts.Cancel();
}
public static async Task<T> First<T>(params AsyncFunc<T>[] actions)
{
var cts = new CancellationTokenSource();
var ct = cts.Token;
var t = await Task.WhenAny(actions.Select(a => a(ct)));
cts.Cancel();
return t.Result;
}
} |
@ufcpp The form you put is essentially the syntax I suggested in my post above. 😄 |
@paulomorgado Thanks for your snarky comment, of course I haven't worked with async/await for that long (and I never said I have), but back in the day I did use the old APM (BeginXX / EndXX), which does achieve the same goal (very interesting problem in WebForms), reasonably often to solve the problem of performing multiple calls in parallell. Needing to wait for just the first of many calls to complete has been very uncommon at least for for me. Perhaps it could potentially have a little use to achieve timeouts, but it doesn't seem worth a language feature to me. |
I can think of a lot of more valuable things that the C# team could pursue with the same resources. I'm not even sure that this is a net positive change to the language given well known the "minus 100 points" model. The Go language has such a construct I believe. It might be worth checking out why they need/have that. That said the Go language concurrency design is rather primitive and seems to be a inferior to C# 5. Maybe not a good example. |
We propose a new statement form that waits for a number of tasks to complete, and executes a code block corresponding to the first one that completes.
Syntax
select
is a new context-sensitive keyword.Semantics
It is an error for a select-case to appear where the directly enclosing method is not
async
.For each select-case
Task
or some instance ofTask<>
.Task
, then there is no identifier in the select-case.Task<T>
for some typeT
, then the typeT
must be assignable to type.One may specify
var
to have the type of the variable inferred.At runtime, the expressions to the right of the
await
keywords in each select-case are evaluated, in order. If none of those tasks has completed:async
method is suspended until one of the tasks completes.Then (if the select-default block was not executed), for one of those tasks t that has completed:
OperationCanceledException
is thrown.We still need to define the definite-assignment and reachability rules for the select-statement.
Example
Thanks to @HaloFour
The text was updated successfully, but these errors were encountered: