Cách sử dụng await trong một vòng lặp


86

Tôi đang cố gắng tạo một ứng dụng bảng điều khiển không đồng bộ thực hiện một số hoạt động trên một bộ sưu tập. Tôi có một phiên bản sử dụng song song cho vòng lặp khác phiên bản sử dụng async / await. Tôi mong đợi phiên bản async / await hoạt động tương tự như phiên bản song song nhưng nó thực thi đồng bộ. Tôi đang làm gì sai?

class Program
{
    static void Main(string[] args)
    {
        var worker = new Worker();
        worker.ParallelInit();
        var t = worker.Init();
        t.Wait();
        Console.ReadKey();
    }
}

public class Worker
{
    public async Task<bool> Init()
    {
        var series = Enumerable.Range(1, 5).ToList();
        foreach (var i in series)
        {
            Console.WriteLine("Starting Process {0}", i);
            var result = await DoWorkAsync(i);
            if (result)
            {
                Console.WriteLine("Ending Process {0}", i);
            }
        }

        return true;
    }

    public async Task<bool> DoWorkAsync(int i)
    {
        Console.WriteLine("working..{0}", i);
        await Task.Delay(1000);
        return true;
    }

    public bool ParallelInit()
    {
        var series = Enumerable.Range(1, 5).ToList();
        Parallel.ForEach(series, i =>
        {
            Console.WriteLine("Starting Process {0}", i);
            DoWorkAsync(i);
            Console.WriteLine("Ending Process {0}", i);
        });
        return true;
    }
}

Câu trả lời:


123

Cách bạn đang sử dụng awaittừ khóa cho C # biết rằng bạn muốn đợi mỗi khi đi qua vòng lặp, vòng lặp không song song. Bạn có thể viết lại phương thức của mình như thế này để làm những gì bạn muốn, bằng cách lưu trữ một danh sách các Tasks và sau đó nhập awaittất cả chúng với Task.WhenAll.

public async Task<bool> Init()
{
    var series = Enumerable.Range(1, 5).ToList();
    var tasks = new List<Task<Tuple<int, bool>>>();
    foreach (var i in series)
    {
        Console.WriteLine("Starting Process {0}", i);
        tasks.Add(DoWorkAsync(i));
    }
    foreach (var task in await Task.WhenAll(tasks))
    {
        if (task.Item2)
        {
            Console.WriteLine("Ending Process {0}", task.Item1);
        }
    }
    return true;
}

public async Task<Tuple<int, bool>> DoWorkAsync(int i)
{
    Console.WriteLine("working..{0}", i);
    await Task.Delay(1000);
    return Tuple.Create(i, true);
}

3
Tôi không biết về những người khác, nhưng một vòng lặp song song cho / foreach dường như thẳng hơn đối với các vòng lặp song song.
Brettski

8
Điều quan trọng cần lưu ý là khi bạn thấy Ending Processthông báo không phải là khi nhiệm vụ thực sự kết thúc. Tất cả các thông báo đó được loại bỏ tuần tự ngay sau khi tác vụ cuối cùng kết thúc. Vào thời điểm bạn nhìn thấy "Kết thúc quá trình 1", quá trình 1 có thể đã kết thúc trong một thời gian dài. Ngoài sự lựa chọn từ ở đó, +1.
Asad Saeeduddin

@Brettski Tôi có thể sai, nhưng một vòng lặp song song bẫy bất kỳ loại kết quả không đồng bộ nào. Bằng cách trả về một Task <T>, bạn ngay lập tức nhận lại một đối tượng Task trong đó bạn có thể quản lý công việc đang diễn ra bên trong chẳng hạn như hủy nó hoặc xem các ngoại lệ. Giờ đây với Async / Await, bạn có thể làm việc với đối tượng Task một cách thân thiện hơn - đó là bạn không phải thực hiện Task.Result.
The Muffin Man

@Tim S, nếu tôi muốn trả về một giá trị có hàm async bằng phương thức Tasks.WhenAll thì sao?
Mihir

Sẽ là một thực tiễn xấu nếu thực hiện một Semaphoretrong DoWorkAsyncđể hạn chế các tác vụ thực thi tối đa?
C4d

39

Mã của bạn đợi mỗi thao tác (sử dụng await) kết thúc trước khi bắt đầu lần lặp tiếp theo.
Do đó, bạn không nhận được bất kỳ sự song song nào.

Nếu bạn muốn chạy song song một hoạt động không đồng bộ hiện có, bạn không cần await; bạn chỉ cần lấy một tập hợp các Tasks và gọi Task.WhenAll()để trả về một tác vụ đang chờ tất cả chúng:

return Task.WhenAll(list.Select(DoWorkAsync));

vì vậy bạn không thể sử dụng bất kỳ phương thức không đồng bộ nào trong bất kỳ vòng lặp nào?
Satish

4
@Satish: Bạn có thể. Tuy nhiên, awaitkhông hoàn toàn ngược lại những gì bạn muốn - nó chờ đợi cho Taskđến kết thúc.
SLaks

Tôi muốn chấp nhận câu trả lời của bạn nhưng Tims S có câu trả lời hay hơn.
Satish

Hoặc nếu bạn không cần phải biết khi nhiệm vụ hoàn tất, bạn chỉ có thể gọi các phương pháp mà không cần chờ đợi cho họ
disklosr

Để xác nhận cú pháp đó đang làm gì - nó đang chạy Tác vụ được gọi DoWorkAsynctrên từng mục trong list(chuyển từng mục vào DoWorkAsync, mà tôi cho rằng có một tham số duy nhất)?
jbyrd

12
public async Task<bool> Init()
{
    var series = Enumerable.Range(1, 5);
    Task.WhenAll(series.Select(i => DoWorkAsync(i)));
    return true;
}

4

Trong C # 7.0, bạn có thể sử dụng tên ngữ nghĩa cho từng thành viên của bộ tuple , đây là câu trả lời của Tim S. sử dụng cú pháp mới:

public async Task<bool> Init()
{
    var series = Enumerable.Range(1, 5).ToList();
    var tasks = new List<Task<(int Index, bool IsDone)>>();

    foreach (var i in series)
    {
        Console.WriteLine("Starting Process {0}", i);
        tasks.Add(DoWorkAsync(i));
    }

    foreach (var task in await Task.WhenAll(tasks))
    {
        if (task.IsDone)
        {
            Console.WriteLine("Ending Process {0}", task.Index);
        }
    }

    return true;
}

public async Task<(int Index, bool IsDone)> DoWorkAsync(int i)
{
    Console.WriteLine("working..{0}", i);
    await Task.Delay(1000);
    return (i, true);
}

Bạn cũng có thể loại bỏ task. bên trongforeach :

// ...
foreach (var (IsDone, Index) in await Task.WhenAll(tasks))
{
    if (IsDone)
    {
        Console.WriteLine("Ending Process {0}", Index);
    }
}
// ...
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.