C#中的 Awaiter

1、背景知识

(1)接口 INotifyCompletion

public interface INotifyCompletion
{
    void OnCompleted(Action continuation);
}
View Code

实现这个接口的实例(例如一个 Task 对象),在任务完成后,调用 OnCompleted 方法,以执行后续的委托(continuation)。

(2)关键字 await 

关键字 await 是一个运算符,它的操作数是一个实现了 GetAwaiter 方法的实例。该方法返回一个 Awaiter。

internal partial class Program
{
    //调用方线程
    private static async void ToTest6()
    {
        await new Test6();
    }
}

internal class Test6
{
    public MyAwaiter GetAwaiter()
    {
        return new MyAwaiter();
    }
}
View Code

2、Awaiter

要实现一个 Awaiter,必须实现 INotifyCompletion 接口、IsCompleted 属性和 GetResult 方法

public class MyAwaiter : INotifyCompletion
{
    public bool IsCompleted { get; set; }

    public void GetResult()
    {
        //throw new NotImplementedException();
    }

    public void OnCompleted(Action continuation)
    {
        //throw new NotImplementedException();
    }
}

3、执行流程

(1)先实现一个方法,用于输出当前线程Id,以及调用时给的标记

internal partial class Test6
{
    public static void PrintThreadInfo(string callTag)
    {
        //输出当前线程Id,以及调用时给的标记
        Console.WriteLine($" Thread Id:{Environment.CurrentManagedThreadId,2} - {callTag}");
    }
}
View Code

(2)测试代码

在 MyAwaier 中的重要位置都输出调用时给的标记:

public class MyAwaiter : INotifyCompletion
{
    private bool _isCompleted = true;
    public bool IsCompleted
    {
        get
        {
            Test6.PrintThreadInfo($"Get IsCompleted:{_isCompleted}");
            return _isCompleted;
        }
        set
        {
            Test6.PrintThreadInfo($"Set IsCompleted:{value}");
            _isCompleted = value;
        }
    }

    public void GetResult()
    {
        Test6.PrintThreadInfo("Get Result");
    }

    public void OnCompleted(Action continuation)
    {
        Test6.PrintThreadInfo("On Completed");
    }
}
View Code

实现一个可作为 await 运算符的操作数的类:

internal partial class Test6
{
    public MyAwaiter GetAwaiter()
    {
        PrintThreadInfo("Get a awaiter");
        return new MyAwaiter();
    }
}
View Code

对它测试调用:

internal partial class Program
{
    static void Main(string[] args)
    {
        Test6.PrintThreadInfo("Main");
        Task.Run(() =>
        {
            ToTest6();
        });

        Console.ReadKey();
    }
    
    private static async void ToTest6()
    {
        Test6.PrintThreadInfo("before await");
        await new Test6();
        Test6.PrintThreadInfo("after await");
    }
}
View Code

运行结果:

结果分析:貌似一切正常,除了 OnCompleted 方法未被调用外。

A、将 _isCompleted 设置为 false,即:private bool _isCompleted = false;

 运行结果: 

结果分析:未见“Get Result”和“after await”,即:Awaiter 的 GetResult 方法 以及 调用方 await 之后的代码未被执行——线程被挂起了【异常】。

B、修改 INotifyCompletion 接口的实现,添加对参数 continuation 进行是否为 null 的判断,如果不为 null,则调用:

public void OnCompleted(Action continuation)
{
    Test6.PrintThreadInfo("On Completed");
    if (continuation != null)
    {
        Test6.PrintThreadInfo($"continuation is not null");
        continuation.Invoke();
    }
}
View Code

运行结果:

结果分析:

  • 对 continuation 的调用,可以使 Awaiter 的 GetResult 方法 以及 调用方 await 之后的代码被执行;
  • 但并未见“Set IsCompleted”,可见在代码全部跑完之后,IsCompleted 属性仍然为 false【应设置为 true】;
  • 结合之前“_isCompleted 为 true 时的测试结果”,可知,在 IsCompleted 为 true 时,GetResult 方法 以及 调用方 await 之后的代码会立即被调用。

C、再次修改 INotifyCompletion 接口的实现,执行任务之后,将 IsCompleted 属性设置为 true,并调用参数 continuation:

public void OnCompleted(Action continuation)
{
    Test6.PrintThreadInfo("On Completed");
    //if (continuation != null)
    //{
    //    Test6.PrintThreadInfo($"continuation is not null");
    //    continuation.Invoke();
    //}

    Task.Run(() =>
    {
        Test6.PrintThreadInfo("Task Run");
        Thread.Sleep(1000);
    }).ContinueWith((a) =>
    {
        IsCompleted = true;
        continuation.Invoke();
    });
}
View Code

运行结果:

 结果分析:Id 为 9 的线程果然被挂起,而由新线程 6 继续之后的代码,看起来一切正常。

(3)测试结论

根据上述测试代码,可以初步得出结论:

  • Awaiter 的 GetResult 方法 以及 调用方 await 之后的代码,被自动塞进了委托 continuation 中;
  • Awaiter 初始状态 IsCompleted 属性为 true 时,GetResult 方法 以及 调用方 await 之后(下一个 await 之前)的代码 立即被执行;
  • Awaiter 初始状态 IsCompleted 属性为 false 时,在异步任务结束后,将 IsCompleted 属性设置为 true,并调用委托 continuation,否则线程将被挂起。

测试代码的执行顺序如下:

  • 执行调用方 await 之前的代码;
  • 执行 GetAwaiter 方法;
  • 读取属性 IsCompleted;
    • 如果为 true,则执行 GetResult 方法;
      • 执行调用方 await 之后的代码;
    • 如果为 false,则执行 OnCompleted 方法;
      • 挂起线程,等待任务完成
      • 任务完成之后,执行 GetResult 方法;
      • 执行调用方 await 之后的代码。

4、改进代码

目的:使 Test6 成为一个类似任务(Task-Like)的类。

(1)根据执行流程测试代码的结论,对测试代码做如下改进:

  • 合并 Test6 与 MyAwaier,使它们成为一个类;
  • 添加一个参数为 Task 的构造函数,使 Awaiter 可以执行指定的任务;
    构造函数的参数也可以是委托或方法。
    这根据需要来,哪个方便就按哪个来,此处使用任务、方法作为构造函数的参数;
  • 添加一个工作任务(Work 方法);
  • 修改 GetResult 方法,让它返回一个整数。
internal partial class Test6 : INotifyCompletion
{
    private readonly Func<int> _func = null!;

    public Test6(Func<int> func)
    {
        _func = func;
    }

    public Test6 GetAwaiter()
    {
        PrintThreadInfo("Get a awaiter");
        return this;
    }

    private bool _isCompleted = false;

    public bool IsCompleted
    {
        get
        {
            PrintThreadInfo($"Get IsCompleted:{_isCompleted}");
            return _isCompleted;
        }
        set
        {
            PrintThreadInfo($"Set IsCompleted:{value}");
            _isCompleted = value;
        }
    }

    private int _result = 0;

    public int GetResult()
    {
        PrintThreadInfo("Get Result");
        return _result;
    }

    public void OnCompleted(Action continuation)
    {
        PrintThreadInfo("On Completed");

        ThreadPool.QueueUserWorkItem(state =>
        {
            PrintThreadInfo($"Task Run at {DateTime.Now}");
            _result = _func.Invoke();

            IsCompleted = true;
            continuation.Invoke();
        });
    } 

    public static void PrintThreadInfo(string callTag)
    {
        //输出当前线程Id,以及调用时给的标记
        Console.WriteLine($" Thread Id:{Environment.CurrentManagedThreadId,2} - {callTag}");
    }
}
View Code

调用并输出结果:

internal partial class Program
{
    static void Main(string[] args)
    {
        Test6.PrintThreadInfo("Main");
        Task.Run(() =>
        {
            ToTest6();
        });

        Console.ReadKey();
    }
    
    private static async void ToTest6()
    {
        Test6.PrintThreadInfo("before await");
        int awaitResult = await new Test6(Work);
        Test6.PrintThreadInfo($"after await:{awaitResult}");
    }

    private static int Work()
    {
        Thread.Sleep(2000);
        Test6.PrintThreadInfo($"Work finished at {DateTime.Now}");
        return 1;
    }
}
View Code

运行结果:

 结果分析:Id 为 6 的线程被挂起,而由新线程 10 继续之后的代码,看起来一切正常。

(2)使用泛型改进,并可异步启动(具有 Start 方法):

public interface IAwaiter<out T> : INotifyCompletion
{
    bool IsCompleted { get; }

    T GetResult();
}

public interface IAwaitable<out T>
{
    IAwaiter<T> GetAwaiter();
}

internal class Test6<T> : IAwaitable<T>, IAwaiter<T>
{
    private readonly Task<T> _task = null!;

    public Test6(Task<T> task)
    {
        _task = task;
    }

    public Test6(Func<T> func)
    {
        _task = new Task<T>(func);
    }

    public void Start()
    {
        _task.ContinueWith(t =>
        {
            _isCompleted = true;
            _result = _task.Result;
        });
        _task.Start();
    }

    public IAwaiter<T> GetAwaiter()
    {
        TestHelper.PrintThreadInfo("Get a awaiter");
        return this;
    }

    private bool _isCompleted = false;

    public bool IsCompleted
    {
        get
        {
            TestHelper.PrintThreadInfo($"Get IsCompleted:{_isCompleted}");
            return _isCompleted;
        }
        set
        {
            TestHelper.PrintThreadInfo($"Set IsCompleted:{value}");
            _isCompleted = value;
        }
    }

    private T _result = default!;

    public T GetResult()
    {
        TestHelper.PrintThreadInfo("Get Result");
        return _result;
    }

    public void OnCompleted(Action continuation)
    {
        TestHelper.PrintThreadInfo("On Completed");

        if (!_isCompleted)
        {
            if (_task.Status == TaskStatus.Created)
            {
                Start();
            }
            _task.Wait();
        }
        continuation.Invoke();
    }
}
View Code

注:将 PrintThreadInfo 方法移到了 TestHelper 类中。

调用并输出结果:

internal partial class Program
{
    static void Main(string[] args)
    {
        TestHelper.PrintThreadInfo("Main");
        Task.Run(() =>
        {
            ToTest6();
        });

        Console.ReadKey();
    }
    
    //调用方线程
    private static async void ToTest6()
    {
        var test1 = new Test6<string>(Work1);
        var test2 = new Test6<int>(Work2);

        test1.Start();
        //test2.Start();

        TestHelper.PrintThreadInfo("before await1");
        string awaitResult1 = await test1;
        TestHelper.PrintThreadInfo($"after await1:{awaitResult1}");

        TestHelper.PrintThreadInfo("before await2");
        int awaitResult2 = await test2;
        TestHelper.PrintThreadInfo($"after await2:{awaitResult2}");
    }

    private static string Work1()
    {
        TestHelper.PrintThreadInfo($"Work1 started at {DateTime.Now}");
        Thread.Sleep(2000);
        return $"Work finished at {DateTime.Now}";
    }

    private static int Work2()
    {
        TestHelper.PrintThreadInfo($"Work2 started at {DateTime.Now}");
        return 1;
    }
}

public static class TestHelper
{
    public static void PrintThreadInfo(string callTag)
    {
        //输出当前线程Id,以及调用时给的标记
        Console.WriteLine($" Thread Id:{Environment.CurrentManagedThreadId,2} - {callTag}");
    }
}
View Code

运行结果:

 结果分析:

  • Id 为 6 的线程执行到第一个 await 的 OnCompleted 方法时被挂起;
  • test1 的任务 Work1 由新线程 9 完成任务;
  • Work1 完成后,线程 6 继续之后的代码,直到下一个await (的 OnCompleted 方法)时被挂起。
  • 线程池中线程 9 复用,用于执行 test2 的任务 Work2;
  • Work2 完成后,线程 6 继续之后的代码。

至此,一个简单的具有泛型能力的类似任务(Task-Like)的类 Test6 实现完成。

5、总结

(1)要实现一个 Awaiter,必须实现 INotifyCompletion 接口、IsCompleted 属性和 GetResult 方法。

(2)关键字 await 是一个运算符,它的操作数是一个实现了 GetAwaiter 方法的实例。

(3)Awaiter 的 GetResult 方法 以及 调用方 await 之后(下一个 await 之前)的代码,被自动塞进了一个委托中(continuation)。 

(4)Awaiter 的 IsCompleted 属性为 true 时,“GetResult 方法 以及 调用方 await 之后(下一个 await 之前)的代码” 会立即被执行,并不执行 OnCompleted 方法;
   Awaiter 的 IsCompleted 属性为 false 时,立即执行 OnCompleted 方法,并且通过委托(continuation)的调用,来执行“GetResult 方法 以及 调用方 await 之后(下一个 await 之前)的代码”;
   如果不调用委托(continuation)将导致线程被挂起,程序无法进行下去,而成为一个 BUG。

posted @ 2023-12-12 16:50  误会馋  阅读(236)  评论(0编辑  收藏  举报