如何创建一个总是产生的任务?
How to create a Task which always yields?
与 Task.Wait()
或 Task.Result
相比,await
在 C# 5 中使用 Task
可防止执行等待的线程闲置。相反,使用 await
关键字的方法需要是 async
,这样 await
的调用只会使 return 的方法成为一个新任务,代表 async
的执行=18=]方法。
但是当 await
的 Task
在 async
方法再次收到 CPU 时间之前完成时,await
识别 Task
完成,因此 async
方法将 return Task
对象仅在稍后的时间。在某些情况下,这可能会晚于可接受的时间,因为开发人员假设 await
总是推迟他的 async
方法中的后续语句可能是一个常见的错误。
错误的 async
方法的结构可能如下所示:
async Task doSthAsync()
{
var a = await getSthAsync();
// perform a long operation
}
然后有时 doSthAsync()
会 return Task
很久以后才会
我知道应该这样写:
async Task doSthAsync()
{
var a = await getSthAsync();
await Task.Run(() =>
{
// perform a long operation
};
}
...或者那个:
async Task doSthAsync()
{
var a = await getSthAsync();
await Task.Yield();
// perform a long operation
}
但我觉得最后两个模式不漂亮,想防止错误发生。我正在开发一个提供 getSthAsync
的框架,第一个结构应该是通用的。所以 getSthAsync
应该 return 一个 Awaitable,它总是像 Task.Yield()
编辑的 YieldAwaitable
return 那样产生。
不幸的是,任务并行库提供的大多数功能(如 Task.WhenAll(IEnumerable<Task> tasks)
)仅在 Task
上运行,因此 getSthAsync
的结果应该是 Task
。
那么有没有可能 return 一个 Task
总是 yield?
首先,异步方法的使用者不应该假设它会 "yield" 因为这与异步方法无关。如果消费者需要确保卸载到另一个线程,他们应该使用 Task.Run
来强制执行。
其次,我不明白使用 Task.Run
或 Task.Yield
有什么问题,因为它在 returns 和 Task
的异步方法中使用不是 YieldAwaitable
.
如果你想创建一个行为类似于 YieldAwaitable
的 Task
,你可以在异步方法中使用 Task.Yield
:
async Task Yield()
{
await Task.Yield();
}
编辑:
正如评论中提到的,这有一个竞争条件,它可能并不总是屈服。这种竞争条件是 Task
和 TaskAwaiter
的实现方式所固有的。为避免这种情况,您可以创建自己的 Task
和 TaskAwaiter
:
public class YieldTask : Task
{
public YieldTask() : base(() => {})
{
Start(TaskScheduler.Default);
}
public new TaskAwaiterWrapper GetAwaiter() => new TaskAwaiterWrapper(base.GetAwaiter());
}
public struct TaskAwaiterWrapper : INotifyCompletion
{
private TaskAwaiter _taskAwaiter;
public TaskAwaiterWrapper(TaskAwaiter taskAwaiter)
{
_taskAwaiter = taskAwaiter;
}
public bool IsCompleted => false;
public void OnCompleted(Action continuation) => _taskAwaiter.OnCompleted(continuation);
public void GetResult() => _taskAwaiter.GetResult();
}
这将创建一个 always 产生的任务,因为 IsCompleted
always returns false。可以这样使用:
public static readonly YieldTask YieldTask = new YieldTask();
private static async Task MainAsync()
{
await YieldTask;
// something
}
注意:我强烈反对任何人实际做这种事情。
这是 YieldTask
的完善版本:
public class YieldTask : Task
{
public YieldTask() : base(() => { },
TaskCreationOptions.RunContinuationsAsynchronously)
=> RunSynchronously();
public new YieldAwaitable.YieldAwaiter GetAwaiter()
=> default;
public new YieldAwaitable ConfigureAwait(bool continueOnCapturedContext)
{
if (!continueOnCapturedContext) throw new NotSupportedException();
return default;
}
}
YieldTask
创建后立即完成,但其等待者另有说明。 GetAwaiter().IsCompleted
总是returnsfalse
。这个恶作剧使得 await
运算符每次等待此任务时都会触发所需的异步切换。实际上创建多个 YieldTask
实例是多余的。单例也可以。
虽然这种方法存在问题。 Task
class 的底层方法不是虚拟的,用 new
修饰符隐藏它们意味着多态性不起作用。如果将 YieldTask
实例存储到 Task
变量,您将获得默认任务行为。这对我的用例来说是一个相当大的缺点,但我看不到任何解决方案。
与 Task.Wait()
或 Task.Result
相比,await
在 C# 5 中使用 Task
可防止执行等待的线程闲置。相反,使用 await
关键字的方法需要是 async
,这样 await
的调用只会使 return 的方法成为一个新任务,代表 async
的执行=18=]方法。
但是当 await
的 Task
在 async
方法再次收到 CPU 时间之前完成时,await
识别 Task
完成,因此 async
方法将 return Task
对象仅在稍后的时间。在某些情况下,这可能会晚于可接受的时间,因为开发人员假设 await
总是推迟他的 async
方法中的后续语句可能是一个常见的错误。
错误的 async
方法的结构可能如下所示:
async Task doSthAsync()
{
var a = await getSthAsync();
// perform a long operation
}
然后有时 doSthAsync()
会 return Task
很久以后才会
我知道应该这样写:
async Task doSthAsync()
{
var a = await getSthAsync();
await Task.Run(() =>
{
// perform a long operation
};
}
...或者那个:
async Task doSthAsync()
{
var a = await getSthAsync();
await Task.Yield();
// perform a long operation
}
但我觉得最后两个模式不漂亮,想防止错误发生。我正在开发一个提供 getSthAsync
的框架,第一个结构应该是通用的。所以 getSthAsync
应该 return 一个 Awaitable,它总是像 Task.Yield()
编辑的 YieldAwaitable
return 那样产生。
不幸的是,任务并行库提供的大多数功能(如 Task.WhenAll(IEnumerable<Task> tasks)
)仅在 Task
上运行,因此 getSthAsync
的结果应该是 Task
。
那么有没有可能 return 一个 Task
总是 yield?
首先,异步方法的使用者不应该假设它会 "yield" 因为这与异步方法无关。如果消费者需要确保卸载到另一个线程,他们应该使用 Task.Run
来强制执行。
其次,我不明白使用 Task.Run
或 Task.Yield
有什么问题,因为它在 returns 和 Task
的异步方法中使用不是 YieldAwaitable
.
如果你想创建一个行为类似于 YieldAwaitable
的 Task
,你可以在异步方法中使用 Task.Yield
:
async Task Yield()
{
await Task.Yield();
}
编辑:
正如评论中提到的,这有一个竞争条件,它可能并不总是屈服。这种竞争条件是 Task
和 TaskAwaiter
的实现方式所固有的。为避免这种情况,您可以创建自己的 Task
和 TaskAwaiter
:
public class YieldTask : Task
{
public YieldTask() : base(() => {})
{
Start(TaskScheduler.Default);
}
public new TaskAwaiterWrapper GetAwaiter() => new TaskAwaiterWrapper(base.GetAwaiter());
}
public struct TaskAwaiterWrapper : INotifyCompletion
{
private TaskAwaiter _taskAwaiter;
public TaskAwaiterWrapper(TaskAwaiter taskAwaiter)
{
_taskAwaiter = taskAwaiter;
}
public bool IsCompleted => false;
public void OnCompleted(Action continuation) => _taskAwaiter.OnCompleted(continuation);
public void GetResult() => _taskAwaiter.GetResult();
}
这将创建一个 always 产生的任务,因为 IsCompleted
always returns false。可以这样使用:
public static readonly YieldTask YieldTask = new YieldTask();
private static async Task MainAsync()
{
await YieldTask;
// something
}
注意:我强烈反对任何人实际做这种事情。
这是 YieldTask
的完善版本:
public class YieldTask : Task
{
public YieldTask() : base(() => { },
TaskCreationOptions.RunContinuationsAsynchronously)
=> RunSynchronously();
public new YieldAwaitable.YieldAwaiter GetAwaiter()
=> default;
public new YieldAwaitable ConfigureAwait(bool continueOnCapturedContext)
{
if (!continueOnCapturedContext) throw new NotSupportedException();
return default;
}
}
YieldTask
创建后立即完成,但其等待者另有说明。 GetAwaiter().IsCompleted
总是returnsfalse
。这个恶作剧使得 await
运算符每次等待此任务时都会触发所需的异步切换。实际上创建多个 YieldTask
实例是多余的。单例也可以。
虽然这种方法存在问题。 Task
class 的底层方法不是虚拟的,用 new
修饰符隐藏它们意味着多态性不起作用。如果将 YieldTask
实例存储到 Task
变量,您将获得默认任务行为。这对我的用例来说是一个相当大的缺点,但我看不到任何解决方案。