那些年,我们一起追寻的异步编程

术语:

APM 异步编程模型,Asynchronous Programming Model

EAP 基于事件的异步编程模式,Event-based Asynchronous Pattern

TAP 基于任务的异步编程模式,Task-based Asynchronous Pattern

TPL 任务并行库,Task Parallel Library

现在我给这个系列整个目录和做个简单介绍。

“概要 + 目录”整理

C#语言是微软于2000年发布,基于.NET Framewrok框架的、面向对象的高级语言。经过近十三年的发展,经历了5次大的升级,目前最新版本为C#5.0(对应于.NET Framework 4.5)。其中每个版本发布都是有一个“主题”。即:C#1.0托管代码→C#2.0泛型→C#3.0LINQ→C#4.0动态语言→C#5.0异步编程。这系列既是针对“异步编程”所写。

C#版本 .NET 版本 Visual Studio 版本 特性描述
C# 1.0 .NET 1.0/1.1 VS 2002/2003 C#的第一个正式发行版本。微软的团队从无到有创造了一种语言,专门为.NET编程提供支持
C# 2.0 .NET 2.0 VS 2005 C#语言开始支持泛型,.NET Framework 2.0新增了支持泛型的库
C# 2.0 .NET 3.0 VS 2005 新增了一套API来支持分布式通信(Windows Communication Foundation— WCF)、富客户端表示(Windows Presentation Foundation)、工作流(Windows Workflow—WF)以及Web身份验证(Cardspaces)
C# 3.0 .NET 3.5 VS 2008 添加了对LINQ的支持,对用于集合编程的API进行了大幅改进。.NET Framework 3.5对原有的API进行了扩展,从而支持了LINQ
C# 4.0 .NET 4.0 VS 2010 添加了动态类型(dynamic)的支持,引入了新的轻量级线程同步基元及新的异步编程类库TPL
C# 5.0 .NET 4.5 VS 2012 改进并扩展了.NET4.0中引入的TPL类库,并引入async和await关键字轻松构建异步方法。

1. 我的异步编程整理

资料整理路线:线程—-线程池—-线程同步—-并行任务—-三种异步编程模型。首先了解最基础的线程(Thread类),再进一步明白线程管理器(ThreadPool类)。因为多个工作项之间可能出现并行运行,会造成对共享资源的访问问题,所以引入线程同步基元来让共享资源得到合理使用。最后介绍.NET4.0新引入并在.NET4.5中得到优化和扩展的TPL(任务并行库),并结合C# 5.0中新引入的asyncawait关键字轻松构建异步方法。详细如下:

异步编程:线程概述及使用

异步编程:使用线程池管理线程

异步编程:线程同步基元对象

异步编程:轻量级线程同步基元对象

异步编程:.NET4.5 数据并行

异步编程:异步编程模型 (APM)

异步编程:基于事件的异步编程模式(EAP)

异步编程:.NET 4.5 基于任务的异步编程模型(TAP)


异步编程:IAsyncResult异步编程模型 (APM)

大部分开发人员,在开发多线程应用程序时,都是使用ThreadPoolQueueUserWorkItem方法来发起一次简单的异步操作。然而,这个技术存在许多限制。最大的问题是没有一个内建的机制让你知道操作在什么时候完成,也没有一个机制在操作完成时获得一个返回值。为了克服这些限制(并解决其他一些问题),Microsoft引入了三种异步编程模式:

  1. .NET1.0异步编程模型 (APM),基于IAsyncResult接口实现。

  2. .NET2.0基于事件的异步编程模式(EMP),基于事件实现。

  3. .NET4.X基于任务的异步编程模式(TPL),新型异步编程模式,对于.NET4.0之后的异步构造都推荐使用此模式

尽管在新的设计上我们推荐都使用“.NET4.0基于任务的编程模式”,但我还是计划整理出旧版的异步编程模型,因为:

  1. 在一些特殊场合下我们可能觉得一种模式更适合;

  2. 可以更充分认识三种模式之间的优劣,便于选择;

  3. 很多遗留的代码包含了旧的设计模式;

  4. 等等…

示例下载:异步编程:IAsyncResult异步编程模型.rar

IAsyncResult设计模式—-规范概述

使用IAsyncResult设计模式的异步操作是通过名为 Begin***End*** 的两个方法来实现的,这两个方法分别指代开始和结束异步操作。例如,FileStream类提供BeginReadEndRead方法来从文件异步读取字节。这两个方法实现了 Read 方法的异步版本。

在调用 Begin*** 后,应用程序可以继续在调用线程上执行指令,同时异步操作在另一个线程上执行。(如果有返回值还应调用 End*** 来获取操作的结果)。

1)Begin***

a)Begin*** 方法带有该方法的同步版本签名中声明的任何参数。

b)Begin*** 方法签名中不包含任何输出参数。方法签名最后两个参数的规范是:第一个参数定义一个AsyncCallback委托,此委托引用在异步操作完成时调用的方法。第二个参数是一个用户定义的对象。此对象可用来向异步操作完成时为AsyncCallback委托方法传递应用程序特定的状态信息(eg:可通过此对象在委托中访问End*** 方法)。另外,这两个参数都可以传递null。

c)返回IAsyncResult对象。

// 表示异步操作的状态。
[ComVisible(true)]
public interface IAsyncResult
{
    // 获取用户定义的对象,它限定或包含关于异步操作的信息。
    object AsyncState { get; }
    // 获取用于等待异步操作完成的System.Threading.WaitHandle,待异步操作完成时获得信号。
    WaitHandle AsyncWaitHandle { get; }
    // 获取一个值,该值指示异步操作是否同步完成。
    bool CompletedSynchronously { get; }
    // 获取一个值,该值指示异步操作是否已完成。
    bool IsCompleted { get; }
}

// 常用委托声明(我后面示例是使用了自定义的带ref参数的委托)
public delegate void AsyncCallback(IAsyncResult ar)

2)End***

a) End*** 方法可结束异步操作,如果调用 End*** 时,IAsyncResult对象表示的异步操作还未完成,则 End*** 将在异步操作完成之前阻塞调用线程。

b) End*** 方法的返回值与其同步副本的返回值类型相同。End*** 方法带有该方法同步版本的签名中声明的所有outref 参数以及由BeginInvoke返回的IAsyncResult,规范上 IAsyncResult 参数放最后。

i.要想获得返回结果,必须调用的方法;
ii.若带有out 和 ref 参数,实现上委托也要带有outref参数,以便在回调中获得对应引用传参值做相应逻辑;

3) 总是调用 End***() 方法,而且只调用一次

以下理由都是针对“I/O限制”的异步操作提出。然而,对于计算限制的异步操作,尽管都是用户代码,但还是推荐遵守此规则。

I/O限制的异步操作:比如像带FileOptions.Asynchronous标识的FileStream,其BeginRead()方法向Windows发送一个I/O请求包(I/O Request Packet,IRP)后方法不会阻塞线程而是立即返回,由WindowsIRP传送给适当的设备驱动程序,IRP中包含了为BeginRead()方法传入的回调函数,待硬件设备处理好IRP后,会将IRP的委托排队到CLR的线程池队列中。

必须调用End***方法,否则会造成资源的泄露。有的开发人员写代码调用Begin***方法异步执行I/O限制后就不需要进行任何处理了,所以他们不关心End***方法的调用。但是,出于以下两个原因,End***方法是必须调用的:

a) 在异步操作时,对于I/O限制操作,CLR会分配一些内部资源,操作完成时,CLR继续保留这些资源直至End***方法被调用。如果一直不调用End***,这些资源会直到进程终止时才会被回收。(End***方法设计中常常包含资源释放)

b) 发起一个异步操作时,实际上并不知道该操作最终是成功还是失败(因为操作由硬件在执行)。要知道这一点,只能通过调用End***方法,检查它的返回值或者看它是否抛出异常。

另外,需要注意的是I/O限制的异步操作完全不支持取消(因为操作由硬件执行),但可以设置一个标识,在完成时丢弃结果来模拟取消行为。

现在我们清楚了IAsyncResult设计模式的设计规范,接下来我们再通过IAsyncResult异步编程模式的三个经典场合来加深理解。

一、基于IAsyncResult构造一个异步API

现在来构建一个IAsyncResult的类,并且实现异步调用。

// 带ref参数的自定义委托
public delegate void RefAsyncCallback(ref string resultStr, IAsyncResult ar);

public class CalculateAsyncResult : IAsyncResult
{
    private int _calcNum1;
    private int _calcNum2;
    private RefAsyncCallback _userCallback;

    public CalculateAsyncResult(int num1, int num2, RefAsyncCallback userCallback, object asyncState)
    {
        this._calcNum1 = num1;
        this._calcNum2 = num2;
        this._userCallback = userCallback;
        this._asyncState = asyncState;
        // 异步执行操作
        ThreadPool.QueueUserWorkItem((obj) => { AsyncCalculate(obj); }, this);
    }

    #region IAsyncResult接口
    private object _asyncState;
    public object AsyncState { get { return _asyncState; } }

    private ManualResetEvent _asyncWaitHandle;
    public WaitHandle AsyncWaitHandle
    {
        get
        {
            if (this._asyncWaitHandle == null)
            {
                ManualResetEvent event2 = new ManualResetEvent(false);
                Interlocked.CompareExchange<ManualResetEvent>(ref this._asyncWaitHandle, event2, null);
            }
            return _asyncWaitHandle;
        }
    }

    private bool _completedSynchronously;
    public bool CompletedSynchronously { get { return _completedSynchronously; } }

    private bool _isCompleted;
    public bool IsCompleted { get { return _isCompleted; } }
    #endregion

    /// <summary>
    /// 
    /// 存储最后结果值
    /// </summary>
    public int FinnalyResult { get; set; }
    /// <summary>
    /// End方法只应调用一次,超过一次报错
    /// </summary>
    public int EndCallCount = 0;
    /// <summary>
    /// ref参数
    /// </summary>
    public string ResultStr;

    /// <summary>
    /// 异步进行耗时计算
    /// </summary>
    /// <param name="obj">CalculateAsyncResult实例本身</param>
    private static void AsyncCalculate(object obj)
    {
        CalculateAsyncResult asyncResult = obj as CalculateAsyncResult;
        Thread.SpinWait(1000);
        asyncResult.FinnalyResult = asyncResult._calcNum1 * asyncResult._calcNum2;
        asyncResult.ResultStr = asyncResult.FinnalyResult.ToString();

        // 是否同步完成
        asyncResult._completedSynchronously = false;
        asyncResult._isCompleted = true;
        ((ManualResetEvent)asyncResult.AsyncWaitHandle).Set();
        if (asyncResult._userCallback != null)
            asyncResult._userCallback(ref asyncResult.ResultStr, asyncResult);
    }
}

public class CalculateLib
{
    public IAsyncResult BeginCalculate(int num1, int num2, RefAsyncCallback userCallback, object asyncState)
    {
        CalculateAsyncResult result = new CalculateAsyncResult(num1, num2, userCallback, asyncState);
        return result;
    }

    public int EndCalculate(ref string resultStr, IAsyncResult ar)
    {
        CalculateAsyncResult result = ar as CalculateAsyncResult;
        if (Interlocked.CompareExchange(ref result.EndCallCount, 1, 0) == 1)
        {
            throw new Exception("End方法只能调用一次。");
        }
        result.AsyncWaitHandle.WaitOne();

        resultStr = result.ResultStr;

        return result.FinnalyResult;
    }

    public int Calculate(int num1, int num2, ref string resultStr)
    {
        resultStr = (num1 * num2).ToString();
        return num1 * num2;
    }
}

使用上面通过IAsyncResult设计模式实现的带ref引用参数的异步操作,我将展示三种阻塞式响应异步调用和一种无阻塞式委托响应异步调用。即:

1.执行异步调用后,若我们需要控制后续执行代码在异步操作执行完之后执行,可通过下面三种方式阻止其他工作:(当然我们不推荐你阻塞线程或轮询浪费CPU时间)

a) IAsyncResultAsyncWaitHandle属性,待异步操作完成时获得信号。

b) 通过IAsyncResultIsCompleted属性进行轮询。

c) 调用异步操作的 End*** 方法。

/// <summary>
/// APM 阻塞式异步响应
/// </summary>
public class Calculate_For_Break
{
    public static void Test()
    {
        CalculateLib cal = new CalculateLib();

        // 基于IAsyncResult构造一个异步API   (回调参数和状态对象都传递null)
        IAsyncResult calculateResult = cal.BeginCalculate(123, 456, null, null);
        // 执行异步调用后,若我们需要控制后续执行代码在异步操作执行完之后执行,可通过下面三种方式阻止其他工作:
        // 1、IAsyncResult 的 AsyncWaitHandle 属性,带异步操作完成时获得信号。
        // 2、通过 IAsyncResult 的 IsCompleted 属性进行轮询。通过轮询还可实现进度条功能。
        // 3、调用异步操作的 `End***` 方法。
        // ***********************************************************
        // 1、calculateResult.AsyncWaitHandle.WaitOne();
        // 2、while (calculateResult.IsCompleted) { Thread.Sleep(1000); }
        // 3、
        string resultStr = string.Empty;
        int result = cal.EndCalculate(ref resultStr, calculateResult);
    }
}

2.执行异步调用后,若我们不需要阻止后续代码的执行,那么我们可以把异步执行操作后的响应放到回调中进行。(推荐使用无阻塞式回调模式)

/// <summary>
/// APM 回调式异步响应
/// </summary>
public class Calculate_For_Callback
{
    public static void Test()
    {
        CalculateLib cal = new CalculateLib();

        // 基于IAsyncResult构造一个异步API
        IAsyncResult calculateResult = cal.BeginCalculate(123, 456, AfterCallback, cal);
    }

    /// <summary>
    /// 异步操作完成后做出响应
    /// </summary>
    private static void AfterCallback(ref string resultStr, IAsyncResult ar)
    {
        // 执行异步调用后,若我们不需要阻止后续代码的执行,那么我们可以把异步执行操作后的响应放到回调中进行。
        CalculateLib cal = ar.AsyncState as CalculateLib;
        cal.EndCalculate(ref resultStr, ar);
        // 再根据resultStr值做逻辑。
    }
}

二、使用委托进行异步编程

对于委托,编译器会为我们生成同步调用方法“invoke”以及异步调用方法“BeginInvoke”和“EndInvoke”。对于异步调用方式,公共语言运行库 (CLR) 将对请求进行排队并立即返回到调用方,由线程池的线程调度目标方法并与提交请求的原始线程并行运行,为BeginInvoke()方法传入的回调方法也将在同一个线程上运行。

异步委托是快速为方法构建异步调用的方式,它基于IAsyncResult设计模式实现的异步调用,即,通过BeginInvoke返回IAsyncResult对象;通过EndInvoke获取结果值。

示例:

上节的CalculateLib类中的同步方法以及所要使用到的委托如下:

// 带ref参数的自定义委托
public delegate int AsyncInvokeDel(int num1, int num2, ref string resultStr);
public int Calculate(int num1, int num2, ref string resultStr)
{
    resultStr = (num1 * num2).ToString();
    return num1 * num2;
}

然后,通过委托进行同步或异步调用:

/// <summary>
/// 使用委托进行异步调用
/// </summary>
public class Calculate_For_Delegate
{
    public static void Test()
    {
        CalculateLib cal = new CalculateLib();

        // 使用委托进行同步或异步调用
        AsyncInvokeDel calculateAction = cal.Calculate;
        string resultStrAction = string.Empty;
        // int result1 = calculateAction.Invoke(123, 456);
        IAsyncResult calculateResult1 = calculateAction.BeginInvoke(123, 456, ref resultStrAction, null, null);
        int result1 = calculateAction.EndInvoke(ref resultStrAction, calculateResult1);
    }
}

三、多线程操作控件

访问 Windows 窗体控件本质上不是线程安全的。如果有两个或多个线程操作某一控件的状态,则可能会迫使该控件进入一种不一致的状态。还可能出现其他与线程相关的 bug,包括争用情况和死锁。确保以线程安全方式访问控件非常重要。

不过,在有些情况下,您可能需要多线程调用控件的方法。.NET Framework 提供了从任何线程操作控件的方式:

1.非安全方式访问控件(此方式请永远不要再使用)

多线程访问窗口中的控件,可以在窗口的构造函数中将FormCheckForIllegalCrossThreadCalls静态属性设置为false

// 获取或设置一个值,该值指示是否捕获对错误线程的调用,
// 这些调用在调试应用程序时访问控件的System.Windows.Forms.Control.Handle属性。
// 如果捕获了对错误线程的调用,则为 true ;否则为 false 。
public static bool CheckForIllegalCrossThreadCalls { get; set; }

2.安全方式访问控件

原理:从一个线程封送调用并跨线程边界将其发送到另一个线程,并将调用插入到创建控件线程的消息队列中,当控件创建线程处理这个消息时,就会在自己的上下文中执行传入的方法。(此过程只有调用线程和创建控件线程,并没有创建新线程)

注意:从一个线程封送调用并跨线程边界将其发送到另一个线程会耗费大量的系统资源,所以应避免重复调用其他线程上的控件。

1)使用BackgroundWork后台辅助线程控件方式(详见:基于事件的异步编程模式(EMP))。

2)结合TaskScheduler.FromCurrentSynchronizationContext()Task 实现。

3)捕获线程上下文ExecuteContext,并调用ExeceteContext.Run()静态方法在指定的线程上下文中执行。(详见:执行上下文

4)使用Control类上提供的InvokeBeginInvoke方法。

5)在WPF应用程序中可以通过WPF提供的Dispatcher对象提供的Invoke方法、BeginInvoke方法来完成跨线程工作。

因本文主要解说IAsyncResult异步编程模式,所以只详细分析InvokeBeginInvoke跨线程访问控件方式。

Control类实现了ISynchronizeInvoke接口,提供了InvokeBeginInvoke方法来支持其它线程更新GUI界面控件的机制。

public interface ISynchronizeInvoke
{
    // 获取一个值,该值指示调用线程是否与控件的创建线程相同。
    bool InvokeRequired { get; }
    // 在控件创建的线程上异步执行指定委托。
    AsyncResult BeginInvoke(Delegate method, params object[] args);
    object EndInvoke(IAsyncResult asyncResult);
    // 在控件创建的线程上同步执行指定委托。
    object Invoke(Delegate method, params object[] args);
}

1)Control类的 Invoke,BeginInvoke 内部实现如下:

a)Invoke (同步调用)先判断控件创建线程与当前线程是否相同,相同则直接调用委托方法;否则使用Win32APIPostMessage 异步执行,但是 Invoke 内部会调用IAsyncResult.AsyncWaitHandle等待执行完成。

b)BeginInvoke (异步调用)使用Win32APIPostMessage 异步执行,并且返回 IAsyncResult 对象。

UnsafeNativeMethods.PostMessage(new HandleRef(this, this.Handle)
                  , threadCallbackMessage, IntPtr.Zero, IntPtr.Zero);
[DllImport("user32.dll", CharSet=CharSet.Auto)]
public static extern bool PostMessage(HandleRefhwnd, intmsg, IntPtrwparam, IntPtrlparam);

PostMessagewindows api,用来把一个消息发送到一个窗口的消息队列。这个方法是异步的,也就是该方法封送完毕后马上返回,不会等待委托方法的执行结束,调用者线程将不会被阻塞。(对应同步方法的windows api是:SendMessage();消息队列里的消息通过调用GetMessagePeekMessage取得)

2)InvokeRequired

获取一个值,该值指示调用线程是否与控件的创建线程相同。内部关键如下:

Int windowThreadProcessId = SafeNativeMethods.GetWindowThreadProcessId(ref2, out num);
Int currentThreadId = SafeNativeMethods.GetCurrentThreadId();
return (windowThreadProcessId != currentThreadId);

即返回“通过GetWindowThreadProcessId功能函数得到创建指定窗口线程的标识和创建窗口的进程的标识符与当前线程Id进行比较”的结果。

3)示例(详见示例文件)

在使用的时候,我们使用 this.InvokeRequired 属性来判断是使用InvokeBeginInvoke 还是直接调用方法。

private void InvokeControl(object mainThreadId)
{
    if (this.InvokeRequired)
    {
        this.Invoke(new Action<String>(ChangeText), "InvokeRequired = true.改变控件Text值");
        //this.textBox1.Invoke(new Action<int>(InvokeCount), (int)mainThreadId);
    }
    else
    {
        ChangeText("在创建控件的线程上,改变控件Text值");
    }
}

private void ChangeText(String str)
{
    this.textBox1.Text += str;
}

注意,在InvokeControl方法中使用 this.Invoke(Delegate del) 和使用 this.textBox1.Invoke(Delegate del) 效果是一样的。因为在执行InvokeBeginInvoke时,内部首先调用 FindMarshalingControl() 进行一个循环向上回溯,从当前控件开始回溯父控件,直到找到最顶级的父控件,用它作为封送对象。也就是说 this.textBox1.Invoke(Delegate del) 会追溯到和 this.Invoke(Delegate del) 一样的起点。(子控件的创建线程一定是创建父控件的线程,所以这种追溯不会导致将调用封送到错误的目的线程)

4)异常信息:”在创建窗口句柄之前,不能在控件上调用 InvokeBeginInvoke

a) 可能是在窗体还未构造完成时,在构造函数中异步去调用了InvokeBeginInvoke

b) 可能是使用辅助线程创建一个窗口并用Application.Run()去创建句柄,在句柄未创建好之前调用了InvokeBeginInvoke。(此时新建的窗口相当于开了另一个进程,并且为新窗口关联的辅助线程开启了消息循环机制),类似下面代码:

new Thread((ThreadStart)delegate
    {
        WaitBeforeLogin = new Form2();
        Application.Run(WaitBeforeLogin);
    }).Start();

解决方案:在调用InvokeBeginInvoke之前轮询检查窗口的IsHandleCreated属性。

// 获取一个值,该值指示控件是否有与它关联的句柄。
public bool IsHandleCreated { get; }
while (!this.IsHandleCreated) { …… }

本节到此结束,本节主要讲了异步编程模式之一“异步编程模型(APM)”,是基于IAsyncResult设计模式实现的异步编程方式,并且构建了一个继承自IAsyncResult接口的示例,及展示了这种模式在委托及跨线程访问控件上的经典应用。下一节中,我将为大家介绍基于事件的编程模型……

阅读更多
换一批

没有更多推荐了,返回首页