54、异步编程模式详解

异步编程模式详解

1. 使用TPL调用APM

虽然TPL能显著简化对长时间运行方法的异步调用,但通常使用API提供的APM方法比针对同步版本编写TPL代码更好。这是因为API开发者最清楚如何编写最高效的线程代码、同步哪些数据以及使用何种同步方式。幸运的是,TPL的 TaskFactory 有专门用于调用APM方法的特殊方法。

1.1 TPL与CPS结合使用APM

TPL的 FromAsync 有一组重载方法用于调用APM。以下示例展示了如何使用TPL调用APM,同时支持下载多个URL:

using System;
using System.IO; 
using System.Net; 
using System.Linq; 
using System.Threading.Tasks; 
using System.Collections.Generic;

public class Program 
{
    static private object ConsoleSyncObject = new object();

    public static void Main(string[] args)
    {
        string[] urls = args;
        if (args.Length == 0)
        {
            urls = new string[]  
            {
                "http://www.habitat-spokane.org",
                "http://www.partnersintl.org",
                "http://www.iassist.org",
                "http://www.fh.org",
                "http://www.worldvision.org"
            };
        }
        int line = 0;
        Task<WebResponse>[] tasksWithState = urls.Select(
            url => DisplayPageSizeAsync(url, line++)).ToArray();

        while (!Task.WaitAll(tasksWithState.ToArray(), 50))
        {
            DisplayProgress(tasksWithState);
        }
        Console.SetCursorPosition(0, line);
    }

    private static Task<WebResponse> DisplayPageSizeAsync(string url, int line)
    {
        lock (ConsoleSyncObject)
        {
            Console.WriteLine(url);
        }
        WebRequest webRequest = WebRequest.Create(url);
        WebRequestState state = new WebRequestState(webRequest, line);
        Task<WebResponse> task = Task<WebResponse>.Factory.FromAsync(
            webRequest.BeginGetResponse, 
            GetResponseAsyncCompleted, state);
        return task;
    }

    private static WebResponse GetResponseAsyncCompleted(IAsyncResult asyncResult)
    {
        WebRequestState completedState = (WebRequestState)asyncResult.AsyncState;
        HttpWebResponse response = (HttpWebResponse)completedState.WebRequest.EndGetResponse(asyncResult);
        Stream stream = response.GetResponseStream();
        using (StreamReader reader = new StreamReader(stream))
        {
            int length = reader.ReadToEnd().Length;
            DisplayPageSize(completedState, length);
        }
        return response;
    }

    private static void DisplayProgress(IEnumerable<Task<WebResponse>> tasksWithState)
    {
        foreach (WebRequestState state in tasksWithState
            .Where(task => !task.IsCompleted)
            .Select(task => (WebRequestState)task.AsyncState))
        {
            DisplayProgress(state);
        }
    }

    private static void DisplayPageSize(WebRequestState completedState, int length)
    {
        lock (ConsoleSyncObject)
        {
            Console.SetCursorPosition(completedState.ConsoleColumn, completedState.ConsoleLine);
            Console.Write(FormatBytes(length));
            completedState.ConsoleColumn += length.ToString().Length;
        }
    }

    private static void DisplayProgress(WebRequestState state)
    {
        int left = state.ConsoleColumn;
        int top = state.ConsoleLine;
        lock (ConsoleSyncObject)
        {
            if (left >= Console.BufferWidth - int.MaxValue.ToString().Length)
            {
                left = state.Url.Length;
                Console.SetCursorPosition(left, top);
                Console.Write("".PadRight(Console.BufferWidth - state.Url.Length));
                state.ConsoleColumn = left;
            }
            else
            {
                state.ConsoleColumn++;
            }
            Console.SetCursorPosition(left, top);
            Console.Write('.');
        }
    }

    static public string FormatBytes(long bytes)
    {
        string[] magnitudes = new string[] { "GB", "MB", "KB", "Bytes" };
        long max = (long)Math.Pow(1024, magnitudes.Length);
        return string.Format("{1:##.##} {0}",
            magnitudes.FirstOrDefault(magnitude => bytes > (max /= 1024))?? "0 Bytes",
            (decimal)bytes / (decimal)max).Trim();
    } 
}

class WebRequestState 
{
    public WebRequestState(WebRequest webRequest, int line)
    {
        WebRequest = webRequest;
        ConsoleLine = line;
        ConsoleColumn = Url.Length + 1;
    }

    public WebRequestState(WebRequest webRequest)
    {
        WebRequest = webRequest;
    }

    public WebRequest WebRequest { get; private set; }

    public string Url 
    {
        get
        {
            return WebRequest.RequestUri.ToString();
        }
    }

    public int ConsoleLine { get; set; }
    public int ConsoleColumn { get; set; } 
}

Task 与APM方法对连接起来相对容易。上述代码中使用的重载方法接受三个参数:
- 第一个是 BeginX 方法委托(如 webRequest.BeginGetResponse )。
- 第二个是与 EndX 方法匹配的委托。虽然可以直接使用 EndX 方法(如 webRequest.EndGetResponse ),但传递一个委托(如 GetResponseAsyncCompleted )并使用CPS可以执行额外的完成活动。
- 最后一个参数是与 BeginX 方法接受的类似的状态参数。

使用TPL调用APM方法对的一个优点是,我们不必担心发出 AsyncCallback 方法结束的信号,而是监视 Task 是否完成。因此, WebRequestState 不再需要包含 ManualResetEventSlim

1.2 使用TPL和 ContinueWith() 调用APM

调用 TaskFactory.FromAsync() 的另一个选择是直接传递 EndX 方法,然后使用 ContinueWith() 处理任何后续代码。这种方法的优点是可以查询 continueWithTask 参数的结果( continueWithTask.Result ),而不是通过异步状态对象或使用闭包和匿名委托来存储访问 EndX 方法的方式。

private static Tuple<Task<WebResponse>, WebRequestState> DisplayPageSizeAsync(string url, int line)
{
    lock (ConsoleSyncObject)
    {
        Console.WriteLine(url);
    }
    WebRequest webRequest = WebRequest.Create(url);
    WebRequestState state = new WebRequestState(url, line);

    Task<WebResponse> task = Task<WebResponse>.Factory.FromAsync(
        webRequest.BeginGetResponse, 
        webRequest.EndGetResponse, state)
    .ContinueWith(continueWithTask =>
    {
        // Optional since state is available with closure
        WebRequestState completedState = (WebRequestState)continueWithTask.AsyncState;
        Stream stream = continueWithTask.Result.GetResponseStream();
        using (StreamReader reader = new StreamReader(stream))
        {
            int length = reader.ReadToEnd().Length;
            DisplayPageSize(completedState, length);
        }
        return continueWithTask.Result;
    });

    return new Tuple<Task<WebResponse>, WebRequestState>(task, state);
}

然而, ContinueWith() 方法也有一个问题。 ContinueWith() 返回的 Task AsyncState 属性包含 null ,而不是调用 FromAsync() 时指定的状态。要在 ContinueWith() 之外访问状态,需要将其保存到另一个位置,上述代码通过将其放入 Tuple<T1, T2> 并返回该元组来实现。

2. 异步委托调用

有一种派生的APM模式称为异步委托调用,它利用了所有委托数据类型上C#编译器生成的特殊代码。例如,对于 Func<string, int> 类型的委托实例,有一个APM方法对可用:

System.IAsyncResult BeginInvoke(string arg, AsyncCallback callback, object @object) 
int EndInvoke(IAsyncResult result)

这意味着可以通过使用C#编译器生成的方法同步调用任何委托(因此也可以调用任何方法)。

不幸的是,异步委托调用模式使用的底层技术是分布式编程中不再进一步发展的远程处理技术。虽然Microsoft仍然支持异步委托调用的使用,并且在可预见的未来它将继续像现在这样工作,但与其他方法(如 Thread ThreadPool 和TPL)相比,其性能特征欠佳。因此,开发者应倾向于选择这些替代方法,而不是使用异步委托调用API进行新的开发。

以下是异步委托调用的详细代码示例:

using System;

public class Program 
{
    public static void Main(string[] args)
    {
        Console.WriteLine("Application started....");
        Console.WriteLine("Starting thread....");

        Func<int, string> workerMethod = PiCalculator.Calculate;
        IAsyncResult asyncResult = workerMethod.BeginInvoke(500, null, null);

        while (!asyncResult.AsyncWaitHandle.WaitOne(100, false))
        {
            Console.Write('.');
        }
        Console.WriteLine();
        Console.WriteLine("Thread ending....");

        Console.WriteLine(workerMethod.EndInvoke(asyncResult));
        Console.WriteLine("Application shutting down....");
    } 
}

异步编程模式的高级应用与其他模式

3. 异步委托调用的详细解析

在异步委托调用中,不需要显式引用 Task Thread ,而是使用委托实例和编译器生成的 BeginInvoke() EndInvoke() 方法,这些方法的实现会从线程池中请求线程。以下是具体步骤:
1. 定义委托并赋值 :在 Main 方法中,定义一个 Func<int, string> 类型的委托 workerMethod ,并将其指向 PiCalculator.Calculate 方法。

Func<int, string> workerMethod = PiCalculator.Calculate;
  1. 调用 BeginInvoke 方法 :使用 BeginInvoke 方法启动 PiCalculator.Calculate 方法在一个线程池线程上执行,并立即返回。这样可以让其他代码与 PiCalculator.Calculate 方法并行运行。
IAsyncResult asyncResult = workerMethod.BeginInvoke(500, null, null);
  1. 轮询委托状态 :使用 IAsyncResult.AsyncWaitHandle.WaitOne() 方法轮询委托的状态,在 PiCalculator.Calculate 方法执行期间,每秒在屏幕上打印一个点作为进度指示。
while (!asyncResult.AsyncWaitHandle.WaitOne(100, false))
{
    Console.Write('.');
}
  1. 调用 EndInvoke 方法 :当等待句柄发出信号时,调用 EndInvoke 方法获取结果。注意,必须将调用 BeginInvoke 时返回的 IAsyncResult 引用传递给 EndInvoke 方法。在这个例子中,由于在 while 循环中轮询线程状态,只有在线程完成后才调用 EndInvoke ,所以 EndInvoke 不会阻塞。
Console.WriteLine(workerMethod.EndInvoke(asyncResult));
3.1 数据传递

在异步委托调用中,数据的传递非常简单,它与同步方法的签名一致。例如,在上述例子中,传递了一个整数参数并接收一个字符串结果,符合 Func<int, string> 的签名。对于包含 out ref 参数的委托类型, BeginInvoke 方法与委托签名匹配,但增加了 AsyncCallback object 参数,用于指定回调和传递状态对象。 EndInvoke 方法只包含输出参数,并且其返回值与原始委托的返回值匹配。

4. 基于事件的异步模式(EAP)

EAP是一种比APM更适用于高级编程的模式。API开发者会为长时间运行的方法实现EAP。实现EAP模式的最简单形式是复制一个长时间运行的方法签名,在方法名后添加“Async”,并移除任何输出参数和返回值。“Async”后缀向调用者表明该方法的这个版本将异步执行,而不是阻塞直到方法工作完成。由于调用结束时方法不一定完成,所以需要移除输出参数。

例如,对于 string PiCalculator.Calculate(int digits) 方法,其EAP调用约定的签名为:

void PiCalculator.CalculateAsync(int digits)

与APM不同,EAP模型不需要返回 IAsyncResult 对象。但API实现者可以通过添加一个对象状态参数来支持传递任意状态:

void PiCalculator.CalculateAsync(int digits, object state)

甚至可以有泛型版本:

void PiCalculator.CalculateAsync<T>(int digits, T state)

在.NET Framework 4中,支持 CancellationToken 的版本也是受欢迎的。以下是一个实现EAP的示例代码:

using System;
using System.ComponentModel; 
using System.Threading; 
using System.Threading.Tasks;

partial class PiCalculation 
{
    public void CalculateAsync(int digits)
    {
        CalculateAsync(digits, null);
    }

    public void CalculateAsync(int digits, object userState)
    {
        CalculateAsync(digits, default(CancellationToken), userState);
    }

    public void CalculateAsync<TState>(int digits, CancellationToken cancelToken, TState userState)
    {
        if (SynchronizationContext.Current == null)
        {
            SynchronizationContext.SetSynchronizationContext(new SynchronizationContext());
        }
        TaskScheduler scheduler = TaskScheduler.FromCurrentSynchronizationContext();
        Task<string>.Factory.StartNew(
            () =>
            {
                return PiCalculator.Calculate(digits);
            }, cancelToken)
        .ContinueWith<string>(
            continueTask =>
            {
                CalculateCompleted(typeof(PiCalculator),
                    new CalculateCompletedEventArgs(
                        continueTask.Result,
                        continueTask.Exception,
                        cancelToken.IsCancellationRequested,
                        userState));
                return continueTask.Result;
            }, scheduler);
    }

    public event EventHandler<CalculateCompletedEventArgs> CalculateCompleted = delegate { };

    public class CalculateCompletedEventArgs : AsyncCompletedEventArgs
    {
        public CalculateCompletedEventArgs(string value, Exception error, bool cancelled, object userState) : base(error, cancelled, userState)
        {
            Result = value;
        }

        public string Result { get; private set; }
    }
}

在上述代码中,通过 CalculateCompleted 事件提供了支持。注册这个事件可以让调用者在计算完成时收到通知。计算结果将存储在 CalculateCompletedEventArgs 类的 Result 属性中,该类派生自 AsyncCompletedEventArgs 。这个类还允许调用者检查错误(通过 Error 属性)、取消状态(通过 Canceled 属性)和用户状态(通过 UserState 属性)。

在过去,EAP中的取消支持是通过添加 CancelAsync 方法实现的,该方法可以选择接受一个对象状态参数。但在.NET Framework 4中,使用 CancellationToken 是首选方法,因为它可以避免保存状态的需要。

5. 后台工作者模式

后台工作者模式是EAP的一种具体实现,它提供操作状态和取消的可能性。.NET Framework 2.0(或更高版本)包含一个 BackgroundWorker 类,用于编程这种类型的模式。以下是一个使用后台工作者模式计算指定位数圆周率的示例代码:

using System;
using System.Threading; 
using System.ComponentModel; 
using System.Text;

public class PiCalculator
{
    public static AutoResetEvent resetEvent = new AutoResetEvent(false);

    public static void Main()
    {
        int digitCount;
        Console.Write("Enter the number of digits to calculate:");
        if (int.TryParse(Console.ReadLine(), out digitCount))
        {
            Console.WriteLine("ENTER to cancel");

            BackgroundWorker calculationWorker = new BackgroundWorker();
            calculationWorker.DoWork += CalculatePi;
            calculationWorker.ProgressChanged += UpdateDisplayWithMoreDigits;
            calculationWorker.WorkerReportsProgress = true;
            calculationWorker.RunWorkerCompleted += new RunWorkerCompletedEventHandler(Complete);
            calculationWorker.WorkerSupportsCancellation = true;

            calculationWorker.RunWorkerAsync(digitCount);

            Console.ReadLine();
            calculationWorker.CancelAsync();
            resetEvent.WaitOne();
        }
        else
        {
            Console.WriteLine("The value entered is an invalid integer.");
        }
    }

    private static void CalculatePi(object sender, DoWorkEventArgs eventArgs) 
    {
        int digits = (int)eventArgs.Argument;
        StringBuilder pi = new StringBuilder("3.", digits + 2);
        calculationWorker.ReportProgress(0, pi.ToString());

        if (digits > 0)
        {
            for (int i = 0; i < digits; i += 9)
            {
                int nextDigit = PiDigitCalculator.StartingAt(i + 1);
                int digitCount = Math.Min(digits - i, 9);
                string ds = string.Format("{0:D9}", nextDigit);
                pi.Append(ds.Substring(0, digitCount));
                calculationWorker.ReportProgress(0, ds.Substring(0, digitCount));

                if (calculationWorker.CancellationPending)
                {
                    eventArgs.Cancel = true;
                    break;
                }
            }
        }
        eventArgs.Result = pi.ToString();
    }

    private static void UpdateDisplayWithMoreDigits(object sender, ProgressChangedEventArgs eventArgs)
    {
        // 此处代码原文档未给出完整,可根据实际需求补充
    }
}

总结

  • TPL与APM结合 :使用TPL调用APM方法能简化异步编程,同时利用API开发者的优化。
  • 异步委托调用 :虽然方便,但性能欠佳,建议优先选择其他方法。
  • EAP模式 :适用于高级编程,通过事件提供异步操作的通知和状态管理。
  • 后台工作者模式 :提供操作状态和取消功能,适合长时间运行的任务。

在实际开发中,应根据具体需求选择合适的异步编程模式,以提高程序的性能和可维护性。

异步编程模式的对比与实际应用考量

6. 各异步编程模式的对比

不同的异步编程模式具有各自的特点和适用场景,以下通过表格形式对APM、异步委托调用、EAP和后台工作者模式进行对比:
| 模式 | 优点 | 缺点 | 适用场景 |
| ---- | ---- | ---- | ---- |
| APM(异步编程模型) | - 与API原生支持较好,开发者可利用API开发者的优化代码
- 可与TPL结合,简化异步调用 | - 代码复杂度相对较高,需要处理 IAsyncResult 和回调函数 | - 当API提供APM方法时,优先使用
- 需要对异步操作进行精细控制的场景 |
| 异步委托调用 | - 语法简单,可通过编译器生成的方法轻松实现异步调用 | - 底层技术为远程处理,性能欠佳
- 不适合大规模并发场景 | - 简单的异步任务,对性能要求不高的场景 |
| EAP(基于事件的异步模式) | - 适用于高级编程,通过事件机制提供简洁的异步操作通知和状态管理
- 支持传递用户状态和取消操作 | - 需要额外实现事件处理代码 | - 长时间运行的任务,需要实时获取任务状态和结果的场景 |
| 后台工作者模式 | - 提供操作状态和取消功能,代码结构清晰,易于实现 | - 功能相对固定,灵活性不如其他模式 | - 长时间运行的任务,需要实时更新进度和支持取消操作的场景 |

7. 实际应用中的选择流程

在实际开发中,选择合适的异步编程模式至关重要。以下是一个选择异步编程模式的流程图:

graph TD;
    A[开始] --> B{是否API提供APM方法?};
    B -- 是 --> C[使用APM与TPL结合];
    B -- 否 --> D{是否简单异步任务且对性能要求不高?};
    D -- 是 --> E[使用异步委托调用];
    D -- 否 --> F{是否长时间运行任务且需要实时状态和结果?};
    F -- 是 --> G[使用EAP模式];
    F -- 否 --> H{是否需要实时进度更新和取消操作?};
    H -- 是 --> I[使用后台工作者模式];
    H -- 否 --> J[根据具体需求选择其他合适方式];
    C --> K[结束];
    E --> K;
    G --> K;
    I --> K;
    J --> K;
8. 异步编程中的同步问题

在异步编程中,当涉及多线程对共享资源(如控制台输出)的操作时,需要进行同步以避免数据竞争和不一致的问题。例如,在使用TPL调用APM的示例中,由于多个线程可能同时操作控制台的光标位置和写入文本,因此需要使用 lock 语句进行同步。以下是同步控制台操作的代码示例:

static private object ConsoleSyncObject = new object();

// 在移动光标或写入控制台时使用lock语句
lock (ConsoleSyncObject)
{
    Console.WriteLine(url);
}

使用 lock 语句可以确保在同一时间只有一个线程能够执行锁定块内的代码,从而保证操作的原子性。即使是单行的 Console.WriteLine() 语句也被 lock 包围,以防止它们中断其他非原子操作的代码块。

代码示例优化建议

9. 代码优化方向

在实际开发中,可以对上述代码示例进行进一步优化,以提高代码的性能和可维护性。以下是一些优化建议:
- 异常处理 :在异步操作中,添加适当的异常处理代码,以确保程序在出现异常时能够正确处理,避免程序崩溃。例如,在 Task 中使用 try-catch 块捕获异常:

Task<string>.Factory.StartNew(
    () =>
    {
        try
        {
            return PiCalculator.Calculate(digits);
        }
        catch (Exception ex)
        {
            // 处理异常
            Console.WriteLine($"An error occurred: {ex.Message}");
            return null;
        }
    }, cancelToken)
  • 资源管理 :确保在使用完资源后及时释放,避免资源泄漏。例如,在使用 Stream StreamReader 时,使用 using 语句确保资源在使用完毕后自动释放:
using (Stream stream = response.GetResponseStream())
using (StreamReader reader = new StreamReader(stream))
{
    int length = reader.ReadToEnd().Length;
    // 处理数据
}
  • 性能优化 :对于一些频繁调用的方法,可以考虑使用缓存或其他优化策略来提高性能。例如,对于 FormatBytes 方法,可以使用缓存来避免重复计算:
private static Dictionary<long, string> bytesFormatCache = new Dictionary<long, string>();

static public string FormatBytes(long bytes)
{
    if (bytesFormatCache.TryGetValue(bytes, out string result))
    {
        return result;
    }

    string[] magnitudes = new string[] { "GB", "MB", "KB", "Bytes" };
    long max = (long)Math.Pow(1024, magnitudes.Length);
    result = string.Format("{1:##.##} {0}",
        magnitudes.FirstOrDefault(magnitude => bytes > (max /= 1024))?? "0 Bytes",
        (decimal)bytes / (decimal)max).Trim();

    bytesFormatCache[bytes] = result;
    return result;
}

总结与展望

10. 总结

异步编程模式为开发者提供了多种方式来处理长时间运行的任务,提高程序的性能和响应能力。不同的模式适用于不同的场景,开发者应根据具体需求选择合适的模式。在实际开发中,还需要注意同步问题和代码优化,以确保程序的稳定性和可维护性。

11. 展望

随着技术的不断发展,异步编程模式也在不断演进。未来,可能会出现更加简洁、高效的异步编程方式,同时对异步操作的管理和监控也将更加智能化。开发者需要不断学习和掌握新的技术,以适应不断变化的开发需求。在实际项目中,合理运用异步编程模式,将有助于提升项目的质量和用户体验。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值