C# 中定义Task的扩展类

发布时间 2023-09-26 14:37:57作者: Hello——寻梦者!

背景

在很多的时候我们代码中需要增加很多的Task对应的操作,这些操作很容易就分散在代码的各种地方从而造成大量的冗余,本篇文章主要是通过实际项目中用到的Task类进行一个归纳和总结,从而方便以后进行使用,我们代码中的返回值里面用到了一个称之为ActionResult的类作为返回的结果,所以在开始讲述TaskExtension之前会先来好好分析下这个ActionResult的用法,最后将会对整体上做一个总结。

代码分析

一 ActionResult

在很多场合下我们需要这样的一个类来返回我们执行某一个动作或者一系列动作的一个最终结果,通过这样一个sealed类我们就能够达到我们所需要的结果,比如执行一个动作是:成功、失败、超时、被拒绝、被取消、动作超时......这些,通过这样一个类就达到了封装整个动作执行的一个结果,如果自己的系统中有大量的是需要进行这类动作的执行,那么通过返回统一的ActionResult就能够统一所有的代码返回结果,这个是非常重要的,所以首先我们便需要来分析和理解这个过程,具体代码如下,可以进行具体的分析和使用。

public sealed class ActionResult
    {
        public bool IsSucceed { get; }
        public string Message { get; }
        public ResultType ResultType { get; }
        public bool HasError => ((ResultType & ResultType.Error) == ResultType.Error);
        public Dictionary<string, object> ResultData { get; }

        public ActionResult(bool result, string message, ResultType type) : this(result, message, type, null) { }

        public ActionResult(bool result, string message, ResultType type, Dictionary<string, object> data)
        {
            this.IsSucceed = result;
            this.Message = message;
            this.ResultType = type;
            this.ResultData = data;
        }

        /// <summary>
        /// Succeed status, ResultType is normal, message is empty.
        /// </summary>
        public static ActionResult Succeed => new ActionResult(true, string.Empty, ResultType.Normal);
        /// <summary>
        /// Succeed status, ResultType is Information, message is information.
        /// </summary>
        public static ActionResult SucceedWith(string information = "", Dictionary<string, object> data = null) => new ActionResult(true, information, ResultType.Information, data);
        /// <summary>
        /// Failed status, ResultType is error
        /// </summary>
        /// <param name="failMsg"> fail msg</param>
        public static ActionResult ErrorWith(string failMsg, Dictionary<string, object> data = null) => new ActionResult(false, failMsg, ResultType.Error, data);
        /// <summary>
        /// Failed status, ResultType is RepeatAction, message is empty.
        /// </summary>
        public static ActionResult RepeatAction => new ActionResult(false, string.Empty, ResultType.RepeatAction);
        /// <summary>
        /// Failed status, ResultType is RepeatAction, message is fail reason.
        /// </summary>
        public static ActionResult RepeatWith(string failMsg) => new ActionResult(false, failMsg, ResultType.RepeatAction);

        /// <summary>
        /// Failed status, indicate the action was rejected by remote, Result type is Reject, message is empty.
        /// </summary>
        public static ActionResult Reject => new ActionResult(false, string.Empty, ResultType.Reject);

        /// <summary>
        /// Failed status, indicate the action was rejected by remote, Result type is Reject.
        /// </summary>
        public static ActionResult RejectWith(string msg, Dictionary<string, object> data = null) => new ActionResult(false, msg, ResultType.Reject, data);
        /// <summary>
        /// Failed status, indicate the action was cancel by remote, Result type is Cancel.
        /// </summary>
        public static ActionResult CancelWith(string msg, Dictionary<string, object> data = null) => new ActionResult(false, msg, ResultType.Cancel, data);
        /// <summary>
        /// Failed status, indicate the action was timeout , Result type is timeout.
        /// </summary>
        public static ActionResult TimeOutWith(string msg, Dictionary<string, object> data = null) => new ActionResult(false, msg, ResultType.TimeOut, data);
        public bool Is(ResultType targetType)
        {
            return (this.ResultType & targetType) == targetType;
        }
    }

    [Flags]
    public enum ResultType
    {
        Normal = 0x01,
        Information = 0x01 << 1,

        RepeatAction = 0x01 << 2,
        Error = 0x01 << 3,

        Reject = 0x01 << 4,
        Cancel = 0x01 << 5,
        TimeOut = 0x01 << 6,

        Succeed = Normal | Information,
        Failed = RepeatAction | Error | Reject | Cancel | TimeOut
    }

    public static class ActionResultLogExtension
    {
        public static ActionResult WithLog(this ActionResult ar, Component component, string optTag = "")
        {
            string arMsg = ar.Message;
            if (string.IsNullOrEmpty(optTag) == false)
            {
                arMsg = $"[{optTag}]-[{arMsg}]";
            }
            switch (ar.ResultType)
            {
                case ResultType.Normal: component.LogInfo($"Action succeed."); break;
                case ResultType.Information: component.LogInfo($"Action succeed : {arMsg}"); break;
                case ResultType.RepeatAction: component.LogInfo($"Repeat action : {arMsg}"); break;
                case ResultType.Error: component.LogError($"Action was failed : {arMsg}"); break;
                case ResultType.Reject: component.LogError($"Action was reject, the reason is {arMsg}"); break;
                case ResultType.TimeOut: component.LogError($"Action was timeout, the reason is {arMsg}"); break;
                case ResultType.Cancel: component.LogError($"Action was cancel, the reason is {arMsg}"); break;
                default:
                    component.LogError($"Unknown action result type : " + ar.ResultType);
                    break;
            }

            return ar;
        }

        public static ActionResult Continue(this ActionResult ar, Action<ActionResult> action)
        {
            if (action != null)
            {
                try
                {
                    action(ar);
                }
                catch (Exception ex)
                {
                    string msg = $" | Execute continue action failed : [{action.Method.Name}, {ex.Message}]";
                    return new ActionResult(ar.IsSucceed, ar.Message + msg, ar.ResultType, ar.ResultData);
                }
            }
            return ar;
        }
    }

二 Task的扩展类TaskExtension

这个部分将主要包括几个方面:

2.1 Task中创建相互关联的Cancellation

我们知道在很多的Task中都支持Cancellation,而且这些Cancellation是可以通过一个静态方法CancellationTokenSource.CreateLinkedTokenSource方法来创建多个互相关联的Cancellation,这些互相关联的Cancellation中只要有一个触发了,那个整个Task就会被Cancel掉,所以下面的代码主要是完成这样一个统一的过程。

public static CancellationTokenSource CreateTimeoutCancellationTokenSource(this int millisecondsDelay, params CancellationToken[] linkedTokens)
        {
            if (millisecondsDelay <= 0) throw new ArgumentOutOfRangeException($"{millisecondsDelay} must >= 0");
            CancellationTokenSource cts = new CancellationTokenSource(millisecondsDelay);
            if (linkedTokens == null || false == linkedTokens.Any())
            {
                return cts;
            }
            CancellationToken[] tokens = new CancellationToken[linkedTokens.Length + 1];
            linkedTokens.CopyTo(tokens, 0);
            tokens[linkedTokens.Length] = cts.Token;
            return CancellationTokenSource.CreateLinkedTokenSource(tokens);
        }
2.2 Task中等待特定的结果返回

这个部分主要是围绕如何等待一个Func的结果,在我们的代码中我们可以在一个while的循环中使用Task.Delay来完成等待,当然在我们的代码中可以传入CancellationToken随时进行等待的取消,这些等待的方法中需要注意,有些是一直进行等待,有些还会加一个等待结果的超时时间超时后会退出当前等待的结果,这个需要注意代码中使用了一个DeviceTimer,这个是用于计时使用的,在之前的文章中有过相关的记载。

2.3 Task中等待执行结果的完成

这段代码其实内部是通过Task.WaitAll来完成的,这个实现的结果其实和Task.Wait(cancellationToken)的效果是一致的,这里添加的目的是为了统一代码的调用方,并且不需要关注这个Task执行是否出错,外部只需要拿到对应的结果就可以了。

public static bool WaitTaskResult(this Task<ActionResult> arTask, string optSource, out string failMsg, CancellationToken cancellationToken = default)
        {
            failMsg = string.Empty;
            try
            {
                Task.WaitAll(new[] { arTask }, cancellationToken);
                if (true == arTask.IsCompleted)
                {
                    ActionResult arResult = arTask.Result;
                    failMsg = arResult.Message;
                    return true == arResult.IsSucceed;
                }
                failMsg = $"{optSource} was canceled.";
                return false;
            }
            catch (Exception ex)
            {
                Log.Write(LogCategory.Error, optSource, "Wait for result of 'Task<ActionResult>' was failed : " + ex.Message);
                failMsg = ex.Message;
                return false;
            }
        }
2.4 其它Task中使用的一些小技巧

很多人对于这个方法的扩展很不理解,不知道是做什么用的,其实主要用于下面的一种场景,如果在同步代码中调用Task方法,如果当前代码没有async或者await标识,其实编译器会在这个方法下面加上波浪线进行提示,加上这个IgnoreWait以后,代码中的提示会消失,这个是使用Task时的一个小技巧。

[MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static void IgnoreWait(this Task task)
        { }
2.5 完整的代码展示
public static class TaskExtension
    {
        public const string MSG_TIME_OUT = "Task has time out!";
        public const string MSG_CANCELLED_BY_EXTERNAL = "Task was cancelled by external.";
        public const string MSG_ERROR_CONDITION_NOT_NULL = "condition cannot be null.";
        public const string MSG_ERROR_TIME_PERIOD_NOT_0 = "The timeout period cannot be zero";

        public static async Task<ActionResult> WaitConditionMeetAsync(this Func<Task<bool>> condition, string optName, int millsecsPeriod, int millisecsTimeout)
        {
            if (condition is null) throw new ArgumentNullException("Condition can not be null during waiting operation.");

            Log.Write(LogCategory.Information, "WaitConditionMeetAsync", $"Start wait for condition meeting of {optName}, Timeout setting is {millisecsTimeout} ms.");
            CancellationTokenSource cts = new CancellationTokenSource();
            cts.CancelAfter(millisecsTimeout);
            try
            {
                int delayTime = millsecsPeriod;
                while (true)
                {
                    if (cts.Token.IsCancellationRequested)
                    {
                        return ActionResult.ErrorWith($"Execute {optName} operation failed : timeout ({millisecsTimeout} ms)");
                    }
                    if (await condition.Invoke())
                    {
                        return ActionResult.Succeed;
                    }
                    await Task.Delay(delayTime);
                }
            }
            catch (Exception ex)
            {
                return ActionResult.ErrorWith(ex.Message);
            }
        }

        public static CancellationTokenSource CreateTimeoutCancellationTokenSource(this double secondsDelay, params CancellationToken[] linkedTokens)
        {
            return CreateTimeoutCancellationTokenSource((int)(secondsDelay * 1000), linkedTokens);
        }

        public static CancellationTokenSource CreateTimeoutCancellationTokenSource(this int millisecondsDelay, params CancellationToken[] linkedTokens)
        {
            if (millisecondsDelay <= 0) throw new ArgumentOutOfRangeException($"{millisecondsDelay} must >= 0");
            CancellationTokenSource cts = new CancellationTokenSource(millisecondsDelay);
            if (linkedTokens == null || false == linkedTokens.Any())
            {
                return cts;
            }
            CancellationToken[] tokens = new CancellationToken[linkedTokens.Length + 1];
            linkedTokens.CopyTo(tokens, 0);
            tokens[linkedTokens.Length] = cts.Token;
            return CancellationTokenSource.CreateLinkedTokenSource(tokens);
        }

        public static async Task<ActionResult> WaitConditionMeetAsync(this Func<bool> condition, string optName, int millsecsPeriod, CancellationToken cancellationToken)
        {
            if (condition is null) throw new ArgumentNullException("Condition can not be null during waiting operation.");
            if (cancellationToken == CancellationToken.None) throw new ArgumentException(nameof(cancellationToken));

            Log.Write(LogCategory.Information, "WaitConditionMeetAsync", $"Start wait for condition meeting of {optName}.");
            try
            {
                while (false == condition.Invoke())
                {
                    await Task.Delay(millsecsPeriod, cancellationToken);
                }
                return ActionResult.Succeed;
            }
            catch (TaskCanceledException)
            {
                return ActionResult.CancelWith($"Execute {optName} was canceled or timeout.");
            }
            catch (Exception ex)
            {
                return ActionResult.ErrorWith(ex.Message);
            }
        }

        public static async Task<ActionResult> WaitConditionMeetAsync(this Func<bool> condition, string optName, int millsecsPeriod, int millisecsTimeout, CancellationToken cancellationToken = default)
        {
            if (condition is null) throw new ArgumentNullException("Condition can not be null during waiting operation.");

            Log.Write(LogCategory.Information, "WaitConditionMeetAsync", $"Start wait for condition meeting of {optName}, Timeout setting is {millisecsTimeout} ms.");

            DeviceTimer deviceTimer = new DeviceTimer();
            deviceTimer.Start(millisecsTimeout);
            try
            {
                int delayTime = millsecsPeriod;
                while (true)
                {
                    if (condition.Invoke())
                    {
                        return ActionResult.Succeed;
                    }

                    if (deviceTimer.IsTimeout())
                    {
                        return ActionResult.ErrorWith($"Execute {optName} operation failed : timeout ({millisecsTimeout} ms)");
                    }

                    if (cancellationToken != default && true == cancellationToken.IsCancellationRequested)
                    {
                        return ActionResult.CancelWith($"Execute {optName} was canceled.");
                    }

                    await Task.Delay(delayTime, cancellationToken);
                }
            }
            catch (TaskCanceledException)
            {
                return ActionResult.CancelWith($"Execute {optName} was canceled.");
            }
            catch (Exception ex)
            {
                return ActionResult.ErrorWith(ex.Message);
            }
        }

        public static bool WaitTaskResult(this Task<ActionResult> arTask, string optSource, out string failMsg, CancellationToken cancellationToken = default)
        {
            failMsg = string.Empty;
            try
            {
                Task.WaitAll(new[] { arTask }, cancellationToken);
                if (true == arTask.IsCompleted)
                {
                    ActionResult arResult = arTask.Result;
                    failMsg = arResult.Message;
                    return true == arResult.IsSucceed;
                }
                failMsg = $"{optSource} was canceled.";
                return false;
            }
            catch (Exception ex)
            {
                Log.Write(LogCategory.Error, optSource, "Wait for result of 'Task<ActionResult>' was failed : " + ex.Message);
                failMsg = ex.Message;
                return false;
            }
        }

        private struct Void
        { } // just because TaskCompletionSource class has not generic version

        private static async Task<TResult> WithCancellation<TResult>(this Task<TResult> originalTask, CancellationToken ct)
        {
            var cancelTask = new TaskCompletionSource<Void>();
            // once CancellationToken was cancelled, complete the task
            using (ct.Register(t => ((TaskCompletionSource<Void>)t).TrySetResult(new Void()), cancelTask))
            {
                Task any = await Task.WhenAny(originalTask, cancelTask.Task);
                if (any == cancelTask.Task) ct.ThrowIfCancellationRequested();
            }
            return await originalTask;
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static void IgnoreWait(this Task task)
        { }

        // 有返回值
        public static async Task<TResult> TimeoutAfter<TResult>(this Task<TResult> task, TimeSpan timeout)
        {
            using (var timeoutCancellationTokenSource = new CancellationTokenSource())
            {
                var completedTask = await Task.WhenAny(task, Task.Delay(timeout, timeoutCancellationTokenSource.Token));
                if (completedTask == task)
                {
                    timeoutCancellationTokenSource.Cancel();
                    return await task;  // Very important in order to propagate exceptions
                }
                else
                {
                    throw new TimeoutException("The operation has timed out.");
                }
            }
        }

        // 无返回值
        public static async Task TimeoutAfter(this Task task, TimeSpan timeout)
        {
            using (var timeoutCancellationTokenSource = new CancellationTokenSource())
            {
                var completedTask = await Task.WhenAny(task, Task.Delay(timeout, timeoutCancellationTokenSource.Token));
                if (completedTask == task)
                {
                    timeoutCancellationTokenSource.Cancel();
                    await task;  // Very important in order to propagate exceptions
                }
                else
                {
                    throw new TimeoutException("The operation has timed out.");
                }
            }
        }
    }