Tarefa.Run com parâmetro (s)?

87

Estou trabalhando em um projeto de rede multitarefa e sou novo Threading.Tasks. Implementei um simples Task.Factory.StartNew()e gostaria de saber como posso fazer isso Task.Run()?

Aqui está o código básico:

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

Eu olhei System.Threading.Tasks.Taskno Pesquisador de objetos e não consegui encontrar um Action<T>parâmetro semelhante. Existe apenas Actionaquele que leva voidparâmetro e nenhum tipo .

Existem apenas 2 coisas semelhantes: static Task Run(Action action)e static Task Run(Func<Task> function)mas não pode postar parâmetro (s) com ambos.

Sim, eu sei que posso criar um método de extensão simples para ele, mas minha principal questão é podemos escrevê-lo em uma única linha com Task.Run()?

MFatihMAR
fonte
Não está claro qual você deseja que o valor do parâmetro seja. De onde viria? Se você já tiver, apenas capture-o na expressão lambda ...
Jon Skeet
@JonSkeet rawDataé um pacote de dados de rede que tem uma classe de contêiner (como DataPacket) e estou reutilizando essa instância para reduzir a pressão do GC. Portanto, se eu usar rawDatadiretamente no Task, ele pode (provavelmente) ser alterado antes de ser Taskmanuseado. Agora, acho que posso criar outra byte[]instância para ele. Acho que é a solução mais simples para mim.
MFatihMAR
Sim, se você precisa clonar a matriz de bytes, você clona a matriz de bytes. Ter um Action<byte[]>não muda isso.
Jon Skeet de
Aqui estão algumas boas soluções para passar parâmetros para uma tarefa.
Just Shadow

Respostas:

116
private void RunAsync()
{
    string param = "Hi";
    Task.Run(() => MethodWithParameter(param));
}

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

Editar

Devido à demanda popular, devo observar que o Taskiniciado será executado em paralelo com o thread de chamada. Assumindo o padrão TaskScheduler, usará o .NET ThreadPool. De qualquer forma, isso significa que você precisa levar em conta quaisquer parâmetros que estão sendo passados ​​para o Taskcomo potencialmente sendo acessados ​​por vários threads de uma vez, tornando-os um estado compartilhado. Isso inclui acessá-los no thread de chamada.

No meu código acima, esse caso é inteiramente discutível. Strings são imutáveis. É por isso que os usei como exemplo. Mas digamos que você não esteja usando um String...

Uma solução é usar asynce await. Isso, por padrão, irá capturar o SynchronizationContextdo thread de chamada e criar uma continuação para o resto do método após a chamada awaite anexá-lo ao criado Task. Se esse método estiver sendo executado no thread da GUI do WinForms, ele será do tipo WindowsFormsSynchronizationContext.

A continuação será executada após ser postada de volta no capturado SynchronizationContext- novamente apenas por padrão. Assim, você estará de volta ao tópico com o qual começou após a awaitligação. Você pode alterar isso de várias maneiras, principalmente usando ConfigureAwait. Em suma, o resto do que o método não continuará até que após a Taskcompletou em outro segmento. Mas o thread de chamada continuará a ser executado em paralelo, mas não o resto do método.

Essa espera para concluir a execução do resto do método pode ou não ser desejável. Se nada nesse método acessar posteriormente os parâmetros passados ​​para o, Taskvocê pode não querer mais usar await.

Ou talvez você use esses parâmetros muito mais tarde no método. Não há razão para isso awaitimediatamente, pois você pode continuar fazendo o trabalho com segurança. Lembre-se de que você pode armazenar o Taskretornado em uma variável e awaitposteriormente - até mesmo no mesmo método. Por exemplo, uma vez que você precisa acessar os parâmetros passados ​​com segurança depois de fazer um monte de outro trabalho. Novamente, você não precisa estar awaità Taskdireita ao executá-lo.

De qualquer forma, uma maneira simples de tornar esse thread-safe com relação aos parâmetros passados ​​para Task.Runé fazer isso:

Você deve primeiro decorar RunAsynccom async:

private async void RunAsync()

Nota importante

De preferência, o método marcado não deve retornar void, como a documentação vinculada menciona. A exceção comum são os manipuladores de eventos, como cliques em botões e outros. Eles devem retornar vazios. Caso contrário, sempre tento retornar um ou ao usar . É uma boa prática por vários motivos.async TaskTask<TResult>async

Agora você pode awaitexecutar o exemplo Taskabaixo. Você não pode usar awaitsem async.

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

Portanto, em geral, se você fizer awaita tarefa, poderá evitar tratar os parâmetros passados ​​como um recurso potencialmente compartilhado com todas as armadilhas de modificar algo de vários threads de uma vez. Além disso, tome cuidado com os fechamentos . Não vou cobri-los em profundidade, mas o artigo vinculado faz um ótimo trabalho.

Nota

Um pouco fora do assunto, mas tome cuidado ao usar qualquer tipo de "bloqueio" no thread da GUI do WinForms devido a ele estar marcado com [STAThread]. O uso awaitnão bloqueia de forma alguma, mas às vezes vejo que é usado em conjunto com algum tipo de bloqueio.

"Block" está entre aspas porque você tecnicamente não pode bloquear o thread da GUI do WinForms . Sim, se você usar locko thread da GUI do WinForms, ele ainda enviará mensagens, apesar de você pensar que está "bloqueado". Não é.

Isso pode causar problemas bizarros em casos muito raros. Uma das razões pelas quais você nunca quer usar um lockao pintar, por exemplo. Mas esse é um caso marginal e complexo; no entanto, eu já vi isso causar problemas malucos. Então, eu anotei isso por uma questão de integridade.

Zer0
fonte
21
Você não está esperando Task.Run(() => MethodWithParameter(param));. O que significa que se paramfor modificado após o Task.Run, você pode ter resultados inesperados no MethodWithParameter.
Alexandre Severino
7
Por que essa resposta é aceita quando está errada. Não é de forma alguma equivalente a passar um objeto de estado.
Egor Pavlikhin
6
@ Zer0 um objeto de estado é o segundo parâmetro em Task.Factory.StartNew msdn.microsoft.com/en-us/library/dd321456(v=vs.110).aspx e salva o valor do objeto no momento do ligue para StartNew, enquanto sua resposta cria um encerramento, que mantém a referência (se o valor de param mudar antes da tarefa ser executada, ele também mudará na tarefa), então seu código não é nada equivalente ao que a pergunta estava perguntando . A resposta realmente é que não há como escrevê-lo com Task.Run ().
Egor Pavlikhin
2
@ Zer0 para structs Task.Run with closure e Task.Factory.StartNew com o segundo parâmetro (que não é o mesmo que Task.Run por seu link) terá um comportamento diferente, pois no último caso uma cópia será feita. Meu erro foi me referir a objetos em geral no comentário original, o que eu quis dizer é que eles não são totalmente equivalentes.
Egor Pavlikhin
3
Lendo o artigo de Toub, destacarei esta frase "Você pode usar sobrecargas que aceitam o estado do objeto, que para caminhos de código sensíveis ao desempenho podem ser usados ​​para evitar fechamentos e as alocações correspondentes". Acho que é isso que @Zero está sugerindo quando se considera o uso de Task.Run sobre StartNew.
davidcarr
35

Use a captura de variáveis ​​para "passar" os parâmetros.

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

Você também pode usar rawDatadiretamente, mas deve ter cuidado, se alterar o valor de rawDatafora de uma tarefa (por exemplo, um iterador em um forloop), também mudará o valor dentro da tarefa.

Scott Chamberlain
fonte
11
1 por levar em consideração o fato importante de que a variável pode ser alterada logo após a chamada Task.Run.
Alexandre Severino
1
como isso vai ajudar? se você usar x dentro do thread de tarefas e x for uma referência a um objeto, e se o objeto for modificado ao mesmo tempo quando o thread de tarefas estiver em execução, isso pode causar confusão.
Ovi
1
@ Ovi-WanKenobi Sim, mas não é disso que se trata esta pergunta. Era como passar um parâmetro. Se você passou uma referência a um objeto como um parâmetro para uma função normal, você teria exatamente o mesmo problema aí também.
Scott Chamberlain
Sim, isso não funciona. Minha tarefa não tem referência a x no segmento de chamada. Acabei de receber null.
David Price
7

Sei que este é um tópico antigo, mas queria compartilhar uma solução que acabei tendo que usar, pois a postagem aceita ainda tem um problema.

O problema:

Conforme apontado por Alexandre Severino, se param(na função abaixo) mudar logo após a chamada da função, você pode obter algum comportamento inesperado em MethodWithParameter.

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

Minha solução:

Para explicar isso, acabei escrevendo algo mais parecido com a seguinte linha de código:

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

Isso me permitiu usar o parâmetro com segurança de forma assíncrona, apesar do fato de que o parâmetro mudou muito rapidamente após iniciar a tarefa (o que causou problemas com a solução postada).

Usando essa abordagem, param(tipo de valor) obtém seu valor transmitido, portanto, mesmo se o método assíncrono for executado após as paramalterações, pterá o valor que paramtinha quando esta linha de código foi executada.

Kaden Burgart
fonte
5
Aguardo ansiosamente qualquer um que possa pensar em uma maneira de fazer isso de forma mais legível com menos sobrecarga. Isso é reconhecidamente muito feio.
Kaden Burgart
5
Aqui está:var localParam = param; await Task.Run(() => MethodWithParam(localParam));
Stephen Cleary
1
Que, aliás, Stephen já discutiu em sua resposta, um ano e meio atrás.
Servy
1
@Servy: Essa foi a resposta de Scott , na verdade. Eu não respondi esta.
Stephen Cleary
A resposta de Scott não teria funcionado para mim na verdade, pois eu estava executando isso em um loop for. O parâmetro local teria sido redefinido na próxima iteração. A diferença na resposta que postei é que o parâmetro é copiado para o escopo da expressão lambda, portanto, a variável é imediatamente segura. Na resposta de Scott, o parâmetro ainda está no mesmo escopo, então ele ainda pode mudar entre chamar a linha e executar a função assíncrona.
Kaden Burgart
6

A partir de agora você também pode:

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

Basta usar Task.Run

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

Ou, se você gostaria de usá-lo em um método e aguardar a tarefa mais tarde

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;
}
Travis J
fonte
1
Só tome cuidado com os fechamentos, se você fizer dessa forma for(int rawData = 0; rawData < 10; ++rawData) { Task.Run(() => { Console.WriteLine(rawData); } ) }, não vai se comportar da mesma forma como se rawDatafosse passado como no exemplo StartNew do OP.
Scott Chamberlain
@ScottChamberlain - Esse parece um exemplo diferente;) Espero que a maioria das pessoas entenda sobre o fechamento sobre valores lambda.
Travis J
3
E se esses comentários anteriores não faziam sentido, consulte o blog de Eric Lipper sobre o assunto: blogs.msdn.com/b/ericlippert/archive/2009/11/12/… Isso explica muito bem por que isso acontece.
Travis J
2

Não está claro se o problema original era o mesmo que eu: querer maximizar os threads da CPU na computação dentro de um loop enquanto preserva o valor do iterador e mantém em linha para evitar passar uma tonelada de variáveis ​​para uma função de trabalho.

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

Eu fiz isso funcionar alterando o iterador externo e localizando seu valor com uma porta.

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();
}
Harald J
fonte
0

A ideia é evitar o uso de um sinal como o acima. Colocar valores int em uma estrutura evita que esses valores sejam alterados (na estrutura). Eu tive o seguinte problema: o loop var i mudaria antes de DoSomething (i) ser chamado (i foi incrementado no final do loop antes de () => DoSomething (i, i i) ser chamado). Com as estruturas isso não acontece mais. Bug desagradável para encontrar: DoSomething (i, i i) parece ótimo, mas nunca tenho certeza se ele é chamado a cada vez com um valor diferente para i (ou apenas 100 vezes com i = 100), portanto -> struct

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));
}
CodeDigger
fonte
1
Embora isso possa responder à pergunta, ela foi sinalizada para revisão. Respostas sem explicação são frequentemente consideradas de baixa qualidade. Forneça alguns comentários sobre por que essa é a resposta correta.
Dan