Task.Run với (các) Tham số?


87

Tôi đang làm việc trong một dự án mạng đa tác vụ và tôi là người mới Threading.Tasks. Tôi đã thực hiện một đơn giản Task.Factory.StartNew()và tôi tự hỏi làm thế nào tôi có thể làm điều đó với Task.Run()?

Đây là mã cơ bản:

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

Tôi đã xem xét System.Threading.Tasks.Tasktrong Trình duyệt đối tượng và tôi không thể tìm thấy Action<T>tham số like. Chỉ Actionvoidtham số và không có loại .

Chỉ có 2 thứ giống nhau: static Task Run(Action action)static Task Run(Func<Task> function)nhưng không thể đăng (các) tham số với cả hai.

Vâng, tôi biết tôi có thể tạo ra một phương pháp mở rộng đơn giản cho nó nhưng câu hỏi chính của tôi là chúng tôi có thể viết nó vào dòng duy nhất với Task.Run()?


Không rõ bạn muốn giá trị của tham số là gì. Nó sẽ đến từ đâu? Nếu bạn đã có nó, chỉ cần chụp nó trong biểu thức lambda ...
Jon Skeet

@JonSkeet rawDatalà một gói dữ liệu mạng có lớp chứa (như DataPacket) và tôi đang sử dụng lại phiên bản này để giảm áp lực GC. Vì vậy, nếu tôi sử dụng rawDatatrực tiếp Task, nó có thể (có thể) được thay đổi trước khi Taskxử lý nó. Bây giờ, tôi nghĩ tôi có thể tạo một byte[]phiên bản khác cho nó. Tôi nghĩ đó là giải pháp đơn giản nhất cho tôi.
MFatihMAR

Có, nếu bạn cần sao chép mảng byte, bạn sao chép mảng byte. Có một Action<byte[]>không thay đổi điều đó.
Jon Skeet

Dưới đây là một số giải pháp tốt để chuyển các tham số cho một tác vụ.
Just Shadow

Câu trả lời:


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

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

Biên tập

Do nhu cầu phổ biến, tôi phải lưu ý rằng trình Taskkhởi chạy sẽ chạy song song với luồng gọi. Giả sử mặc định TaskSchedulerđiều này sẽ sử dụng .NET ThreadPool. Dù sao đi nữa, điều này có nghĩa là bạn cần tính đến bất kỳ (các) tham số nào được chuyển đến Taskcó khả năng được nhiều luồng truy cập cùng một lúc, làm cho chúng ở trạng thái chia sẻ. Điều này bao gồm việc truy cập chúng trên chuỗi gọi.

Trong đoạn mã trên của tôi, trường hợp đó được thực hiện hoàn toàn tranh luận. Chuỗi là bất biến. Đó là lý do tại sao tôi sử dụng chúng làm ví dụ. Nhưng nói rằng bạn không sử dụng String...

Một giải pháp là sử dụng asyncawait. Điều này, theo mặc định, sẽ nắm bắt SynchronizationContextluồng đang gọi và sẽ tạo ra một phần tiếp theo cho phần còn lại của phương thức sau khi gọi tới awaitvà gắn nó vào phần đã tạo Task. Nếu phương thức này đang chạy trên chuỗi WinForms GUI thì nó sẽ thuộc loại WindowsFormsSynchronizationContext.

Phần tiếp theo sẽ chạy sau khi được đăng trở lại phần đã chụp SynchronizationContext- chỉ một lần nữa theo mặc định. Vì vậy, bạn sẽ trở lại chuỗi mà bạn đã bắt đầu sau awaitcuộc gọi. Bạn có thể thay đổi điều này theo nhiều cách khác nhau, đặc biệt là sử dụng ConfigureAwait. Nói tóm lại, phần còn lại của phương pháp đó sẽ không tiếp tục cho đến khi sau khi các Taskđã hoàn thành trên thread khác. Nhưng luồng gọi sẽ tiếp tục chạy song song, không chỉ là phần còn lại của phương thức.

Việc chờ đợi này để hoàn tất việc chạy phần còn lại của phương thức có thể mong muốn hoặc không. Nếu không có gì trong phương thức đó sau này truy cập vào các tham số được truyền đến Taskmà bạn có thể không muốn sử dụng await.

Hoặc có thể bạn sử dụng các tham số đó sau này trong phương thức. Không có lý do gì để awaitngay lập tức vì bạn có thể tiếp tục làm việc một cách an toàn. Hãy nhớ rằng, bạn có thể lưu trữ giá trị Tasktrả về trong một biến và awaittrên đó sau này - ngay cả trong cùng một phương thức. Ví dụ, một khi bạn cần truy cập các tham số đã truyền một cách an toàn sau khi thực hiện một số công việc khác. Một lần nữa, bạn không cần phải awaitTaskbên phải khi bạn chạy nó.

Tuy nhiên, một cách đơn giản để làm cho chuỗi này an toàn đối với các tham số được truyền đến Task.Runlà thực hiện điều này:

Đầu tiên bạn phải trang trí RunAsyncbằng async:

private async void RunAsync()

Lưu ý quan trọng

Tốt hơn là phương thức được đánh dấu không được trả về giá trị vô hiệu, như tài liệu liên kết đã đề cập. Ngoại lệ phổ biến cho điều này là các trình xử lý sự kiện chẳng hạn như nhấp vào nút và tương tự. Chúng phải trả về giá trị vô hiệu. Nếu không, tôi luôn cố gắng trả về một hoặc khi sử dụng . Đó là một thực hành tốt vì một số lý do.async TaskTask<TResult>async

Bây giờ bạn có thể awaitchạy Tasknhư bên dưới. Bạn không thể sử dụng awaitmà không có 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

Vì vậy, nói chung, nếu bạn thực awaithiện nhiệm vụ, bạn có thể tránh coi các tham số được truyền vào như một tài nguyên được chia sẻ tiềm năng với tất cả các cạm bẫy của việc sửa đổi thứ gì đó từ nhiều luồng cùng một lúc. Ngoài ra, hãy cẩn thận với việc đóng cửa . Tôi sẽ không trình bày sâu về những vấn đề đó nhưng bài viết được liên kết thực hiện rất tốt.

Ghi chú bên lề

Hơi lạc đề một chút, nhưng hãy cẩn thận khi sử dụng bất kỳ loại "chặn" nào trên chuỗi GUI của WinForms do nó được đánh dấu bằng [STAThread]. Sử dụng awaitsẽ không chặn, nhưng đôi khi tôi thấy nó được sử dụng kết hợp với một số loại chặn.

"Block" nằm trong dấu ngoặc kép vì về mặt kỹ thuật, bạn không thể chặn luồng GUI WinForms . Có, nếu bạn sử dụng locktrên chuỗi WinForms GUI, nó sẽ vẫn bơm thông báo, mặc dù bạn nghĩ rằng nó bị "chặn". Nó không thể.

Điều này có thể gây ra các vấn đề kỳ lạ trong một số trường hợp rất hiếm. lockVí dụ, một trong những lý do bạn không bao giờ muốn sử dụng khi vẽ tranh. Nhưng đó là một trường hợp ngoài lề và phức tạp; tuy nhiên tôi đã thấy nó gây ra các vấn đề điên rồ. Vì vậy, tôi ghi nhận nó vì lợi ích hoàn chỉnh.


22
Bạn không phải chờ đợi Task.Run(() => MethodWithParameter(param));. Có nghĩa là nếu parambị sửa đổi sau khi các Task.Run, bạn có thể có kết quả bất ngờ trên MethodWithParameter.
Alexandre Severino

8
Tại sao đây là một câu trả lời được chấp nhận khi nó sai. Nó hoàn toàn không tương đương với đối tượng trạng thái đi qua.
Egor Pavlikhin

6
@ Zer0 một đối tượng trạng thái là paremeter thứ hai trong Task.Factory.StartNew msdn.microsoft.com/en-us/library/dd321456(v=vs.110).aspx và nó lưu giá trị của đối tượng tại thời điểm gọi đến StartNew, trong khi câu trả lời của bạn tạo ra một bao đóng, giữ tham chiếu (nếu giá trị của tham số thay đổi trước khi tác vụ được chạy thì nó cũng sẽ thay đổi trong tác vụ), vì vậy mã của bạn hoàn toàn không tương đương với những gì câu hỏi đã hỏi . Câu trả lời thực sự là không có cách nào để viết nó bằng Task.Run ().
Egor Pavlikhin

3
@ Zer0 Có lẽ bạn nên đọc mã nguồn. Một cái vượt qua đối tượng trạng thái, cái còn lại thì không. Đó là những gì tôi đã nói từ đầu. Task.Run không phải là tay ngắn của Task.Factory.StartNew. Phiên bản đối tượng trạng thái ở đó vì những lý do kế thừa, nhưng nó vẫn ở đó và đôi khi nó hoạt động khác nhau, vì vậy mọi người nên biết về điều đó.
Egor Pavlikhin

3
Đọc bài viết của Toub, tôi sẽ nhấn mạnh câu này "Bạn có thể sử dụng quá tải chấp nhận trạng thái đối tượng, mà đối với các đường dẫn mã nhạy cảm với hiệu suất có thể được sử dụng để tránh đóng và phân bổ tương ứng". Tôi nghĩ đây là những gì @Zero đang ngụ ý khi xem xét việc sử dụng Task.Run qua StartNew.
davidcarr

35

Sử dụng bắt biến để "chuyển vào" các tham số.

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

Bạn cũng có thể sử dụng rawDatatrực tiếp nhưng bạn phải cẩn thận, nếu bạn thay đổi giá trị rawDatabên ngoài của một tác vụ (ví dụ: một trình lặp trong forvòng lặp) thì nó cũng sẽ thay đổi giá trị bên trong của tác vụ.


11
+1 vì xem xét thực tế quan trọng là biến có thể bị thay đổi ngay sau khi gọi Task.Run.
Alexandre Severino

1
làm thế nào là điều này sẽ giúp đỡ? nếu bạn sử dụng x bên trong chuỗi tác vụ và x là một tham chiếu đến một đối tượng, và nếu đối tượng được sửa đổi cùng lúc khi chuỗi tác vụ đang chạy, nó có thể dẫn đến sự tàn phá.
Ovi

1
@ Ovi-WanKenobi Có, nhưng đó không phải là câu hỏi này. Đó là cách truyền một tham số. Nếu bạn chuyển một tham chiếu đến một đối tượng dưới dạng tham số cho một hàm bình thường, bạn cũng sẽ gặp vấn đề tương tự ở đó.
Scott Chamberlain 13:16

Đúng, điều này không hoạt động. Nhiệm vụ của tôi không có tham chiếu trở lại x trong chuỗi gọi. Tôi chỉ nhận được null.
David Price

7

Từ bây giờ bạn cũng có thể:

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

Đây là câu trả lời tốt nhất vì nó cho phép một trạng thái được chuyển vào và ngăn chặn tình huống có thể xảy ra được đề cập trong câu trả lời của Kaden Burgart . Ví dụ: nếu bạn cần chuyển một IDisposableđối tượng vào task ủy quyền để giải quyết cảnh báo ReSharper "Biến Captured được xử lý trong phạm vi bên ngoài" , điều này thực hiện rất tốt. Trái với suy nghĩ thông thường, không có gì sai khi sử dụng Task.Factory.StartNewthay vì Task.Runnơi bạn cần chuyển trạng thái. Xem tại đây .
Neo

7

Tôi biết đây là một chủ đề cũ, nhưng tôi muốn chia sẻ một giải pháp mà tôi đã phải sử dụng vì bài đăng được chấp nhận vẫn gặp sự cố.

Vấn đề:

Như đã chỉ ra bởi Alexandre Severino, nếu param(trong hàm bên dưới) thay đổi ngay sau lệnh gọi hàm, bạn có thể nhận được một số hành vi không mong muốn trong MethodWithParameter.

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

Giải pháp của tôi:

Để giải thích cho điều này, tôi đã viết một cái gì đó giống như dòng mã sau:

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

Điều này cho phép tôi sử dụng tham số không đồng bộ một cách an toàn mặc dù thực tế là tham số đã thay đổi rất nhanh sau khi bắt đầu tác vụ (điều này gây ra sự cố với giải pháp đã đăng).

Sử dụng cách tiếp cận này, param(kiểu giá trị) nhận giá trị của nó, vì vậy ngay cả khi phương thức không đồng bộ chạy sau các paramthay đổi, psẽ có bất kỳ giá trị nào paramcó khi dòng mã này chạy.


5
Tôi háo hức chờ đợi bất cứ ai có thể nghĩ ra cách làm điều này dễ hiểu hơn với chi phí thấp hơn. Điều này được thừa nhận là khá xấu xí.
Kaden Burgart

5
Của bạn đây:var localParam = param; await Task.Run(() => MethodWithParam(localParam));
Stephen Cleary.

1
Nhân tiện, Stephen đã thảo luận trong câu trả lời của mình, một năm rưỡi trước.
Servy

1
@Servy: Thực ra đó là câu trả lời của Scott . Tôi đã không trả lời câu hỏi này.
Stephen Cleary

Câu trả lời của Scott sẽ không thực sự phù hợp với tôi, vì tôi đang chạy câu trả lời này trong một vòng lặp for. Tham số cục bộ sẽ được đặt lại trong lần lặp tiếp theo. Sự khác biệt trong câu trả lời tôi đã đăng là tham số được sao chép vào phạm vi của biểu thức lambda, vì vậy biến ngay lập tức an toàn. Trong câu trả lời của Scott, tham số vẫn ở trong cùng một phạm vi, vì vậy nó vẫn có thể thay đổi giữa việc gọi dòng và thực thi hàm không đồng bộ.
Kaden Burgart

5

Chỉ cần sử dụng Task.Run

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

Hoặc, nếu bạn muốn sử dụng nó trong một phương thức và chờ tác vụ sau

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;
}

1
Chỉ cần cẩn thận với các lệnh đóng nếu bạn làm theo cách đó for(int rawData = 0; rawData < 10; ++rawData) { Task.Run(() => { Console.WriteLine(rawData); } ) }sẽ không hoạt động giống như khi rawDatađược chuyển vào như trong ví dụ StartNew của OP.
Scott Chamberlain

@ScottChamberlain - Đó có vẻ như là một ví dụ khác;) Tôi hy vọng hầu hết mọi người hiểu về việc đóng trên các giá trị lambda.
Travis J

3
Và nếu những nhận xét trước đó không có ý nghĩa gì, vui lòng xem blog của Eric Lipper về chủ đề: blog.msdn.com/b/ericlippert/archive/2009/11/12/… Nó giải thích tại sao điều này xảy ra rất tốt.
Travis J

2

Không rõ vấn đề ban đầu có phải là vấn đề tương tự mà tôi gặp phải hay không: muốn tối đa hóa các luồng CPU khi tính toán bên trong vòng lặp trong khi vẫn giữ nguyên giá trị của trình vòng lặp và giữ nội tuyến để tránh truyền hàng tấn biến cho một hàm worker.

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

Tôi đã làm việc này bằng cách thay đổi trình vòng lặp bên ngoài và bản địa hóa giá trị của nó bằng một cổng.

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

Ý tưởng là tránh sử dụng một Tín hiệu như trên. Việc bơm các giá trị int vào một cấu trúc sẽ ngăn các giá trị đó thay đổi (trong cấu trúc). Tôi đã gặp sự cố sau: vòng lặp var tôi sẽ thay đổi trước khi DoSomething (i) được gọi (tôi đã tăng lên ở cuối vòng lặp trước khi () => DoSomething (i, i i) được gọi). Với các cấu trúc, điều đó không xảy ra nữa. Lỗi khó tìm: DoSomething (i, i i) trông rất tuyệt, nhưng không bao giờ chắc chắn liệu nó có được gọi mỗi lần với một giá trị khác nhau cho i (hoặc chỉ 100 lần với i = 100), do đó -> 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));
}

1
Mặc dù điều này có thể trả lời câu hỏi, nhưng nó đã được gắn cờ để xem xét. Các câu trả lời không có lời giải thích thường được coi là chất lượng thấp. Vui lòng cung cấp một số bình luận cho lý do tại sao đây là câu trả lời chính xác.
Dan
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.