Làm thế nào để mang lại lợi nhuận và chờ đợi triển khai luồng kiểm soát trong .NET?


105

Như tôi hiểu yieldtừ khóa, nếu được sử dụng từ bên trong khối trình lặp, nó sẽ trả về luồng điều khiển cho mã gọi và khi trình lặp được gọi lại, nó sẽ tiếp tục nơi nó đã dừng lại.

Ngoài ra, awaitkhông chỉ đợi callee, mà nó trả lại quyền điều khiển cho người gọi, chỉ để tiếp tục nơi nó dừng lại khi người gọi awaitsphương thức.

Nói cách khác - không có luồng nào , và "sự đồng thời" của async và await là một ảo ảnh gây ra bởi luồng điều khiển thông minh, các chi tiết được che giấu bởi cú pháp.

Bây giờ, tôi là một cựu lập trình viên hợp ngữ và tôi rất quen thuộc với con trỏ lệnh, ngăn xếp, v.v. và tôi hiểu cách hoạt động của các luồng điều khiển bình thường (chương trình con, đệ quy, vòng lặp, nhánh). Nhưng những cấu trúc mới này - tôi không hiểu.

Khi awaitđạt đến một, làm thế nào để thời gian chạy biết đoạn mã nào sẽ thực thi tiếp theo? Làm thế nào nó biết khi nào nó có thể tiếp tục lại nơi nó đã dừng lại, và làm thế nào nó nhớ được nơi nào? Điều gì xảy ra với ngăn xếp cuộc gọi hiện tại, nó có được lưu bằng cách nào đó không? Điều gì sẽ xảy ra nếu phương thức gọi thực hiện các cuộc gọi phương thức khác trước nó await- tại sao ngăn xếp không bị ghi đè? Và làm thế quái nào mà thời gian chạy lại hoạt động theo cách của nó thông qua tất cả những điều này trong trường hợp ngoại lệ và ngăn xếp bị rút lại?

Khi nào yieldđạt được, làm thế nào để thời gian chạy theo dõi điểm mà mọi thứ nên được chọn? Trạng thái trình lặp được bảo toàn như thế nào?


4
Bạn có thể có một cái nhìn tại các mã được tạo trong TryRoslyn biên dịch trực tuyến
xanatos

1
Bạn có thể muốn xem loạt bài viết trên Eduasync của Jon Skeet.
Leonid Vasilev

Bài đọc thú vị liên quan: stackoverflow.com/questions/37419572/…
Jason C

Câu trả lời:


115

Tôi sẽ trả lời các câu hỏi cụ thể của bạn bên dưới, nhưng bạn có thể chỉ cần đọc các bài viết mở rộng của tôi về cách chúng tôi thiết kế lợi nhuận và chờ đợi.

https://blogs.msdn.microsoft.com/ericlippert/tag/continuation-passing-style/

https://blogs.msdn.microsoft.com/ericlippert/tag/iterators/

https://blogs.msdn.microsoft.com/ericlippert/tag/async/

Một số bài báo này hiện đã lỗi thời; mã được tạo khác nhau theo nhiều cách. Nhưng những điều này chắc chắn sẽ cung cấp cho bạn ý tưởng về cách nó hoạt động.

Ngoài ra, nếu bạn không hiểu cách lambdas được tạo ra dưới dạng các lớp đóng, hãy hiểu điều đó trước . Bạn sẽ không tạo ra đầu hoặc đuôi của không đồng bộ nếu bạn không có lambdas xuống.

Khi đạt đến thời gian chờ, làm cách nào thời gian chạy biết đoạn mã nào sẽ thực thi tiếp theo?

await được tạo dưới dạng:

if (the task is not completed)
  assign a delegate which executes the remainder of the method as the continuation of the task
  return to the caller
else
  execute the remainder of the method now

Về cơ bản là vậy. Chờ đợi chỉ là một sự trở lại ưa thích.

Làm thế nào nó biết khi nào nó có thể tiếp tục lại nơi nó đã dừng lại, và làm thế nào nó nhớ được nơi nào?

Chà, làm thế nào để bạn làm điều đó mà không cần chờ đợi? Khi phương thức foo gọi thanh phương thức, bằng cách nào đó, chúng tôi nhớ cách quay lại giữa foo, với tất cả các điểm kích hoạt foo vẫn còn nguyên vẹn, bất kể thanh nào xảy ra.

Bạn biết điều đó được thực hiện như thế nào trong trình lắp ráp. Một bản ghi kích hoạt cho foo được đẩy lên ngăn xếp; nó chứa đựng những giá trị của người dân địa phương. Tại thời điểm cuộc gọi, địa chỉ trả về trong foo được đẩy vào ngăn xếp. Khi thanh được hoàn thành, con trỏ ngăn xếp và con trỏ hướng dẫn được đặt lại về vị trí cần thiết và foo tiếp tục đi từ vị trí đã dừng.

Việc tiếp tục chờ đợi hoàn toàn giống nhau, ngoại trừ việc bản ghi được đưa vào heap vì lý do rõ ràng là chuỗi kích hoạt không tạo thành một ngăn xếp .

Ủy quyền đang chờ cung cấp khi tiếp tục nhiệm vụ chứa (1) một số là đầu vào của bảng tra cứu cung cấp cho con trỏ hướng dẫn mà bạn cần thực hiện tiếp theo và (2) tất cả các giá trị của cục bộ và dấu tạm thời.

Có một số thiết bị bổ sung trong đó; ví dụ, trong .NET, việc phân nhánh vào giữa khối try là bất hợp pháp, vì vậy bạn không thể chỉ cần gắn địa chỉ mã bên trong khối try vào bảng. Nhưng đây là chi tiết sổ sách kế toán. Về mặt khái niệm, bản ghi kích hoạt chỉ đơn giản là được chuyển vào heap.

Điều gì xảy ra với ngăn xếp cuộc gọi hiện tại, nó có được lưu bằng cách nào đó không?

Thông tin liên quan trong hồ sơ kích hoạt hiện tại không bao giờ được đưa vào ngăn xếp ngay từ đầu; nó được phân bổ khỏi đống từ lúc bắt đầu. (Chà, các tham số chính thức được truyền vào ngăn xếp hoặc trong các thanh ghi một cách bình thường và sau đó được sao chép vào vị trí đống khi phương thức bắt đầu.)

Hồ sơ kích hoạt của người gọi không được lưu trữ; sự chờ đợi có thể sẽ trở lại với họ, hãy nhớ, vì vậy họ sẽ được xử lý bình thường.

Lưu ý rằng đây là một sự khác biệt hoàn toàn giữa kiểu truyền tiếp tục đơn giản của await và cấu trúc tiếp tục gọi với-hiện tại thực sự mà bạn thấy trong các ngôn ngữ như Scheme. Trong những ngôn ngữ đó, toàn bộ phần tiếp diễn bao gồm cả phần tiếp tục quay trở lại người gọi được ghi lại bởi call-cc .

Điều gì sẽ xảy ra nếu phương thức gọi thực hiện các cuộc gọi phương thức khác trước khi nó chờ - tại sao ngăn xếp không bị ghi đè?

Các cuộc gọi phương thức đó quay trở lại, và do đó, các bản ghi kích hoạt của chúng không còn nằm trên ngăn xếp tại thời điểm chờ đợi.

Và làm thế quái nào mà thời gian chạy lại hoạt động theo cách của nó thông qua tất cả những điều này trong trường hợp ngoại lệ và ngăn xếp bị rút lại?

Trong trường hợp có một ngoại lệ chưa được nhập, ngoại lệ được bắt, lưu trữ bên trong tác vụ và được ném lại khi kết quả của tác vụ được tìm nạp.

Nhớ tất cả những gì tôi đã đề cập đến sổ sách kế toán trước đây? Việc hiểu đúng ngữ nghĩa ngoại lệ là một nỗi đau lớn, hãy để tôi nói cho bạn biết.

Khi đạt được năng suất, làm thế nào để thời gian chạy theo dõi điểm mà mọi thứ nên được chọn? Trạng thái trình lặp được bảo toàn như thế nào?

Cùng một cách. Trạng thái của người dân địa phương được chuyển lên đống và một số đại diện cho hướng dẫn MoveNextsẽ tiếp tục vào lần sau khi nó được gọi được lưu trữ cùng với người dân địa phương.

Và một lần nữa, có một loạt các thiết bị trong khối trình vòng lặp để đảm bảo rằng các ngoại lệ được xử lý chính xác.


1
do nền tảng của các tác giả câu hỏi (Assemblybler và cộng sự), điều đáng nói là cả hai cấu trúc này đều không thể thực hiện được nếu không có bộ nhớ được quản lý. nếu không có bộ nhớ được quản lý, việc cố gắng điều phối thời gian tồn tại của một quá trình đóng chắc chắn sẽ khiến bạn vấp phải chuỗi khởi động của mình.
Jim

Liên kết tất cả các trang không được tìm thấy (404)
Digital3D

Tất cả các bài viết của bạn hiện không có sẵn. Bạn có thể đăng lại chúng?
Michał Turczyn

1
@ MichałTurczyn: Họ vẫn đang ở trên internet; Microsoft tiếp tục di chuyển nơi lưu trữ blog. Tôi sẽ dần dần chuyển chúng sang trang cá nhân của mình và cố gắng cập nhật các liên kết này khi có thời gian.
Eric Lippert

38

yield là dễ hơn trong hai, vì vậy hãy kiểm tra nó.

Giả sử chúng tôi có:

public IEnumerable<int> CountToTen()
{
  for (int i = 1; i <= 10; ++i)
  {
    yield return i;
  }
}

Đây được biên soạn một chút như thế nào nếu chúng tôi đã viết:

// Deliberately use name that isn't valid C# to not clash with anything
private class <CountToTen> : IEnumerator<int>, IEnumerable<int>
{
    private int _i;
    private int _current;
    private int _state;
    private int _initialThreadId = CurrentManagedThreadId;

    public IEnumerator<CountToTen> GetEnumerator()
    {
        // Use self if never ran and same thread (so safe)
        // otherwise create a new object.
        if (_state != 0 || _initialThreadId != CurrentManagedThreadId)
        {
            return new <CountToTen>();
        }

        _state = 1;
        return this;
    }

    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();

    public int Current => _current;

    object IEnumerator.Current => Current;

    public bool MoveNext()
    {
        switch(_state)
        {
            case 1:
                _i = 1;
                _current = i;
                _state = 2;
                return true;
            case 2:
                ++_i;
                if (_i <= 10)
                {
                    _current = _i;
                    return true;
                }
                break;
        }
        _state = -1;
        return false;
    }

    public void Dispose()
    {
      // if the yield-using method had a `using` it would
      // be translated into something happening here.
    }

    public void Reset()
    {
        throw new NotSupportedException();
    }
}

Vì vậy, không hiệu quả bằng việc triển khai viết tay IEnumerable<int>IEnumerator<int>(ví dụ: chúng tôi có thể sẽ không lãng phí việc có một phần riêng biệt _state, _i_currenttrong trường hợp này) nhưng không tệ (mẹo sử dụng lại chính nó khi an toàn thay vì tạo một phần mềm mới đối tượng là tốt), và có thể mở rộng để đối phó với yieldcác phương pháp sử dụng rất phức tạp .

Và tất nhiên kể từ

foreach(var a in b)
{
  DoSomething(a);
}

Giống như:

using(var en = b.GetEnumerator())
{
  while(en.MoveNext())
  {
     var a = en.Current;
     DoSomething(a);
  }
}

Sau đó, được tạo ra MoveNext()nhiều lần được gọi.

Các asynctrường hợp được khá nhiều việc cùng một nguyên tắc, nhưng với một chút thêm phức tạp. Để sử dụng lại một ví dụ từ một câu trả lời khác Mã như:

private async Task LoopAsync()
{
    int count = 0;
    while(count < 5)
    {
       await SomeNetworkCallAsync();
       count++;
    }
}

Tạo mã như:

private struct LoopAsyncStateMachine : IAsyncStateMachine
{
  public int _state;
  public AsyncTaskMethodBuilder _builder;
  public TestAsync _this;
  public int _count;
  private TaskAwaiter _awaiter;
  void IAsyncStateMachine.MoveNext()
  {
    try
    {
      if (_state != 0)
      {
        _count = 0;
        goto afterSetup;
      }
      TaskAwaiter awaiter = _awaiter;
      _awaiter = default(TaskAwaiter);
      _state = -1;
    loopBack:
      awaiter.GetResult();
      awaiter = default(TaskAwaiter);
      _count++;
    afterSetup:
      if (_count < 5)
      {
        awaiter = _this.SomeNetworkCallAsync().GetAwaiter();
        if (!awaiter.IsCompleted)
        {
          _state = 0;
          _awaiter = awaiter;
          _builder.AwaitUnsafeOnCompleted<TaskAwaiter, TestAsync.LoopAsyncStateMachine>(ref awaiter, ref this);
          return;
        }
        goto loopBack;
      }
      _state = -2;
      _builder.SetResult();
    }
    catch (Exception exception)
    {
      _state = -2;
      _builder.SetException(exception);
      return;
    }
  }
  [DebuggerHidden]
  void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine param0)
  {
    _builder.SetStateMachine(param0);
  }
}

public Task LoopAsync()
{
  LoopAsyncStateMachine stateMachine = new LoopAsyncStateMachine();
  stateMachine._this = this;
  AsyncTaskMethodBuilder builder = AsyncTaskMethodBuilder.Create();
  stateMachine._builder = builder;
  stateMachine._state = -1;
  builder.Start(ref stateMachine);
  return builder.Task;
}

Nó phức tạp hơn, nhưng một nguyên tắc cơ bản rất giống nhau. Các phức tạp phụ chính là bây giờ GetAwaiter()đang được sử dụng. Nếu bất kỳ lúc nào awaiter.IsCompletedđược kiểm tra, nó sẽ trả về truevì tác vụ awaited đã được hoàn thành (ví dụ: các trường hợp nó có thể trả về đồng bộ) thì phương thức tiếp tục di chuyển qua các trạng thái, nhưng nếu không thì nó tự thiết lập như một lệnh gọi lại cho người chờ đợi.

Điều gì xảy ra với điều đó phụ thuộc vào người chờ, về điều gì sẽ kích hoạt lệnh gọi lại (ví dụ: hoàn thành I / O không đồng bộ, một tác vụ đang chạy trên một chuỗi đang hoàn thành) và những yêu cầu nào đối với việc sắp xếp thành một chuỗi cụ thể hoặc chạy trên một chuỗi threadpool , ngữ cảnh nào từ cuộc gọi ban đầu có thể cần hoặc không, v.v. Bất kể nó là gì, mặc dù một cái gì đó trong awaiter đó sẽ gọi vào MoveNextvà nó sẽ tiếp tục với phần công việc tiếp theo (cho đến phần tiếp theo await) hoặc kết thúc và quay trở lại trong trường hợp Taskmà nó đang thực hiện trở nên hoàn thành.


Bạn có dành thời gian để biên dịch bản dịch của riêng mình không? O_O uao.
CoffeDeveloper 18/02/17

4
@DarioOO Cái đầu tiên tôi có thể làm khá nhanh, đã thực hiện rất nhiều bản dịch từ yieldtay đến tay khi làm như vậy có lợi (thường là tối ưu hóa, nhưng muốn đảm bảo rằng điểm bắt đầu gần với trình biên dịch tạo vì vậy không có gì trở nên tối ưu hóa thông qua các giả định xấu). Câu thứ hai lần đầu tiên được sử dụng trong một câu trả lời khác và có một vài lỗ hổng trong kiến ​​thức của tôi vào thời điểm đó, vì vậy tôi đã có lợi cho mình khi điền vào những câu trả lời đó trong khi cung cấp câu trả lời đó bằng cách dịch ngược mã thủ công.
Jon Hanna

13

Đã có rất nhiều câu trả lời tuyệt vời ở đây; Tôi chỉ chia sẻ một vài quan điểm có thể giúp hình thành một mô hình tinh thần.

Đầu tiên, một asyncphương thức được trình biên dịch chia thành nhiều phần; các awaitbiểu thức là các điểm đứt gãy. (Điều này rất dễ hiểu đối với các phương pháp đơn giản; các phương pháp phức tạp hơn với các vòng lặp và xử lý ngoại lệ cũng bị phá vỡ, với việc bổ sung một máy trạng thái phức tạp hơn).

Thứ hai, awaitđược dịch thành một trình tự khá đơn giản; Tôi thích mô tả của Lucian , trong đó khá nhiều từ "nếu quá trình chờ đã hoàn tất, hãy lấy kết quả và tiếp tục thực hiện phương thức này; nếu không, hãy lưu trạng thái của phương thức này và quay lại". (Tôi sử dụng thuật ngữ rất giống trong asyncphần giới thiệu của mình ).

Khi đạt đến thời gian chờ, làm cách nào thời gian chạy biết đoạn mã nào sẽ thực thi tiếp theo?

Phần còn lại của phương thức tồn tại dưới dạng một lệnh gọi lại có thể chờ đợi (trong trường hợp tác vụ, các lệnh gọi lại này là sự liên tục). Khi quá trình chờ hoàn tất, nó sẽ gọi lại các lệnh gọi lại của nó.

Lưu ý rằng ngăn xếp cuộc gọi không được lưu và khôi phục; gọi lại được gọi trực tiếp. Trong trường hợp I / O chồng chéo, chúng được gọi trực tiếp từ nhóm luồng.

Các lệnh gọi lại đó có thể tiếp tục thực thi phương thức trực tiếp hoặc chúng có thể lên lịch để nó chạy ở nơi khác (ví dụ: nếu awaitgiao diện người dùng được chụp SynchronizationContextvà I / O đã hoàn thành trên nhóm luồng).

Làm thế nào nó biết khi nào nó có thể tiếp tục lại nơi nó đã dừng lại, và làm thế nào nó nhớ được nơi nào?

Tất cả chỉ là gọi lại. Khi một phương thức chờ hoàn thành, nó sẽ gọi lại các lệnh gọi lại của nó và bất kỳ asyncphương thức nào đã chỉnh sửa awaitnó sẽ được tiếp tục. Gọi lại nhảy vào giữa phương thức đó và có các biến cục bộ của nó trong phạm vi.

Các lệnh gọi lại không chạy một chuỗi cụ thể và chúng không được khôi phục gói gọi lại.

Điều gì xảy ra với ngăn xếp cuộc gọi hiện tại, nó có được lưu bằng cách nào đó không? Điều gì sẽ xảy ra nếu phương thức gọi thực hiện các cuộc gọi phương thức khác trước khi nó chờ - tại sao ngăn xếp không bị ghi đè? Và làm thế quái nào mà thời gian chạy lại hoạt động theo cách của nó thông qua tất cả những điều này trong trường hợp ngoại lệ và ngăn xếp bị rút lại?

Callstack không được lưu ngay từ đầu; nó không cần thiết.

Với mã đồng bộ, bạn có thể kết thúc với ngăn xếp cuộc gọi bao gồm tất cả những người gọi của bạn và thời gian chạy biết nơi để quay lại bằng cách sử dụng nó.

Với mã không đồng bộ, bạn có thể kết thúc với một loạt các con trỏ gọi lại - bắt nguồn từ một số hoạt động I / O hoàn thành nhiệm vụ của nó, có thể tiếp tục một asyncphương thức hoàn thành nhiệm vụ của nó, có thể tiếp tục một asyncphương thức đã hoàn thành nhiệm vụ của nó, v.v.

Như vậy, với đồng bộ đang Agọi điện thoại Bgọi điện thoại C, callstack của bạn có thể trông như thế này:

A:B:C

trong khi mã không đồng bộ sử dụng lệnh gọi lại (con trỏ):

A <- B <- C <- (I/O operation)

Khi đạt được năng suất, làm thế nào để thời gian chạy theo dõi điểm mà mọi thứ nên được chọn? Trạng thái trình lặp được bảo toàn như thế nào?

Hiện tại, khá kém hiệu quả. :)

Nó hoạt động giống như bất kỳ lambda nào khác - vòng đời của biến được mở rộng và các tham chiếu được đặt vào một đối tượng trạng thái sống trên ngăn xếp. Tài nguyên tốt nhất cho tất cả các chi tiết cấp độ sâu là loạt bài EduAsync của Jon Skeet .


7

yieldawaittrong khi cả hai đều giải quyết vấn đề kiểm soát luồng, hai việc hoàn toàn khác nhau. Vì vậy, tôi sẽ giải quyết chúng một cách riêng biệt.

Mục đích yieldlà làm cho việc xây dựng chuỗi lười biếng dễ dàng hơn. Khi bạn viết một vòng lặp điều tra viên với một yieldcâu lệnh trong đó, trình biên dịch sẽ tạo ra rất nhiều mã mới mà bạn không thấy. Dưới mui xe, nó thực sự tạo ra một lớp hoàn toàn mới. Lớp chứa các thành viên theo dõi trạng thái của vòng lặp và triển khai IEnumerable để mỗi lần bạn gọi MoveNextnó lại bước qua vòng lặp đó. Vì vậy, khi bạn thực hiện một vòng lặp foreach như thế này:

foreach(var item in mything.items()) {
    dosomething(item);
}

mã được tạo trông giống như sau:

var i = mything.items();
while(i.MoveNext()) {
    dosomething(i.Current);
}

Bên trong việc triển khai mything.items () là một loạt mã máy trạng thái sẽ thực hiện một "bước" của vòng lặp sau đó quay trở lại. Vì vậy, trong khi bạn viết nó trong nguồn giống như một vòng lặp đơn giản, thì nó không phải là một vòng lặp đơn giản. Vì vậy, thủ thuật biên dịch. Nếu bạn muốn xem chính mình, hãy kéo ILDASM hoặc ILSpy hoặc các công cụ tương tự và xem IL được tạo ra trông như thế nào. Nó phải là hướng dẫn.

asyncawaitmặt khác, là một ấm cá hoàn toàn khác. Await, trong tóm tắt, là một nguyên thủy đồng bộ hóa. Đó là một cách để nói với hệ thống "Tôi không thể tiếp tục cho đến khi việc này được hoàn thành." Nhưng, như bạn đã lưu ý, không phải lúc nào cũng có một chuỗi liên quan.

Những gì liên quan được gọi là bối cảnh đồng bộ hóa. Luôn luôn có một người xung quanh. Công việc của họ đồng bộ hóa bối cảnh là lên lịch các nhiệm vụ đang được chờ đợi và sự liên tục của chúng.

Khi bạn nói await thisThing(), một vài điều xảy ra. Trong phương thức không đồng bộ, trình biên dịch thực sự chia nhỏ phương thức thành các phần nhỏ hơn, mỗi phần là một phần "trước một chờ đợi" và một phần "sau một chờ đợi" (hoặc tiếp tục). Khi quá trình chờ thực thi, tác vụ đang được chờ đợi phần tiếp theo sau - nói cách khác, phần còn lại của hàm - được chuyển đến ngữ cảnh đồng bộ hóa. Bối cảnh sẽ đảm nhận việc lập lịch tác vụ và khi nó hoàn thành, ngữ cảnh sẽ chạy phần tiếp theo, chuyển bất kỳ giá trị trả về nào mà nó muốn.

Bối cảnh đồng bộ có thể tự do làm bất cứ điều gì nó muốn miễn là nó lên lịch cho mọi thứ. Nó có thể sử dụng nhóm chủ đề. Nó có thể tạo một chuỗi cho mỗi nhiệm vụ. Nó có thể chạy chúng một cách đồng bộ. Các môi trường khác nhau (ASP.NET so với WPF) cung cấp các triển khai ngữ cảnh đồng bộ khác nhau để thực hiện những việc khác nhau dựa trên những gì tốt nhất cho môi trường của chúng.

(Phần thưởng: bạn có bao giờ tự hỏi điều gì .ConfigurateAwait(false)không? Nó yêu cầu hệ thống không sử dụng ngữ cảnh đồng bộ hiện tại (thường dựa trên loại dự án của bạn - ví dụ: WPF vs ASP.NET) và thay vào đó sử dụng mặc định, sử dụng nhóm luồng).

Vì vậy, một lần nữa, đó là rất nhiều thủ thuật biên dịch. Nếu bạn nhìn vào mã được tạo ra thì nó phức tạp nhưng bạn sẽ có thể biết nó đang làm gì. Những loại biến đổi này rất khó, nhưng xác định và toán học, đó là lý do tại sao thật tuyệt khi trình biên dịch thực hiện chúng cho chúng ta.

PS Có một ngoại lệ đối với sự tồn tại của ngữ cảnh đồng bộ mặc định - các ứng dụng bảng điều khiển không có ngữ cảnh đồng bộ mặc định. Kiểm tra blog của Stephen Toub để biết thêm thông tin. Đó là một nơi tuyệt vời để tìm kiếm thông tin trên asyncawaitnói chung.


1
"Nó yêu cầu hệ thống không sử dụng ngữ cảnh đồng bộ hóa mặc định và thay vào đó sử dụng bối cảnh mặc định, sử dụng nhóm luồng" bạn có thể làm rõ ý bạn với điều này không? "không sử dụng mặc định, mặc định sử dụng"
Kroltan

3
Xin lỗi, đã trộn lẫn thuật ngữ của tôi, tôi sẽ sửa bài. Về cơ bản, không sử dụng cái mặc định cho môi trường bạn đang ở, hãy sử dụng cái mặc định cho .NET (tức là nhóm luồng).
Chris Tavares

rất đơn giản, có thể hiểu được, bạn đã nhận được phiếu bầu của tôi :)
Ehsan Sajjad

4

Thông thường, tôi muốn xem xét CIL, nhưng trong trường hợp này, đó là một mớ hỗn độn.

Hai cấu trúc ngôn ngữ này hoạt động tương tự nhau, nhưng được triển khai hơi khác một chút. Về cơ bản, nó chỉ là một cú pháp cho phép trình biên dịch, không có gì điên rồ / không an toàn ở cấp độ lắp ráp. Chúng ta hãy nhìn vào chúng một cách ngắn gọn.

yieldlà một câu lệnh cũ hơn và đơn giản hơn, và nó là một cú pháp cho một máy trạng thái cơ bản. Một phương thức trả về IEnumerable<T>hoặc IEnumerator<T>có thể chứa một yield, sau đó biến phương thức thành một nhà máy máy trạng thái. Một điều bạn cần lưu ý là không có mã nào trong phương thức được chạy tại thời điểm bạn gọi nó, nếu có mã yieldbên trong. Nguyên nhân là do mã bạn viết được chuyển sang IEnumerator<T>.MoveNextphương thức, phương thức này sẽ kiểm tra trạng thái của nó và chạy đúng phần của mã. yield return x;sau đó được chuyển đổi thành một cái gì đó tương tự nhưthis.Current = x; return true;

Nếu bạn thực hiện một số phản ánh, bạn có thể dễ dàng kiểm tra cỗ máy trạng thái đã xây dựng và các lĩnh vực của nó (ít nhất một cho nhà nước và cho người dân địa phương). Bạn thậm chí có thể đặt lại nó nếu bạn thay đổi các trường.

awaityêu cầu một chút hỗ trợ từ thư viện loại và hoạt động hơi khác. Nó nhận một Taskhoặc một Task<T>đối số, sau đó dẫn đến giá trị của nó nếu nhiệm vụ được hoàn thành hoặc đăng ký một sự tiếp tục qua Task.GetAwaiter().OnCompleted. Việc triển khai đầy đủ của async/ awaitsystem sẽ mất quá nhiều thời gian để giải thích, nhưng nó cũng không quá thần bí. Nó cũng tạo ra một máy trạng thái và chuyển nó dọc theo phần tiếp theo cho OnCompleted . Nếu nhiệm vụ được hoàn thành, nó sẽ sử dụng kết quả của nó để tiếp tục. Việc thực hiện awaiter quyết định cách gọi phần tiếp theo. Thông thường, nó sử dụng ngữ cảnh đồng bộ hóa của chuỗi cuộc gọi.

Cả hai yieldawaitphải chia nhỏ phương pháp dựa trên sự xuất hiện của chúng để tạo thành một máy trạng thái, với mỗi nhánh của máy đại diện cho từng phần của phương pháp.

Bạn không nên nghĩ về những khái niệm này trong các thuật ngữ "cấp thấp hơn" như ngăn xếp, luồng, v.v. Đây là những thứ trừu tượng và hoạt động bên trong của chúng không yêu cầu bất kỳ sự hỗ trợ nào từ CLR, nó chỉ là trình biên dịch làm điều kỳ diệu. Điều này hoàn toàn khác với các coroutines của Lua, vốn có sự hỗ trợ của thời gian chạy, hoặc longjmp của C , chỉ là ma thuật đen.


5
Lưu ý phụ : awaitkhông cần phải thực hiện một Nhiệm vụ . Bất cứ điều gì với INotifyCompletion GetAwaiter()là đủ. Một chút tương tự với cách foreachkhông cần IEnumerable, bất cứ thứ gì có IEnumerator GetEnumerator()là đủ.
IllidanS4 muốn Monica trở lại vào 17/02/17
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.