จะให้ผลตอบแทนและรอดำเนินการตามขั้นตอนการควบคุมใน. NET ได้อย่างไร


105

ตามที่ฉันเข้าใจyieldคำสำคัญหากใช้จากภายในบล็อกตัววนซ้ำมันจะส่งคืนขั้นตอนการควบคุมไปยังรหัสการเรียกและเมื่อตัววนซ้ำถูกเรียกอีกครั้งคำหลักจะดึงขึ้นมาจากจุดที่ค้างไว้

นอกจากนี้awaitไม่เพียง แต่รอผู้เรียกเท่านั้น แต่ยังส่งกลับการควบคุมไปยังผู้โทรเพียงเพื่อเลือกจุดที่ค้างไว้เมื่อผู้เรียกawaitsใช้เมธอด

กล่าวอีกนัยหนึ่ง - ไม่มีเธรดและ "ภาวะพร้อมกัน" ของ async และการรอคอยเป็นภาพลวงตาที่เกิดจากการควบคุมอย่างชาญฉลาดซึ่งรายละเอียดจะถูกปกปิดโดยไวยากรณ์

ตอนนี้ฉันเป็นอดีตโปรแกรมเมอร์แอสเซมบลีและฉันคุ้นเคยกับตัวชี้คำสั่งสแต็กและอื่น ๆ เป็นอย่างดีและฉันได้รับวิธีการทำงานของโฟลว์การควบคุมปกติ (รูทีนย่อยการเรียกซ้ำลูปสาขา) แต่โครงสร้างใหม่เหล่านี้ - ฉันไม่เข้าใจ

เมื่อawaitถึงเวลารันไทม์จะรู้ได้อย่างไรว่าโค้ดส่วนใดควรรันต่อไป มันรู้ได้อย่างไรว่ามันสามารถกลับมาทำงานต่อจากจุดที่ค้างไว้ได้อย่างไรและมันจำได้อย่างไร เกิดอะไรขึ้นกับ call stack ปัจจุบันได้รับการบันทึกหรือไม่? จะเกิดอะไรขึ้นถ้าวิธีการโทรทำให้เมธอดอื่นเรียกก่อน - awaitเหตุใดสแต็กจึงไม่ถูกเขียนทับ และรันไทม์บนโลกจะทำงานผ่านสิ่งนี้ได้อย่างไรในกรณีที่มีข้อยกเว้นและสแต็กคลายตัว

เมื่อyieldถึงเวลารันไทม์จะติดตามจุดที่ควรหยิบสิ่งของได้อย่างไร? สถานะตัวทำซ้ำถูกเก็บรักษาไว้อย่างไร


4
คุณสามารถดูที่โค้ดที่สร้างขึ้นในTryRoslynคอมไพเลอร์ออนไลน์
Xanatos

1
คุณอาจต้องการตรวจสอบชุดบทความ Eduasyncโดย Jon Skeet
Leonid Vasilev

ที่น่าสนใจอ่าน: stackoverflow.com/questions/37419572/…
Jason C

คำตอบ:


115

ฉันจะตอบคำถามเฉพาะของคุณด้านล่าง แต่คุณน่าจะทำได้ดีเพียงแค่อ่านบทความมากมายของฉันเกี่ยวกับวิธีที่เราออกแบบผลตอบแทนและรอคอย

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/

บทความเหล่านี้บางส่วนล้าสมัยแล้ว รหัสที่สร้างขึ้นมีความแตกต่างกันหลายวิธี แต่สิ่งเหล่านี้จะทำให้คุณมีแนวคิดว่ามันทำงานอย่างไร

นอกจากนี้ถ้าคุณไม่เข้าใจวิธีการ lambdas จะถูกสร้างเป็นชั้นเรียนปิดเข้าใจว่าเป็นครั้งแรก คุณจะไม่สร้างหัวหรือหางของ async ถ้าคุณไม่มี lambdas ลง

เมื่อถึงการรอคอยรันไทม์จะรู้ได้อย่างไรว่าโค้ดส่วนใดควรรันต่อไป

await ถูกสร้างขึ้นเป็น:

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

โดยพื้นฐานแล้ว การรอคอยเป็นเพียงการกลับมาที่สวยงาม

มันรู้ได้อย่างไรว่ามันสามารถกลับมาทำงานต่อจากจุดที่ค้างไว้ได้อย่างไรและมันจำได้อย่างไรว่าอยู่ที่ไหน?

คุณจะทำอย่างไรโดยไม่ต้องรอ? เมื่อเมธอด foo เรียกแถบวิธีการเราจำวิธีกลับไปที่จุดกึ่งกลางของ foo โดยที่คนในพื้นที่ทั้งหมดของการเปิดใช้งาน foo ยังคงอยู่ไม่ว่าจะเป็นแถบใดก็ตาม

คุณรู้ว่ามันทำอย่างไรในแอสเซมเบลอร์ บันทึกการเปิดใช้งานสำหรับ foo ถูกผลักลงบนสแต็ก มีคุณค่าของคนในท้องถิ่น เมื่อถึงจุดที่มีการโทรที่อยู่สำหรับส่งคืนใน foo จะถูกผลักเข้าไปในกอง เมื่อแถบเสร็จสิ้นตัวชี้สแต็กและตัวชี้คำสั่งจะถูกรีเซ็ตเป็นตำแหน่งที่ต้องการและ foo จะไปจากจุดที่ค้างไว้

ความต่อเนื่องของรอคอยเป็นสิ่งเดียวกันยกเว้นที่บันทึกจะถูกนำเข้าสู่กองด้วยเหตุผลที่ชัดเจนว่าลำดับของการเปิดใช้งานไม่ได้ในรูปแบบสแต็ค

ผู้รับมอบสิทธิ์ที่รอคอยให้เป็นความต่อเนื่องของงานประกอบด้วย (1) ตัวเลขซึ่งเป็นข้อมูลเข้าในตารางการค้นหาที่ให้ตัวชี้คำสั่งที่คุณต้องดำเนินการต่อไปและ (2) ค่าทั้งหมดของโลคัลและเทมไลบรารี่

มีอุปกรณ์เพิ่มเติมอยู่ที่นั่น ตัวอย่างเช่นใน. NET เป็นสิ่งผิดกฎหมายที่จะแยกไปอยู่ตรงกลางของบล็อกการลองดังนั้นคุณจึงไม่สามารถติดที่อยู่ของรหัสไว้ในบล็อกลองลงในตารางได้ แต่นี่คือรายละเอียดการทำบัญชี ตามแนวคิดแล้วบันทึกการเปิดใช้งานจะถูกย้ายไปยังฮีป

เกิดอะไรขึ้นกับ call stack ปัจจุบันได้รับการบันทึกหรือไม่?

ข้อมูลที่เกี่ยวข้องในบันทึกการเปิดใช้งานปัจจุบันจะไม่ถูกใส่ไว้ในสแต็กตั้งแต่แรก มันถูกจัดสรรออกจากฮีปจาก get-go (พารามิเตอร์ที่เป็นทางการจะถูกส่งไปบนสแต็กหรือในรีจิสเตอร์ตามปกติแล้วคัดลอกไปยังตำแหน่งฮีปเมื่อวิธีการเริ่มต้น)

ไม่มีการจัดเก็บบันทึกการเปิดใช้งานของผู้โทร การรอคอยอาจจะกลับมาหาพวกเขาโปรดจำไว้ดังนั้นพวกเขาจะได้รับการจัดการตามปกติ

โปรดทราบว่านี่เป็นความแตกต่างระหว่างรูปแบบการส่งผ่านความต่อเนื่องที่เรียบง่ายของการรอคอยและโครงสร้างการเรียกที่มีกระแส - ต่อเนื่องที่คุณเห็นในภาษาเช่น Scheme ในภาษาเหล่านั้นต่อเนื่องทั้งหมดรวมทั้งด้านหลังต่อเนื่องเข้าไปในสายที่ถูกจับโดยเรียกร้องซีซี

จะเกิดอะไรขึ้นถ้าวิธีการโทรทำให้เรียกวิธีอื่นก่อนที่มันจะรอทำไมสแต็กไม่ถูกเขียนทับ?

เมธอดเหล่านั้นเรียกกลับดังนั้นเร็กคอร์ดการเปิดใช้งานจึงไม่อยู่ในสแต็กที่จุดรออีกต่อไป

และรันไทม์บนโลกจะทำงานผ่านสิ่งนี้ได้อย่างไรในกรณีที่มีข้อยกเว้นและสแต็กคลายตัว

ในกรณีที่มีข้อยกเว้นที่ไม่ถูกจับข้อยกเว้นจะถูกจับเก็บไว้ในงานและโยนใหม่เมื่อมีการดึงผลลัพธ์ของงาน

จำการทำบัญชีทั้งหมดที่ฉันพูดถึงก่อนหน้านี้ได้ไหม การได้รับความหมายยกเว้นอย่างถูกต้องเป็นความเจ็บปวดอย่างมากขอบอกคุณ

เมื่อถึงผลตอบแทนรันไทม์จะติดตามจุดที่ควรหยิบสิ่งของอย่างไร สถานะตัวทำซ้ำถูกเก็บรักษาไว้อย่างไร

วิธีการเดียวกัน. สถานะของชาวบ้านถูกย้ายไปที่กองและตัวเลขที่แสดงถึงคำสั่งที่MoveNextควรกลับมาใช้งานในครั้งต่อไปที่เรียกนั้นจะถูกเก็บไว้พร้อมกับชาวบ้าน

และอีกครั้งมีเฟืองมากมายในบล็อกตัววนซ้ำเพื่อให้แน่ใจว่ามีการจัดการข้อยกเว้นอย่างถูกต้อง


1
เนื่องจากพื้นหลังของผู้เขียนคำถาม (แอสเซมเบลอร์และคณะ) อาจเป็นไปได้ว่าโครงสร้างทั้งสองนี้เป็นไปไม่ได้หากไม่มีหน่วยความจำที่มีการจัดการ หากไม่มีหน่วยความจำที่มีการจัดการที่พยายามประสานอายุการใช้งานของการปิดจะทำให้คุณสะดุดกับ bootstraps ของคุณอย่างแน่นอน
จิม

ไม่พบลิงก์ของทุกหน้า (404)
Digital3D

บทความทั้งหมดของคุณไม่สามารถใช้งานได้ในขณะนี้ คุณสามารถโพสต์ใหม่ได้หรือไม่?
Michał Turczyn

1
@ MichałTurczyn: พวกเขายังคงอยู่บนอินเทอร์เน็ต Microsoft ย้ายไปที่ที่เก็บถาวรของบล็อก ฉันจะค่อยๆย้ายข้อมูลเหล่านี้ไปยังไซต์ส่วนตัวของฉันและพยายามอัปเดตลิงก์เหล่านี้เมื่อฉันมีเวลา
Eric Lippert

38

yield ง่ายกว่าสำหรับทั้งสองอย่างลองพิจารณาดู

สมมติว่าเรามี:

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

สิ่งนี้ได้รับการรวบรวมเล็กน้อยเช่นถ้าเราเขียน:

// 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();
    }
}

ดังนั้นไม่เป็นที่มีประสิทธิภาพการดำเนินงานที่เขียนด้วยมือของIEnumerable<int>และIEnumerator<int>(เช่นเราอาจจะไม่ต้องเสียต้องแยก_state, _iและ_currentในกรณีนี้) แต่ไม่ได้เลวร้าย (เคล็ดลับของอีกครั้งโดยใช้ตัวเองเมื่อสามารถทำได้อย่างปลอดภัยมากกว่าการสร้างใหม่ วัตถุเป็นสิ่งที่ดี) และสามารถขยายได้เพื่อจัดการกับyieldวิธีการใช้งานที่ซับซ้อนมาก

และแน่นอนตั้งแต่นั้นมา

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

เหมือนกับ:

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

จากนั้นสิ่งที่สร้างขึ้นMoveNext()จะถูกเรียกซ้ำ ๆ

asyncกรณีที่สวยมากหลักการเดียวกัน แต่มีบิตของความซับซ้อนเป็นพิเศษ หากต้องการนำตัวอย่างจากรหัสคำตอบอื่นมาใช้ซ้ำเช่น:

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

สร้างรหัสเช่น:

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

มันซับซ้อนกว่า แต่หลักการพื้นฐานที่คล้ายกันมาก ภาวะแทรกซ้อนพิเศษที่สำคัญคือตอนนี้GetAwaiter()กำลังถูกนำมาใช้ หากawaiter.IsCompletedมีการตรวจสอบเวลาใดก็ตามจะส่งคืนtrueเนื่องจากงานawaited เสร็จสิ้นไปแล้ว (เช่นกรณีที่สามารถส่งคืนแบบซิงโครนัส) เมธอดจะเคลื่อนผ่านสถานะต่างๆต่อไป แต่มิฉะนั้นจะตั้งค่าตัวเองเป็นการเรียกกลับไปยังผู้รอ

สิ่งที่เกิดขึ้นกับสิ่งนั้นขึ้นอยู่กับผู้รอในแง่ของสิ่งที่ทริกเกอร์การเรียกกลับ (เช่นการเสร็จสิ้น async I / O งานที่รันบนเธรดที่เสร็จสมบูรณ์) และข้อกำหนดใดบ้างสำหรับการมาร์แชลไปยังเธรดเฉพาะหรือรันบนเธรดพูลเธรด บริบทใดจากการเรียกดั้งเดิมที่อาจจำเป็นหรือไม่จำเป็นเป็นต้น ไม่ว่าจะเป็นอะไรก็ตามในสิ่งที่รอคอยจะเรียกเข้ามาMoveNextและมันจะดำเนินการต่อกับงานชิ้นถัดไป (ถึงงานถัดไปawait) หรือเสร็จสิ้นและกลับมาในกรณีTaskที่การใช้งานเสร็จสมบูรณ์


คุณใช้เวลาในการแปลคำแปลของคุณเองหรือไม่? O_O อือออ.
CoffeDeveloper

4
@DarioOO คนแรกที่ฉันทำได้ค่อนข้างเร็วโดยได้แปลมากมายจากyieldไปจนถึงรีดด้วยมือเมื่อมีประโยชน์ในการทำเช่นนั้น (โดยทั่วไปเป็นการเพิ่มประสิทธิภาพ แต่ต้องการให้แน่ใจว่าจุดเริ่มต้นอยู่ใกล้กับคอมไพเลอร์ที่สร้างขึ้น จึงไม่มีสิ่งใดถูกลดประสิทธิภาพผ่านสมมติฐานที่ไม่ดี) คำตอบที่สองถูกใช้เป็นครั้งแรกในคำตอบอื่นและมีช่องว่างสองสามอย่างในความรู้ของตัวเองในเวลานั้นดังนั้นฉันจึงได้รับประโยชน์จากการกรอกข้อมูลเหล่านั้นในขณะที่ให้คำตอบนั้นด้วยการถอดรหัสรหัสด้วยมือ
จอนฮันนา

13

มีคำตอบดีๆมากมายอยู่ที่นี่แล้ว ฉันแค่จะแบ่งปันมุมมองบางอย่างที่สามารถช่วยสร้างแบบจำลองทางจิต

ขั้นแรกasyncให้คอมไพเลอร์แบ่งวิธีการออกเป็นหลายส่วน awaitแสดงออกเป็นจุดแตกหัก (นี่เป็นวิธีที่ง่ายต่อการตั้งครรภ์สำหรับวิธีง่ายๆวิธีที่ซับซ้อนมากขึ้นด้วยการวนซ้ำและการจัดการข้อยกเว้นก็ถูกแยกออกด้วยการเพิ่มเครื่องสถานะที่ซับซ้อนมากขึ้น)

ประการที่สองawaitแปลเป็นลำดับที่ค่อนข้างง่าย ฉันชอบคำอธิบายของ Lucianซึ่งในคำพูดนั้นค่อนข้างมาก "หากสิ่งที่รอคอยนั้นเสร็จสมบูรณ์แล้วให้รับผลลัพธ์และดำเนินการตามวิธีนี้ต่อไปมิฉะนั้นให้บันทึกสถานะของวิธีนี้และส่งคืน" (ฉันใช้คำศัพท์ที่คล้ายกันมากในasyncบทนำ )

เมื่อถึงการรอคอยรันไทม์จะรู้ได้อย่างไรว่าโค้ดส่วนใดควรรันต่อไป

ส่วนที่เหลือของวิธีการนี้มีอยู่เป็นการเรียกกลับสำหรับสิ่งที่รอคอยนั้น (ในกรณีของงานการเรียกกลับเหล่านี้เป็นความต่อเนื่อง) เมื่อการรอคอยเสร็จสิ้นระบบจะเรียกใช้การโทรกลับ

โปรดทราบว่า call stack ไม่ได้รับการบันทึกและเรียกคืน เรียกกลับโดยตรง ในกรณีของ I / O ที่ทับซ้อนกันระบบจะเรียกใช้โดยตรงจากเธรดพูล

การเรียกกลับเหล่านั้นอาจดำเนินการกับเมธอดโดยตรงต่อไปหรืออาจกำหนดเวลาให้รันที่อื่น (เช่นหากawaitUI ที่บันทึกSynchronizationContextและ I / O เสร็จสมบูรณ์บนเธรดพูล)

มันรู้ได้อย่างไรว่ามันสามารถกลับมาทำงานต่อจากจุดที่ค้างไว้ได้อย่างไรและมันจำได้อย่างไรว่าอยู่ที่ไหน?

ทั้งหมดเป็นเพียงการโทรกลับ เมื่อการรอคอยเสร็จสิ้นจะเรียกใช้การเรียกกลับและasyncวิธีการใด ๆที่แก้ไขไปแล้วawaitจะกลับมาทำงานต่อ การเรียกกลับจะกระโดดเข้ามาตรงกลางของวิธีการนั้นและมีตัวแปรโลคัลอยู่ในขอบเขต

การเรียกกลับไม่ได้เรียกใช้เธรดเฉพาะและไม่มีการเรียกคืน callstack

เกิดอะไรขึ้นกับ call stack ปัจจุบันมันได้รับการบันทึกหรือไม่? จะเกิดอะไรขึ้นถ้าวิธีการโทรทำให้เรียกวิธีอื่นก่อนที่จะรอทำไมสแต็กไม่ถูกเขียนทับ และรันไทม์บนโลกจะทำงานผ่านสิ่งนี้ได้อย่างไรในกรณีที่มีข้อยกเว้นและสแต็กคลายตัว

callstack ไม่ได้บันทึกไว้ตั้งแต่แรก มันไม่จำเป็น

ด้วยรหัสซิงโครนัสคุณสามารถจบลงด้วย call stack ที่มีผู้โทรทั้งหมดของคุณและรันไทม์รู้ว่าจะกลับมาใช้ที่ใด

ด้วยรหัสอะซิงโครนัสคุณสามารถจบลงด้วยตัวชี้การเรียกกลับจำนวนมากซึ่งมีรากฐานมาจากการดำเนินการ I / O บางอย่างที่เสร็จสิ้นภารกิจซึ่งสามารถดำเนินการต่อตามasyncวิธีการที่ทำให้งานเสร็จสิ้นซึ่งสามารถดำเนินการต่อตามasyncวิธีการที่ทำให้งานเสร็จสิ้นเป็นต้น

ดังนั้นด้วยรหัสซิงโครAโทรBโทรC, callstack ของคุณอาจมีลักษณะเช่นนี้

A:B:C

ในขณะที่รหัสอะซิงโครนัสใช้การเรียกกลับ (พอยน์เตอร์):

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

เมื่อถึงผลตอบแทนรันไทม์จะติดตามจุดที่ควรหยิบสิ่งของอย่างไร สถานะตัวทำซ้ำถูกเก็บรักษาไว้อย่างไร

ขณะนี้ค่อนข้างไม่มีประสิทธิภาพ :)

มันทำงานเหมือนกับแลมด้าอื่น ๆ - มีการขยายอายุการใช้งานของตัวแปรและการอ้างอิงจะถูกวางไว้ในวัตถุสถานะที่อาศัยอยู่บนสแต็ก ทรัพยากรที่ดีที่สุดสำหรับทุกรายละเอียดที่ลึกระดับจอนสกีตชุด EduAsync


7

yieldและawaitในขณะที่ทั้งสองจัดการกับการควบคุมการไหลมีสองสิ่งที่แตกต่างกันอย่างสิ้นเชิง ดังนั้นฉันจะจัดการแยกต่างหาก

เป้าหมายyieldคือทำให้ง่ายต่อการสร้างลำดับขี้เกียจ เมื่อคุณเขียน enumerator-loop โดยมีyieldคำสั่งอยู่คอมไพเลอร์จะสร้างโค้ดใหม่จำนวนมากที่คุณไม่เห็น ภายใต้ฝากระโปรงนั้นสร้างคลาสใหม่ทั้งหมด คลาสประกอบด้วยสมาชิกที่ติดตามสถานะของลูปและการใช้งาน IEnumerable เพื่อให้ทุกครั้งที่คุณเรียกMoveNextขั้นตอนนั้นอีกครั้งผ่านลูปนั้น ดังนั้นเมื่อคุณทำ foreach loop ดังนี้:

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

รหัสที่สร้างขึ้นมีลักษณะดังนี้:

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

ภายในการใช้งาน mything.items () คือกลุ่มของรหัสสถานะเครื่องที่จะทำหนึ่ง "ขั้นตอน" ของลูปจากนั้นส่งคืน ดังนั้นในขณะที่คุณเขียนลงในซอร์สเหมือนกับการวนซ้ำแบบธรรมดาภายใต้ประทุนมันไม่ใช่การวนซ้ำแบบธรรมดา ดังนั้นกลอุบายของคอมไพเลอร์ หากคุณต้องการดูตัวเองให้ดึง ILDASM หรือ ILSpy หรือเครื่องมือที่คล้ายกันออกมาและดูว่า IL ที่สร้างขึ้นมีลักษณะอย่างไร ควรให้คำแนะนำ

asyncและawaitในทางกลับกันเป็นปลาอื่น ๆ ทั้งหมด การรอคอยคือในนามธรรมดั้งเดิมของการซิงโครไนซ์ เป็นการบอกระบบว่า "ฉันทำต่อไปไม่ได้จนกว่าจะเสร็จ" แต่อย่างที่คุณสังเกตเห็นว่าไม่มีหัวข้อที่เกี่ยวข้องเสมอไป

สิ่งที่เป็นที่เกี่ยวข้องกับสิ่งที่เรียกว่าบริบทการประสาน มักจะมีคนแขวนอยู่รอบ ๆ งานของบริบทการซิงโครไนซ์คือการจัดกำหนดการงานที่รอคอยและความต่อเนื่อง

เมื่อคุณพูดawait thisThing()มีสองสิ่งเกิดขึ้น ในวิธีการ async คอมไพลเลอร์จะสับเมธอดเป็นชิ้นเล็ก ๆ โดยแต่ละกลุ่มจะเป็นส่วน "ก่อนรอ" และส่วน "หลังจากรอ" (หรือส่วนต่อเนื่อง) เมื่อการรอดำเนินการดำเนินการงานที่กำลังรอคอยและความต่อเนื่องต่อไปนี้กล่าวคือส่วนที่เหลือของฟังก์ชันจะถูกส่งไปยังบริบทการซิงโครไนซ์ บริบทจะดูแลการจัดตารางเวลางานและเมื่อเสร็จสิ้นบริบทจะรันการต่อเนื่องโดยส่งคืนค่าที่ต้องการ

บริบทการซิงค์มีอิสระที่จะทำทุกอย่างที่ต้องการตราบเท่าที่กำหนดเวลาสิ่งต่างๆ มันสามารถใช้เธรดพูล มันสามารถสร้างเธรดต่องาน มันสามารถเรียกใช้พร้อมกันได้ สภาพแวดล้อมที่แตกต่างกัน (ASP.NET เทียบกับ WPF) จัดเตรียมการใช้งานบริบทการซิงค์ที่แตกต่างกันซึ่งทำสิ่งต่าง ๆ ตามสิ่งที่ดีที่สุดสำหรับสภาพแวดล้อมของพวกเขา

(โบนัส: เคยสงสัยไหมว่า.ConfigurateAwait(false)มันทำอะไรมันบอกให้ระบบไม่ใช้บริบทการซิงค์ปัจจุบัน (โดยปกติจะขึ้นอยู่กับประเภทโครงการของคุณเช่น WPF เทียบกับ ASP.NET) และใช้ค่าเริ่มต้นแทนซึ่งใช้เธรดพูล)

อีกครั้งมันเป็นกลอุบายของคอมไพเลอร์มากมาย หากคุณดูโค้ดที่สร้างขึ้นมันซับซ้อน แต่คุณควรจะเห็นว่ามันทำอะไร การเปลี่ยนแปลงประเภทนี้ทำได้ยาก แต่เป็นตัวกำหนดและคณิตศาสตร์ซึ่งเป็นเหตุผลว่าทำไมคอมไพเลอร์จึงทำเพื่อเรา

ป.ล. มีข้อยกเว้นประการหนึ่งสำหรับการมีอยู่ของบริบทการซิงค์เริ่มต้น - แอปคอนโซลไม่มีบริบทการซิงค์เริ่มต้น ตรวจสอบบล็อกของ Stephen Toubเพื่อดูข้อมูลเพิ่มเติมมากมาย เป็นสถานที่ที่ดีเยี่ยมในการค้นหาข้อมูลasyncและawaitโดยทั่วไป


1
"มันกำลังบอกให้ระบบไม่ใช้บริบทการซิงค์เริ่มต้นและใช้บริบทเริ่มต้นแทนซึ่งใช้เธรดพูล" คุณช่วยล้างความหมายของสิ่งนี้ได้ไหม "ไม่ใช้ค่าเริ่มต้นให้ใช้ค่าเริ่มต้น"
Kroltan

3
ขอโทษด้วยคำศัพท์ของฉันสับสนฉันจะแก้ไขโพสต์ โดยทั่วไปอย่าใช้ค่าเริ่มต้นสำหรับสภาพแวดล้อมที่คุณอยู่ให้ใช้ค่าเริ่มต้นสำหรับ. NET (เช่นเธรดพูล)
Chris Tavares

ง่ายมากเข้าใจได้คุณได้รับการโหวตของฉัน :)
Ehsan Sajjad

4

โดยปกติฉันจะกลับมาดู CIL แต่ในกรณีนี้มันเป็นเรื่องยุ่ง

โครงสร้างภาษาทั้งสองนี้มีความคล้ายคลึงกันในการทำงาน แต่ใช้งานต่างกันเล็กน้อย โดยพื้นฐานแล้วมันเป็นเพียงน้ำตาลซินแทติกสำหรับเวทมนตร์ของคอมไพเลอร์ไม่มีอะไรที่บ้า / ไม่ปลอดภัยในระดับการประกอบ ลองดูพวกเขาสั้น ๆ

yieldเป็นคำสั่งที่เก่ากว่าและเรียบง่ายกว่าและเป็นน้ำตาลเชิงไวยากรณ์สำหรับเครื่องสถานะพื้นฐาน วิธีที่ส่งคืนIEnumerable<T>หรือIEnumerator<T>อาจมี a yieldซึ่งจะเปลี่ยนวิธีการเป็นโรงงานเครื่องจักรของรัฐ สิ่งหนึ่งที่คุณควรสังเกตคือไม่มีการเรียกใช้โค้ดในเมธอดในขณะที่คุณเรียกมันหากมีอยู่yieldข้างใน เหตุผลก็คือโค้ดที่คุณเขียนถูกแปลเป็นIEnumerator<T>.MoveNextเมธอดซึ่งจะตรวจสอบสถานะที่มีอยู่และเรียกใช้ส่วนที่ถูกต้องของโค้ด yield return x;จะถูกแปลงเป็นสิ่งที่คล้ายกับthis.Current = x; return true;

หากคุณทำการไตร่ตรองคุณสามารถตรวจสอบเครื่องจักรของรัฐที่สร้างขึ้นและฟิลด์ของมันได้อย่างง่ายดาย (อย่างน้อยหนึ่งเครื่องสำหรับรัฐและสำหรับคนในพื้นที่) คุณสามารถรีเซ็ตได้หากคุณเปลี่ยนฟิลด์

awaitต้องการการสนับสนุนเล็กน้อยจากไลบรารีชนิดและทำงานแตกต่างกันบ้าง มันต้องใช้เวลาTaskหรือโต้แย้งแล้วทั้งผลลัพธ์ที่คุ้มค่าถ้างานเสร็จสมบูรณ์หรือลงทะเบียนต่อเนื่องผ่านทางTask<T> Task.GetAwaiter().OnCompletedการนำระบบasync/ ไปใช้งานเต็มรูปแบบawaitจะใช้เวลาอธิบายนานเกินไป แต่ก็ไม่ได้ลึกลับเช่นกัน นอกจากนี้ยังสร้างเครื่องของรัฐและผ่านมันไปพร้อมความต่อเนื่องในการOnCompleted หากงานเสร็จสมบูรณ์จะใช้ผลลัพธ์ในการดำเนินการต่อ การดำเนินการของผู้รอจะตัดสินใจว่าจะเรียกใช้ความต่อเนื่องอย่างไร โดยทั่วไปจะใช้บริบทการซิงโครไนซ์ของเธรดการโทร

ทั้งสองyieldและawaitต้องแยกวิธีการขึ้นอยู่กับการเกิดขึ้นเพื่อสร้างเครื่องสถานะโดยแต่ละสาขาของเครื่องจะเป็นตัวแทนของแต่ละส่วนของวิธีการ

คุณไม่ควรคิดถึงแนวคิดเหล่านี้ในคำ "ระดับล่าง" เช่นสแต็กเธรด ฯลฯ สิ่งเหล่านี้เป็นนามธรรมและการทำงานภายในของพวกเขาไม่ต้องการการสนับสนุนใด ๆ จาก CLR แต่เป็นเพียงคอมไพเลอร์ที่ทำเวทมนตร์ สิ่งนี้แตกต่างอย่างมากจากโครูทีนของ Lua ซึ่งมีการรองรับรันไทม์หรือlongjmpของ C ซึ่งเป็นเพียงมนต์ดำ


5
บันทึก Side : awaitไม่จำเป็นต้องใช้งาน สิ่งที่มีINotifyCompletion GetAwaiter()ก็เพียงพอแล้ว คล้ายกับว่าforeachไม่ต้องการIEnumerableอะไรIEnumerator GetEnumerator()ก็เพียงพอแล้ว
IllidanS4 ต้องการให้ Monica กลับมาใน
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.