Task.Run 有参数?

Task.Run with Parameter(s)?

我正在做一个多任务网络项目,我是 Threading.Tasks 的新手。我实现了一个简单的 Task.Factory.StartNew(),我想知道如何使用 Task.Run()?

基本代码如下:

Task.Factory.StartNew(new Action<object>(
(x) =>
{
    // Do something with 'x'
}), rawData);

我在 对象浏览器 中查看了 System.Threading.Tasks.Task,但找不到类似 Action<T> 的参数。只有 Action 接受 void 参数而没有 type.

只有 2 个相似的东西:static Task Run(Action action)static Task Run(Func<Task> function) 但两者不能 post 参数。

是的,我知道我可以为它创建一个简单的扩展方法,但我的主要问题是我们可以将它写在单行Task.Run() 上吗?

private void RunAsync()
{
    //Beware of closures.  String is immutable.
    string param = "Hi";
    Task.Run(() => MethodWithParameter(param));
}

private void MethodWithParameter(string param)
{
    //Do stuff
}

编辑

由于大众需求,我必须注意启动的 Task 将 运行 与调用线程并行。假设默认 TaskScheduler 这将使用 .NET ThreadPool。无论如何,这意味着您需要考虑传递给 Task 的任何参数,因为它们可能同时被多个线程访问,从而使它们共享状态。这包括在调用线程上访问它们。

在我上面的代码中,这种情况完全没有实际意义。字符串是不可变的。这就是为什么我用它们作为例子。但是假设您没有使用 String...

一种解决方案是使用 asyncawait。默认情况下,这将捕获调用线程的 SynchronizationContext,并在调用 await 后为方法的其余部分创建一个延续,并将其附加到创建的 Task。如果此方法在 WinForms GUI 线程上 运行ning,它将是 WindowsFormsSynchronizationContext.

类型

在回发到捕获的 SynchronizationContext 后,延续将 运行 - 再次仅在默认情况下。因此,您将回到 await 调用后开始的线程。您可以通过多种方式更改此设置,特别是使用 ConfigureAwait。简而言之,该方法的其余部分将不会继续,直到 after Task 在另一个线程上完成。但是调用线程将继续并行 运行,而不是方法的其余部分。

等待完成 运行方法的其余部分可能需要也可能不需要。如果稍后该方法中没有任何内容访问传递给 Task 的参数,您可能根本不想使用 await

或者您可能稍后会在该方法中使用这些参数。没有理由立即 await,因为您可以继续安全地工作。请记住,您可以将 Task returned 存储在变量中,稍后再将 await 存储在变量中 - 即使是在相同的方法中。例如,一旦您需要在完成一些其他工作后安全地访问传递的参数。同样,您 不需要 Taskawait 就在您 运行 的时候。

无论如何,关于传递给 Task.Run 的参数,使此线程安全的一种简单方法是:

你必须先用async修饰RunAsync:

private async void RunAsync()

重要提示

最好标记为 async 的方法不应 return 无效,如链接文档所述。常见的例外是事件处理程序,例如按钮点击等。他们必须 return 作废。否则我总是在使用 async 时尝试 return 一个 TaskTask<TResult>。出于多种原因,这是一种很好的做法。

现在您可以 await 运行 如下设置 Task。没有 async.

就不能使用 await
await Task.Run(() => MethodWithParameter(param));
//Code here and below in the same method will not run until AFTER the above task has completed in one fashion or another

因此,一般来说,如果您 await 任务,您可以避免将传入的参数视为潜在的共享资源,避免同时从多个线程修改某些内容的所有陷阱。另外,请注意 closures。我不会深入介绍这些内容,但链接的文章做得很好。

关于 RunStartNew 下面的代码我觉得最重要的是要知道,真的。使用其中任何一种都有正当的理由,两者都没有过时或比另一种“更好”。请注意,除非您了解这一点,否则简单地将一个替换为另一个是一个非常糟糕的主意:

//These are exactly the same
Task.Run(x); 
Task.Factory.StartNew(x, CancellationToken.None,
TaskCreationOptions.DenyChildAttach, TaskScheduler.Default);

//These are also exactly the same
Task.Factory.StartNew(x);
Task.Factory.StartNew(x, CancellationToken.None, 
TaskCreationOptions.None, TaskScheduler.Current);

旁注

有点偏离主题,但在 WinForms GUI 线程上使用任何类型的“阻塞”时要小心,因为它被标记为 [STAThread]。使用 await 根本不会阻塞,但有时我确实看到它与某种阻塞一起使用。

“阻止”在引号中是因为您在技术上 cannot block the WinForms GUI thread。是的,如果您在 WinForms GUI 线程上使用 lock,它 仍会 发送消息,尽管您认为它已被“阻止”。不是。

在极少数情况下,这可能会导致奇怪的问题。例如,您不想在绘画时使用 lock 的原因之一。但这是一个边缘而复杂的案例;但是我已经看到它会导致疯狂的问题。所以为了完整起见,我把它记下来了。

使用变量捕获 "pass in" 参数。

var x = rawData;
Task.Run(() =>
{
    // Do something with 'x'
});

您也可以直接使用 rawData 但您必须小心,如果您在任务之外更改 rawData 的值(例如 for 循环中的迭代器)它还会更改任务内部的值。

只需使用Task.Run

var task = Task.Run(() =>
{
    //this will already share scope with rawData, no need to use a placeholder
});

或者,如果您想在方法中使用它并稍后等待任务

public Task<T> SomethingAsync<T>()
{
    var task = Task.Run(() =>
    {
        //presumably do something which takes a few ms here
        //this will share scope with any passed parameters in the method
        return default(T);
    });

    return task;
}

从现在开始你还可以:

Action<int> action = (o) => Thread.Sleep(o);
int param = 10;
await new TaskFactory().StartNew(action, param)

我知道这是一个旧线程,但我想分享一个我最终不得不使用的解决方案,因为已接受的 post 仍然存在问题。

问题:

正如 Alexandre Severino 所指出的,如果 param(在下面的函数中)在函数调用后不久发生变化,您可能会在 MethodWithParameter.

中出现一些意外行为
Task.Run(() => MethodWithParameter(param)); 

我的解决方案:

为了解决这个问题,我最终编写了类似于以下代码行的内容:

(new Func<T, Task>(async (p) => await Task.Run(() => MethodWithParam(p)))).Invoke(param);

这使我能够安全地异步使用参数,尽管参数在启动任务后变化非常快(这导致 posted 解决方案出现问题)。

使用这种方法,param(值类型)获取传入的值,因此即使异步方法在 param 更改后运行,p 将具有任何值 param 有当这行代码 运行.

不清楚最初的问题是否与我遇到的问题相同:想要在循环内最大 CPU 个计算线程,同时保留迭代器的值并保持内联以避免将大量变量传递给工作人员功能。

for (int i = 0; i < 300; i++)
{
    Task.Run(() => {
        var x = ComputeStuff(datavector, i); // value of i was incorrect
        var y = ComputeMoreStuff(x);
        // ...
    });
}

我通过更改外部迭代器并使用门将其值本地化来实现它。

for (int ii = 0; ii < 300; ii++)
{
    System.Threading.CountdownEvent handoff = new System.Threading.CountdownEvent(1);
    Task.Run(() => {
        int i = ii;
        handoff.Signal();

        var x = ComputeStuff(datavector, i);
        var y = ComputeMoreStuff(x);
        // ...

    });
    handoff.Wait();
}

想法是避免使用像上面那样的信号。 将 int 值泵入结构可防止这些值发生变化(在结构中)。 我有以下问题:在调用 DoSomething(i) 之前循环变量 i 会更改(在调用 ()=> DoSomething(i,ii) 之前循环结束时 i 递增)。有了结构,它就不会再发生了。讨厌的 bug:DoSomething(i, ii) 看起来不错,但不确定它是否每次都以不同的 i 值调用(或者只是 100 次 i=100),因此 ->结构

struct Job { public int P1; public int P2; }
…
for (int i = 0; i < 100; i++) {
    var job = new Job { P1 = i, P2 = i * i}; // structs immutable...
    Task.Run(() => DoSomething(job));
}