CTP 5 không đồng bộ CTP: tại sao trạng thái nội bộ của Bang được đặt thành 0 trong mã được tạo trước khi kết thúc cuộc gọi?


195

Hôm qua tôi đã nói chuyện về tính năng "không đồng bộ" C # mới, đặc biệt đi sâu vào việc mã được tạo trông như thế nào và the GetAwaiter()/ BeginAwait()/ EndAwait()cuộc gọi.

Chúng tôi đã xem xét một số chi tiết tại máy trạng thái được tạo bởi trình biên dịch C # và có hai khía cạnh chúng tôi không thể hiểu:

  • Tại sao lớp được tạo chứa một Dispose()phương thức và một $__disposingbiến, dường như không bao giờ được sử dụng (và lớp không thực hiện IDisposable).
  • Tại sao statebiến nội bộ được đặt thành 0 trước bất kỳ cuộc gọi nào EndAwait(), khi 0 thường xuất hiện có nghĩa là "đây là điểm nhập ban đầu".

Tôi nghi ngờ điểm đầu tiên có thể được trả lời bằng cách thực hiện một điều thú vị hơn trong phương thức async, mặc dù nếu có ai có thêm thông tin nào tôi sẽ rất vui khi nghe nó. Câu hỏi này là nhiều hơn về điểm thứ hai, tuy nhiên.

Đây là một đoạn mã mẫu rất đơn giản:

using System.Threading.Tasks;

class Test
{
    static async Task<int> Sum(Task<int> t1, Task<int> t2)
    {
        return await t1 + await t2;
    }
}

... Và đây là mã được tạo cho MoveNext()phương thức thực hiện máy trạng thái. Điều này được sao chép trực tiếp từ Reflector - Tôi chưa sửa các tên biến không thể nói rõ:

public void MoveNext()
{
    try
    {
        this.$__doFinallyBodies = true;
        switch (this.<>1__state)
        {
            case 1:
                break;

            case 2:
                goto Label_00DA;

            case -1:
                return;

            default:
                this.<a1>t__$await2 = this.t1.GetAwaiter<int>();
                this.<>1__state = 1;
                this.$__doFinallyBodies = false;
                if (this.<a1>t__$await2.BeginAwait(this.MoveNextDelegate))
                {
                    return;
                }
                this.$__doFinallyBodies = true;
                break;
        }
        this.<>1__state = 0;
        this.<1>t__$await1 = this.<a1>t__$await2.EndAwait();
        this.<a2>t__$await4 = this.t2.GetAwaiter<int>();
        this.<>1__state = 2;
        this.$__doFinallyBodies = false;
        if (this.<a2>t__$await4.BeginAwait(this.MoveNextDelegate))
        {
            return;
        }
        this.$__doFinallyBodies = true;
    Label_00DA:
        this.<>1__state = 0;
        this.<2>t__$await3 = this.<a2>t__$await4.EndAwait();
        this.<>1__state = -1;
        this.$builder.SetResult(this.<1>t__$await1 + this.<2>t__$await3);
    }
    catch (Exception exception)
    {
        this.<>1__state = -1;
        this.$builder.SetException(exception);
    }
}

Nó dài, nhưng các dòng quan trọng cho câu hỏi này là:

// End of awaiting t1
this.<>1__state = 0;
this.<1>t__$await1 = this.<a1>t__$await2.EndAwait();

// End of awaiting t2
this.<>1__state = 0;
this.<2>t__$await3 = this.<a2>t__$await4.EndAwait();

Trong cả hai trường hợp, trạng thái được thay đổi một lần nữa sau đó trước khi nó được quan sát rõ ràng ... vậy tại sao lại đặt nó thành 0? Nếu MoveNext()được gọi lại vào thời điểm này (trực tiếp hoặc thông qua Dispose), nó sẽ khởi động lại phương thức async một cách hiệu quả, điều này hoàn toàn không phù hợp theo như tôi có thể nói ... nếu và MoveNext() không được gọi, thay đổi trạng thái là không liên quan.

Đây có phải chỉ đơn giản là một tác dụng phụ của trình biên dịch tái sử dụng mã tạo khối lặp cho async, nơi nó có thể có một lời giải thích rõ ràng hơn?

Từ chối trách nhiệm quan trọng

Rõ ràng đây chỉ là một trình biên dịch CTP. Tôi hoàn toàn mong đợi mọi thứ sẽ thay đổi trước khi phát hành cuối cùng - và thậm chí trước cả phiên bản CTP tiếp theo. Câu hỏi này không có cách nào để khẳng định đây là một lỗ hổng trong trình biên dịch C # hoặc bất cứ điều gì tương tự. Tôi chỉ đang cố gắng tìm hiểu xem có một lý do tinh tế nào cho việc này mà tôi đã bỏ lỡ không :)


7
Trình biên dịch VB tạo ra một máy trạng thái tương tự (không biết có dự kiến ​​hay không, nhưng VB không có các khối lặp trước đó)
Damien_The_Unbeliever 17/211

1
@Rune: MoveNextDelegate chỉ là trường đại biểu đề cập đến MoveNext. Tôi tin rằng nó được lưu trong bộ nhớ cache để tránh tạo ra một Hành động mới để chuyển sang người phục vụ mỗi lần.
Jon Skeet

5
Tôi nghĩ câu trả lời là: đây là CTP. Bit thứ tự cao cho nhóm đã đưa nó ra khỏi đó và thiết kế ngôn ngữ được xác nhận. Và họ đã làm rất nhanh một cách đáng kinh ngạc. Bạn nên mong đợi việc triển khai được vận chuyển (của trình biên dịch, không phải MoveNext) sẽ khác biệt đáng kể. Tôi nghĩ rằng Eric hoặc Lucian sẽ quay lại với một câu trả lời dọc theo dòng rằng không có gì sâu sắc ở đây, chỉ là một hành vi / lỗi không quan trọng trong hầu hết các trường hợp và không ai nhận thấy. Bởi vì đó là CTP.
Chris Burrows

2
@Stilgar: Tôi vừa mới kiểm tra bằng ildasm và nó thực sự đang làm điều này.
Jon Skeet

3
@JonSkeet: Lưu ý cách không ai đưa ra câu trả lời. 99% chúng ta không thể thực sự biết câu trả lời có đúng hay không.
ngày

Câu trả lời:


71

Được rồi, cuối cùng tôi đã có một câu trả lời thực sự. Tôi tự mình giải quyết nó, nhưng chỉ sau khi Lucian Wischik từ phần VB của nhóm xác nhận rằng thực sự có một lý do chính đáng cho nó. Rất cám ơn anh ấy - và xin vui lòng ghé thăm blog của anh ấy , đá.

Giá trị 0 ở đây chỉ đặc biệt vì đó không phải là trạng thái hợp lệ mà bạn có thể ở ngay trước awaittrường hợp bình thường. Cụ thể, đó không phải là trạng thái mà máy trạng thái có thể kết thúc thử nghiệm ở nơi khác. Tôi tin rằng việc sử dụng bất kỳ giá trị không tích cực nào cũng sẽ hoạt động tốt: -1 không được sử dụng cho điều này vì nó không chính xác về mặt logic , vì -1 thường có nghĩa là "đã hoàn thành". Tôi có thể lập luận rằng chúng tôi đang đưa ra một ý nghĩa bổ sung cho trạng thái 0 vào lúc này, nhưng cuối cùng nó không thực sự quan trọng. Điểm của câu hỏi này là tìm hiểu tại sao nhà nước lại được thiết lập.

Giá trị này có liên quan nếu sự chờ đợi kết thúc trong một ngoại lệ được bắt. Cuối cùng, chúng ta có thể quay trở lại cùng một tuyên bố chờ đợi, nhưng chúng ta không được ở trạng thái có nghĩa là "Tôi sắp trở lại từ sự chờ đợi đó" vì nếu không thì tất cả các loại mã sẽ bị bỏ qua. Thật đơn giản để thể hiện điều này với một ví dụ. Lưu ý rằng tôi hiện đang sử dụng CTP thứ hai, vì vậy mã được tạo hơi khác với mã trong câu hỏi.

Đây là phương thức async:

static async Task<int> FooAsync()
{
    var t = new SimpleAwaitable();

    for (int i = 0; i < 3; i++)
    {
        try
        {
            Console.WriteLine("In Try");
            return await t;
        }                
        catch (Exception)
        {
            Console.WriteLine("Trying again...");
        }
    }
    return 0;
}

Về mặt khái niệm, SimpleAwaitablecó thể là bất kỳ điều gì đang chờ đợi - có thể là một nhiệm vụ, có thể là một cái gì đó khác. Đối với mục đích thử nghiệm của tôi, nó luôn trả về false IsCompletedvà ném ngoại lệ vào GetResult.

Đây là mã được tạo cho MoveNext:

public void MoveNext()
{
    int returnValue;
    try
    {
        int num3 = state;
        if (num3 == 1)
        {
            goto Label_ContinuationPoint;
        }
        if (state == -1)
        {
            return;
        }
        t = new SimpleAwaitable();
        i = 0;
      Label_ContinuationPoint:
        while (i < 3)
        {
            // Label_ContinuationPoint: should be here
            try
            {
                num3 = state;
                if (num3 != 1)
                {
                    Console.WriteLine("In Try");
                    awaiter = t.GetAwaiter();
                    if (!awaiter.IsCompleted)
                    {
                        state = 1;
                        awaiter.OnCompleted(MoveNextDelegate);
                        return;
                    }
                }
                else
                {
                    state = 0;
                }
                int result = awaiter.GetResult();
                awaiter = null;
                returnValue = result;
                goto Label_ReturnStatement;
            }
            catch (Exception)
            {
                Console.WriteLine("Trying again...");
            }
            i++;
        }
        returnValue = 0;
    }
    catch (Exception exception)
    {
        state = -1;
        Builder.SetException(exception);
        return;
    }
  Label_ReturnStatement:
    state = -1;
    Builder.SetResult(returnValue);
}

Tôi đã phải di chuyển Label_ContinuationPointđể biến nó thành mã hợp lệ - nếu không thì nó không nằm trong phạm vi của gototuyên bố - nhưng điều đó không ảnh hưởng đến câu trả lời.

Hãy suy nghĩ về những gì xảy ra khi GetResultném ngoại lệ của nó. Chúng ta sẽ đi qua khối bắt, tăng dần ivà sau đó lặp lại vòng tròn (giả sử ivẫn còn ít hơn 3). Chúng tôi vẫn ở trong bất kỳ trạng thái nào trước GetResultcuộc gọi ... nhưng khi vào trong trykhối, chúng tôi phải in "Thử" và gọi GetAwaiterlại ... và chúng tôi sẽ chỉ làm điều đó nếu trạng thái không 1. Không có các state = 0nhiệm vụ, nó sẽ sử dụng các awaiter hiện và bỏ qua Console.WriteLinecuộc gọi.

Đó là một đoạn mã khá khó khăn để xử lý, nhưng điều đó chỉ thể hiện những loại điều mà nhóm phải suy nghĩ. Tôi rất vui vì tôi không chịu trách nhiệm thực hiện việc này :)


8
@Shekhar_Pro: Vâng, đó là một goto. Bạn nên mong đợi để xem nhiều goto báo cáo trong các máy trạng thái tự động tạo ra :)
Jon Skeet

12
@Shekhar_Pro: Trong mã được viết thủ công, đó là - bởi vì nó làm cho mã khó đọc và làm theo. Không ai đọc mã tự động mặc dù, ngoại trừ những kẻ ngu ngốc như tôi, người dịch ngược mã đó :)
Jon Skeet

Vì vậy, những gì không xảy ra khi chúng ta đang chờ đợi một lần nữa sau khi một ngoại lệ? Chúng ta bắt đầu lại từ đầu?
cấu hình

1
@configurator: Nó gọi GetAwaiter là điều đáng chờ đợi, đó là điều tôi mong đợi nó sẽ làm.
Jon Skeet

gotos không luôn luôn làm cho mã khó đọc hơn. Trong thực tế, đôi khi chúng thậm chí còn có ý nghĩa để sử dụng (đặc quyền để nói, tôi biết). Ví dụ, đôi khi bạn có thể cần phải phá vỡ nhiều vòng lặp lồng nhau. Tôi ít sử dụng tính năng của goto (và sử dụng xấu hơn IMO) là khiến các câu lệnh chuyển đổi thành tầng. Trên một ghi chú riêng biệt, tôi nhớ một ngày và tuổi khi gotos là nền tảng chính của một số ngôn ngữ lập trình và vì điều đó tôi hoàn toàn nhận ra lý do tại sao việc nhắc đến goto chỉ khiến các nhà phát triển rùng mình. Họ có thể làm cho mọi thứ xấu xí nếu sử dụng kém.
Ben Lesh

5

nếu nó được giữ ở mức 1 (trường hợp đầu tiên), bạn sẽ nhận được một cuộc gọi đến EndAwaitmà không có cuộc gọi đến BeginAwait. Nếu nó được giữ ở mức 2 (trường hợp thứ hai), bạn sẽ nhận được kết quả tương tự chỉ với người phục vụ khác.

Tôi đoán rằng việc gọi BeginAwait trả về sai nếu nó đã được bắt đầu (một dự đoán từ phía tôi) và giữ giá trị ban đầu để trả về tại EndAwait. Nếu đó là trường hợp, nó sẽ hoạt động chính xác trong khi nếu bạn đặt nó thành -1, bạn có thể chưa được khởi tạo this.<1>t__$await1cho trường hợp đầu tiên.

Tuy nhiên, điều này giả định rằng BeginAwaiter sẽ không thực sự bắt đầu hành động đối với bất kỳ cuộc gọi nào sau cuộc gọi đầu tiên và nó sẽ trả về sai trong những trường hợp đó. Bắt đầu tất nhiên sẽ không được chấp nhận vì nó có thể có tác dụng phụ hoặc đơn giản là cho một kết quả khác. Nó cũng giả định rằng EndAwaiter sẽ luôn trả về cùng một giá trị cho dù nó được gọi bao nhiêu lần và điều đó có thể được gọi khi BeginAwait trả về sai (theo giả định ở trên)

Nó dường như là một người bảo vệ chống lại các điều kiện chủng tộc Nếu chúng ta đặt nội tuyến các câu lệnh trong đó Movenext được gọi bởi một luồng khác sau trạng thái = 0 trong các câu hỏi, nó sẽ trông giống như bên dưới

this.<a1>t__$await2 = this.t1.GetAwaiter<int>();
this.<>1__state = 1;
this.$__doFinallyBodies = false;
this.<a1>t__$await2.BeginAwait(this.MoveNextDelegate)
this.<>1__state = 0;

//second thread
this.<a1>t__$await2 = this.t1.GetAwaiter<int>();
this.<>1__state = 1;
this.$__doFinallyBodies = false;
this.<a1>t__$await2.BeginAwait(this.MoveNextDelegate)
this.$__doFinallyBodies = true;
this.<>1__state = 0;
this.<1>t__$await1 = this.<a1>t__$await2.EndAwait();

//other thread
this.<1>t__$await1 = this.<a1>t__$await2.EndAwait();

Nếu các giả định ở trên là chính xác, có một số công việc không cần thiết được thực hiện như lấy cưa và gán lại cùng một giá trị cho <1> t __ $ await1. Nếu trạng thái được giữ ở mức 1 thì phần cuối cùng sẽ thay thế:

//second thread
//I suppose this un matched call to EndAwait will fail
this.<1>t__$await1 = this.<a1>t__$await2.EndAwait();

hơn nữa nếu nó được đặt thành 2, máy trạng thái sẽ cho rằng nó đã nhận được giá trị của hành động đầu tiên không đúng sự thật và một biến (không có khả năng) sẽ được sử dụng để tính kết quả


Hãy nhớ rằng trạng thái không thực sự được sử dụng giữa gán cho 0 và gán cho giá trị có ý nghĩa hơn. Nếu nó có nghĩa là để bảo vệ chống lại các điều kiện chủng tộc, tôi mong đợi một số giá trị khác chỉ ra rằng, ví dụ -2, với một kiểm tra cho điều đó khi bắt đầu MoveNext để phát hiện việc sử dụng không phù hợp. Hãy nhớ rằng một trường hợp duy nhất không bao giờ thực sự được sử dụng bởi hai luồng tại một thời điểm - điều đó có nghĩa là tạo ảo giác cho một cuộc gọi phương thức đồng bộ duy nhất quản lý để "tạm dừng" thường xuyên.
Jon Skeet

@ Tôi đồng ý rằng đó không phải là vấn đề với điều kiện cuộc đua trong trường hợp không đồng bộ nhưng có thể ở khối lặp và có thể là phần còn lại
Rune FS

@Tony: Tôi nghĩ rằng tôi sẽ đợi cho đến khi CTP hoặc beta tiếp theo xuất hiện và kiểm tra hành vi đó.
Jon Skeet

1

Nó có thể là một cái gì đó để làm với các cuộc gọi async xếp chồng / lồng nhau không? ..

I E:

async Task m1()
{
    await m2;
}

async Task m2()
{
    await m3();
}

async Task m3()
{
Thread.Sleep(10000);
}

Liệu đại biểu Movenext có được gọi nhiều lần trong tình huống này không?

Chỉ là một trò hề thực sự?


Sẽ có ba lớp được tạo khác nhau trong trường hợp đó. MoveNext()sẽ được gọi một lần trên mỗi người trong số họ.
Jon Skeet

0

Giải thích về các trạng thái thực tế:

trạng thái có thể:

  • 0 Đã khởi tạo (tôi nghĩ vậy) hoặc chờ kết thúc hoạt động
  • > 0 chỉ được gọi là MoveNext, chọn trạng thái tiếp theo
  • -1 đã kết thúc

Có thể là việc triển khai này chỉ muốn đảm bảo rằng nếu một Cuộc gọi khác đến MoveNext từ bất cứ nơi nào xảy ra (trong khi chờ đợi), nó sẽ đánh giá lại toàn bộ chuỗi trạng thái ngay từ đầu, để đánh giá lại kết quả có thể đã hết thời?


Nhưng tại sao nó lại muốn bắt đầu lại từ đầu? Đó gần như chắc chắn không phải là điều bạn thực sự muốn xảy ra - bạn muốn ném một ngoại lệ, bởi vì không có gì khác nên gọi MoveNext.
Jon Skeet
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.