Consider relaxing restrictions of async methods in blocks that do not contain `await`. (VS 17.11, .NET 9)
Async methods have several restrictions such as not allowing ref and ref-like/span locals.
The reason is the technical difficulties that may arise if such locals need to be hoisted. In a code that tries to mix await and span this could be inconvenient.
Besides there can be patterns which are obviously safe - In particular, in a block that does not contain await expressions, such locals would never be hoisted and therefore we could relax the restrictions.
Are you refering to methods that directly return a Task without awaiting it?
There is no need to mark that method as async. You can simply return task.
public async Task WaitForAll()
{
return await Task.WhenAll(src);
}
Can be rewritten as
public Task WaitForAll()
{
return Task.WhenAll(src);
}
Does this solve your problem?
if a local variable is not accessed after yielding some results or awaiting for results that can be also allowed. For example
public async Task WaitForAll()
{
ref x = ...
// work with x
var result= await something;
// if x is not accessed here its ok!
}
@MkazemAkhgary
Are you refering to methods that directly return a Task without awaiting it?
Does this solve your problem?
No, I don't think Vladimir is lost.
An example:
async Task<int> Test()
{
await Task.Yield();
int x = 42;
// the following block does not contain await expressions
// ref and ref-like locals are safe to use, but currently disallowed
{
Span<int> sp = stackalloc int[1];
ref int r0 = ref sp[0];
return r0 + x;
}
}
A workaround that I have seen in use. But I think it just proves the point - some patterns are clearly safe and the language stands in the way.
async Task<int> Test1()
{
await Task.Yield();
int x = 42;
// one possible workaround is a local function, but that is not nice
return block();
int block()
{
Span<int> sp = stackalloc int[1];
ref int r0 = ref sp[0];
return r0 + x;
}
}
Keying off "before/after await" is tricky in the presence of loops or other nontrivial control flow. Lexically "after" does not imply it will not need to be hoisted.
"In a block with no awaits" seems much easier to reason about.
I see, sounds reasonable. its kind of like how await is forbindden in unsafe context since c# cant get pointer of fields. But unsafe block without awaits is ok.
"In a block with no awaits" seems much easier to reason about.
It also means that you have to change your reasonable code just to make the compiler happy. I think that "fighting with the compiler" should be avoided if possible.
Something like "there can be no await between declaration and usage of a stack-only variable" sounds much easier on the user to me. But yes, it has to take loops and similar constructs into account when evaluating what "between" means.
If it easy to do it would also be nice if you can allow expressions that return Span to be legal in async methods (it may already be true). basically if
- Span
ReturnsSpan(); - void ConsumesSpan(Span
); then I should be able to do
async Task<int> Test1()
{
await Task.Yield();
ConsumesSpan(ReturnsSpan());
await Task.Yield();
}
This avoids needing to make a explicit block {} for the trivial case where there are no local variables.
Expressions/values are allowed. Locals are not allowed. The thing above should work fine.
Great!
There is an obscure case where expressions are not allowed - when we have to stack spill them, but that is very rare.
async Task<int> Test1()
{
await Task.Yield();
ConsumesSpanAndStuff(ReturnsSpan(), await SomethingAsync());
await Task.Yield();
}
In the case above you would need to take the await out of the arg list. We cannot do that because of possible sideeffects that SomethingAsync can produce or observe.
Otherwise "flowing" spans is ok in async, storing into locals - no.
Related: https://github.com/dotnet/roslyn/issues/40213