Task.Run带参数?

113

我已经实现了一个简单的 Task.Factory.StartNew(),但我想知道如何改用 Task.Run() 来实现它?

以下是基本代码:

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

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

只有两个类似的东西:static Task Run(Action action)static Task Run(Func<Task> function),但都不能使用参数。

是的,我知道我可以为此创建一个简单的扩展方法,但我的主要问题是能否在单行中使用Task.Run()编写它


不清楚您希望参数的是什么。它将从哪里来?如果您已经有了它,只需在lambda表达式中捕获即可... - Jon Skeet
@JonSkeet rawData 是一个网络数据包,它有一个容器类(比如 DataPacket),我正在重用这个实例来减少垃圾回收压力。因此,如果我直接在 Task 中使用 rawData,它可能会在 Task 处理之前被改变。现在,我认为可以为它创建另一个 byte[] 实例。我认为这是对我来说最简单的解决方案。 - Thus Spoke Nomad
是的,如果您需要克隆字节数组,则可以克隆字节数组。拥有一个 Action<byte[]> 并不会改变这一点。 - Jon Skeet
这里有一些很好的解决方案来传递任务参数。 - Just Shadow
8个回答

152
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线程上运行,则其类型将为WindowsFormsSynchronizationContext

延续将在被发送回捕获的SynchronizationContext后运行——同样仅限默认情况下。因此,在await调用之后,您将回到开始的线程上。您可以以各种方式更改这一点,特别是使用ConfigureAwait。简而言之,该方法的其余部分将在Task在另一个线程上完成之后才会继续运行。但是,调用线程将继续并行运行,只有该方法的其余部分不会。

该方法等待完成其余部分可能是可取的,也可能不是。如果该方法中没有后来访问传递给Task的参数,您可能根本不想使用await

或者,您稍后在方法中使用这些参数。没有理由立即使用await,因为您可以安全地继续执行工作。请记住,您可以将返回的Task存储在变量中,并稍后在同一方法中await它——甚至在需要在执行一些其他工作后安全访问传递的参数时。再次强调,您不需要在运行Task时立即使用await

无论如何,使传递给Task.Run的参数在线程安全方面变得简单的方法是这样做:

您必须首先使用async修饰RunAsync

private async void RunAsync()

重要提示

如链接文档所述,最好使用标记为async的方法不应该返回 void。常见的例外是事件处理程序(如按钮点击等),它们必须返回 void。否则,我总是尝试在使用 async 时返回 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 等待任务,则可以避免将传入参数视为可能共享资源的情况,并避免多个线程同时修改某些内容的所有陷阱。此外,请注意闭包。我不会深入介绍它们,但链接的文章做得非常好。

关于 Run StartNew ,下面的代码是我认为最重要的要知道的。有合法的理由可选择其中任一种,两者都不过时或比另一种更好。只有在了解以下内容时,才能简单地用一个替换另一个:

//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完全不会阻塞,但我有时候会看到它与某种形式的阻塞一起使用。

“阻塞”使用引号因为你在技术上不能阻塞WinForms GUI线程。即使在WinForms GUI线程上使用lock,它仍然会传递消息,尽管您认为它已经“阻塞”了。它没有。

这可能会在非常罕见的情况下导致奇怪的问题。例如,其中一个原因是你永远不想在绘图时使用lock。但那是一个边缘和复杂的案例;但我曾看到它引起疯狂的问题。所以我为了完整起见而指出了它。


29
你不在等待 Task.Run(() => MethodWithParameter(param));。这意味着,如果 paramTask.Run 之后被修改,那么在 MethodWithParameter 中可能会得到意外的结果。 - Alexandre Severino
14
为什么这个错误的答案被接受了,它根本不等同于传递状态对象。 - Egor Pavlikhin
8
@Zer0 在Task.Factory.StartNew中,状态对象是第二个参数https://msdn.microsoft.com/en-us/library/dd321456(v=vs.110).aspx,它保存了在调用StartNew时对象的值,而您的答案创建了一个闭包,保持对引用的引用(如果param的值在任务运行之前发生更改,则在任务中也会发生更改),因此您的代码与问题所要求的根本不等价。答案真正的意思是,无法使用Task.Run()编写它。 - Egor Pavlikhin
3
也许应该阅读源代码。其中一个传递状态对象,另一个则不传递。这就是我从一开始所说的。Task.Run 不是 Task.Factory.StartNew 的简写。状态对象版本出于历史原因而存在,但它仍然存在,并且有时会表现出不同的行为,因此人们应该注意这一点。 - Egor Pavlikhin
3
阅读Toub的文章,我想强调这句话:“您可以使用接受对象状态的重载,对于性能敏感的代码路径,可以使用该重载来避免闭包和相应的分配”。当考虑使用Task.Run而非StartNew时,我认为这正是@Zero所暗示的。 - davidcarr
显示剩余11条评论

40
使用变量捕获来“传递”参数。

使用变量捕获来“传递”参数。

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

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


11
考虑到变量可能会在调用“Task.Run”后更改这一重要事实,+1。 - Alexandre Severino
2
这会有什么帮助吗? 如果您在任务线程内部使用x,并且x是一个对象的引用,如果该对象在任务线程运行时同时被修改,可能会导致混乱。 - ovi
1
@Ovi-WanKenobi 是的,但这不是这个问题的重点。问题是如何传递参数。如果您将对象的引用作为参数传递给普通函数,那么您也会遇到完全相同的问题。 - Scott Chamberlain
是的,这个不起作用。我的任务在调用线程中没有对x的引用。我只得到了null。 - David Price
通过捕获传递参数会带来自己的问题。特别是内存泄漏和内存压力的问题。尤其是当你尝试扩展时。(更多细节请参见“8种可能导致内存泄漏的方式”)。 - RashadRivera

16

从现在开始,您也可以:

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

3
这是最好的答案,因为它允许传递一个状态,并防止Kaden Burgart的答案中提到的可能发生的情况。例如,如果需要将IDisposable对象传递到任务委托以解决ReSharper警告“在外部范围中处置捕获的变量”的问题,则可以很好地完成这项工作。与流行观点相反,在需要传递状态的情况下,使用Task.Factory.StartNew而不是Task.Run没有任何问题。请参见这里 - Neo
1
虽然这是一个好的指向方向,但上面的例子将无法编译。StartNew需要Action<object>作为参数... - Kirsan
1
@Kirsan 我已经成功编译了,只需将 Action<int> 的签名更改为 Action<object?> 在 .Net 6 中。 - Jason D

10

我知道这是一个旧帖子,但我想分享一下我的解决方案,因为被接受的帖子仍然存在问题。

问题:

如Alexandre Severino所指出的,如果param(在下面的函数中)在函数调用后不久发生变化,则可能会在MethodWithParameter中得到一些意外行为。

Task.Run(() => MethodWithParameter(param)); 

我的解决方案:

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

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

这使我能够安全地异步使用参数,尽管参数在启动任务后非常快地更改(这会导致已发布的解决方案出现问题)。
使用这种方法,param(值类型)会传递其值,因此即使异步方法在param更改后运行,p将具有param在此代码行运行时的任何值。

8
我非常期待有人能想出一种更简明且开销更小的方式来完成这个任务。不可否认,目前这种方法相当丑陋。 - Kaden Burgart
6
请看这里:var localParam = param; await Task.Run(() => MethodWithParam(localParam)); - Stephen Cleary
2
顺便提一下,斯蒂芬在一年半前已经在他的回答中讨论过这个问题了。 - Servy
1
@Servy:那实际上是Scott的回答。我没有回答这个问题。 - Stephen Cleary
实际上,Scott的答案对我并不适用,因为我是在for循环中运行这个程序的。局部参数将在下一次迭代中被重置。 我发布的答案的区别在于,参数被复制到lambda表达式的作用域中,因此变量立即安全。在Scott的答案中,参数仍然在同一作用域中,因此在调用该行和执行异步函数之间仍可能发生更改。 - Kaden Burgart

4

只需使用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;
}

2
如果您按照以下方式操作,请注意闭包:for(int rawData = 0; rawData < 10; ++rawData) { Task.Run(() => { Console.WriteLine(rawData); } ) }与OP的StartNew示例中传递rawData不会表现相同。 - Scott Chamberlain
@ScottChamberlain - 这似乎是一个不同的例子😉我希望大多数人都能理解闭包中的lambda值的概念。 - Travis J
3
如果之前的评论让你感到困惑,请查看Eric Lipper在这个主题上的博客:http://blogs.msdn.com/b/ericlippert/archive/2009/11/12/closing-over-the-loop-variable-considered-harmful.aspx。它很好地解释了为什么会发生这种情况。 - Travis J

4

这个想法是避免使用像上面那样的信号。 将int值注入结构体中可以防止这些值在结构体中发生变化。 我曾经遇到过以下问题:循环变量i在调用DoSomething(i)之前就已经改变了(在循环结束时i被递增,然后()=> DoSomething(i,ii)被调用)。使用结构体后,这种情况不再发生。这是一个非常棘手的错误: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));
}

2
虽然这可能回答了问题,但它被标记为需要审核。没有解释的答案通常被认为是低质量的。请提供一些评论说明为什么这是正确的答案。 - Dan

4

不清楚原始问题是否与我遇到的问题相同:在循环内计算时希望最大化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();
}

0

有另一种方法可以做到这一点。我发现它很有用。

int param;
ThreadPool.QueueUserWorkItem(someMethod, param);
void someMethod(object parameter){
    var param = (int) parameter;
    // do the job
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接