Điều gì chính xác xảy ra khi một luồng chờ một tác vụ trong vòng lặp while?


10

Sau khi xử lý mẫu không đồng bộ / chờ đợi của C # được một lúc, tôi đột nhiên nhận ra rằng tôi thực sự không biết làm thế nào để giải thích những gì xảy ra trong đoạn mã sau:

async void MyThread()
{
    while (!_quit)
    {
        await GetWorkAsync();
    }
}

GetWorkAsync()được giả định để trả về một sự chờ đợi Taskcó thể hoặc có thể không gây ra chuyển đổi luồng khi tiếp tục được thực thi.

Tôi sẽ không bối rối nếu sự chờ đợi không nằm trong một vòng lặp. Tôi tự nhiên mong đợi rằng phần còn lại của phương thức (tức là tiếp tục) có khả năng thực thi trên một luồng khác, điều này là tốt.

Tuy nhiên, trong một vòng lặp, khái niệm "phần còn lại của phương thức" có một chút mù mờ đối với tôi.

Điều gì xảy ra với "phần còn lại của vòng lặp" nếu luồng được bật tiếp tục so với nếu nó không được chuyển đổi? Trên luồng nào là lần lặp tiếp theo của vòng lặp được thực hiện?

Các quan sát của tôi cho thấy (không được xác minh cụ thể) rằng mỗi lần lặp lại bắt đầu trên cùng một luồng (lần đầu tiên) trong khi việc tiếp tục thực hiện trên một lần khác. Điều này thực sự có thể được? Nếu có, đây có phải là một mức độ song song bất ngờ cần được tính đến độ an toàn của luồng vis-a-vis của phương thức GetWorkAsync không?

CẬP NHẬT: Câu hỏi của tôi không phải là một bản sao, như được đề xuất bởi một số người. Mẫu while (!_quit) { ... }mã chỉ đơn giản là mã hóa mã thực tế của tôi. Trong thực tế, luồng của tôi là một vòng lặp dài xử lý hàng đợi đầu vào của các mục công việc theo các khoảng thời gian đều đặn (cứ sau 5 giây theo mặc định). Kiểm tra điều kiện thoát thực tế cũng không phải là kiểm tra trường đơn giản như được đề xuất bởi mã mẫu, mà là kiểm tra xử lý sự kiện.



1
Xem thêm Làm thế nào để sản lượng và chờ thực hiện luồng kiểm soát trong .NET? cho một số thông tin tuyệt vời về cách tất cả được kết nối với nhau.
John Wu

@ John Wu: Tôi chưa thấy chủ đề SO nào. Rất nhiều thông tin thú vị cốm ở đó. Cảm ơn!
aoven

Câu trả lời:


6

Bạn thực sự có thể kiểm tra nó tại Try Roslyn . Phương thức chờ đợi của bạn được viết lại vào void IAsyncStateMachine.MoveNext()lớp async được tạo.

Những gì bạn sẽ thấy là một cái gì đó như thế này:

            if (this.state != 0)
                goto label_2;
            //set up the state machine here
            label_1:
            taskAwaiter.GetResult();
            taskAwaiter = default(TaskAwaiter);
            label_2:
            if (!OuterClass._quit)
            {
               taskAwaiter = GetWorkAsync().GetAwaiter();
               //state machine stuff here
            }
            goto label_1;

Về cơ bản, bạn không quan tâm đến chủ đề nào; máy trạng thái có thể tiếp tục đúng bằng cách thay thế vòng lặp của bạn bằng cấu trúc if / goto tương đương.

Phải nói rằng, các phương thức async không nhất thiết phải thực thi trên một luồng khác. Xem lời giải thích "Không phải phép thuật" của Eric Lippert để giải thích cách bạn có thể làm việc async/awaitchỉ với một luồng.


2
Tôi dường như đang đánh giá thấp mức độ viết lại trình biên dịch đó trên mã async của tôi. Về bản chất, không có "vòng lặp" sau khi viết lại! Đó là phần còn thiếu đối với tôi. Tuyệt vời và cảm ơn bạn vì liên kết 'Hãy thử Roslyn'!
aoven

GOTO là cấu trúc vòng lặp ban đầu . Chúng ta đừng quên điều đó.

2

Đầu tiên, Servy đã viết một số mã trong câu trả lời cho một câu hỏi tương tự, dựa vào đó câu trả lời này dựa trên:

/programming/22049339/how-to-create-a-cancellable-task-loop

Câu trả lời của Servy bao gồm một ContinueWith()vòng lặp tương tự bằng cách sử dụng các cấu trúc TPL mà không sử dụng rõ ràng các từ khóa asyncawaittừ khóa; Vì vậy, để trả lời câu hỏi của bạn, hãy xem xét mã của bạn có thể trông như thế nào khi vòng lặp của bạn không được kiểm soát bằng cách sử dụngContinueWith()

    private static Task GetWorkWhileNotQuit()
    {
        var tcs = new TaskCompletionSource<bool>();

        Task previous = Task.FromResult(_quit);
        Action<Task> continuation = null;
        continuation = t =>
        {
            if (!_quit)
            {
                previous = previous.ContinueWith(_ => GetWorkAsync())
                    .Unwrap()
                    .ContinueWith(_ => previous.ContinueWith(continuation));
            }
            else
            {
                tcs.SetResult(_quit);
            }
        };
        previous.ContinueWith(continuation);
        return tcs.Task;
    }

Điều này cần một chút thời gian để quấn đầu bạn, nhưng tóm lại:

  • continuationđại diện cho một đóng cửa cho "lặp hiện tại"
  • previousđại diện cho Tasktrạng thái chứa "lần lặp trước" (nghĩa là nó biết khi nào việc 'lặp lại' kết thúc và được sử dụng để bắt đầu lần lặp tiếp theo ..)
  • Giả sử GetWorkAsync()trả về a Task, điều đó có nghĩa là ContinueWith(_ => GetWorkAsync())sẽ trả về một Task<Task>cuộc gọi Unwrap()để có được 'nhiệm vụ bên trong' (nghĩa là kết quả thực tế của GetWorkAsync()).

Vì thế:

  1. Ban đầu không có lần lặp trước , vì vậy nó chỉ được gán một giá trị là Task.FromResult(_quit) - trạng thái của nó bắt đầu là Task.Completed == true.
  2. Các continuationđược chạy lần đầu tiên sử dụngprevious.ContinueWith(continuation)
  3. các continuationcập nhật đóng cửa previousđể phản ánh trạng thái hoàn thành của_ => GetWorkAsync()
  4. Khi _ => GetWorkAsync()hoàn thành, nó "tiếp tục với" _previous.ContinueWith(continuation)- tức là gọi lại continuationlambda
    • Rõ ràng tại thời điểm này, previousđã được cập nhật với trạng thái _ => GetWorkAsync()vì vậy continuationlambda được gọi khi GetWorkAsync()trở về.

Các continuationlambda luôn kiểm tra tình trạng _quitnhư vậy, nếu _quit == falsesau đó không có nhiều sự tiếp tục, và TaskCompletionSourceđược thiết lập với giá trị của _quit, và tất cả mọi thứ được hoàn tất.

Đối với quan sát của bạn về việc tiếp tục được thực hiện trong một luồng khác, đó không phải là điều mà từ khóa async/ awaitsẽ làm cho bạn, theo blog này "Nhiệm vụ là (vẫn) không phải là chủ đề và không đồng bộ không song song" . - https://bloss.msdn.microsoft.com/benwilli/2015/09/10/t task - are - still - not - threads - and - async - is - not - omet /

Tôi đề nghị rằng nó thực sự đáng để xem xét kỹ hơn GetWorkAsync()phương pháp của bạn liên quan đến an toàn luồng và luồng. Nếu chẩn đoán của bạn tiết lộ rằng nó đã được thực thi trên một luồng khác do hậu quả của mã async / await lặp lại của bạn, thì một cái gì đó trong hoặc liên quan đến phương thức đó phải khiến một luồng mới được tạo ở nơi khác. (Nếu điều này là bất ngờ, có lẽ có một .ConfigureAwaitnơi nào đó?)


2
Mã tôi đã hiển thị là (rất) đơn giản hóa. Bên trong GetWorkAsync () có nhiều sự chờ đợi hơn nữa. Một số trong số họ truy cập cơ sở dữ liệu và mạng, có nghĩa là I / O thực sự. Theo tôi hiểu, việc chuyển đổi luồng là kết quả tự nhiên (mặc dù không bắt buộc) đối với những chờ đợi như vậy, bởi vì luồng ban đầu không thiết lập bất kỳ bối cảnh đồng bộ hóa nào sẽ chi phối nơi tiếp tục thực thi. Vì vậy, họ thực hiện trên một chủ đề nhóm chủ đề. Là lý luận của tôi sai?
aoven

@aoven Điểm tốt - Tôi đã không xem xét các loại khác nhau SynchronizationContext- điều đó chắc chắn rất quan trọng vì .ContinueWith()sử dụng SyncizationContext để gửi tiếp tục; nó thực sự sẽ giải thích hành vi bạn nhìn thấy nếu awaitđược gọi trên luồng ThreadPool hoặc luồng ASP.NET. Một sự tiếp tục chắc chắn có thể được gửi đến một chủ đề khác nhau trong những trường hợp đó. Mặt khác, việc gọi awaitmột bối cảnh đơn luồng như bối cảnh WPF hoặc Winforms là đủ để đảm bảo sự tiếp diễn diễn ra trên bản gốc. chủ đề
Ben Cottrell
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.