【转】1.6异步编程:IAsyncResult异步编程模型 (APM)

传送门:异步编程系列目录……

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

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

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

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

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

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

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

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

4.         等等…

 

 

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

 

IAsyncResult设计模式----规范概述

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

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

1)         Begin***

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

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

c)         返回IAsyncResult对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 表示异步操作的状态。
[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*** 方法带有该方法同步版本的签名中声明的所有out 和 ref 参数以及由BeginInvoke返回的IAsyncResult,规范上 IAsyncResult 参数放最后。

                         i.              要想获得返回结果,必须调用的方法;

                       ii.              若带有out 和 ref 参数,实现上委托也要带有out 和 ref 参数,以便在回调中获得对应引用传参值做相应逻辑;

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

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

I/O限制的异步操作:比如像带FileOptions.Asynchronous标识的FileStream,其BeginRead()方法向Windows发送一个I/O请求包(I/O Request Packet,IRP)后方法不会阻塞线程而是立即返回,由Windows将IRP传送给适当的设备驱动程序,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的类,并且实现异步调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
// 带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)         IAsyncResult的AsyncWaitHandle属性,待异步操作完成时获得信号。

b)         通过IAsyncResult的IsCompleted属性进行轮询。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/// <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.         执行异步调用后,若我们不需要阻止后续代码的执行,那么我们可以把异步执行操作后的响应放到回调中进行。(推荐使用无阻塞式回调模式)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/// <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类中的同步方法以及所要使用到的委托如下:

1
2
3
4
5
6
7
// 带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;
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/// <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.         非安全方式访问控件(此方式请永远不要再使用)

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

1
2
3
4
// 获取或设置一个值,该值指示是否捕获对错误线程的调用,
// 这些调用在调试应用程序时访问控件的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类上提供的Invoke 和BeginInvoke方法。

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

 

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

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

1
2
3
4
5
6
7
8
9
10
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                (同步调用)先判断控件创建线程与当前线程是否相同,相同则直接调用委托方法;否则使用Win32API的PostMessage 异步执行,但是 Invoke 内部会调用IAsyncResult.AsyncWaitHandle等待执行完成。

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

1
2
3
4
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);

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

2)         InvokeRequired

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

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

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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) 效果是一样的。因为在执行Invoke或BeginInvoke时,内部首先调用 FindMarshalingControl() 进行一个循环向上回溯,从当前控件开始回溯父控件,直到找到最顶级的父控件,用它作为封送对象。也就是说 this.textBox1.Invoke(Delegate del) 会追溯到和 this.Invoke(Delegate del) 一样的起点。(子控件的创建线程一定是创建父控件的线程,所以这种追溯不会导致将调用封送到错误的目的线程)

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

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

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

1
2
3
4
5
new Thread((ThreadStart)delegate
    {
        WaitBeforeLogin = new Form2();
        Application.Run(WaitBeforeLogin);
    }).Start();

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

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

 

 

 

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

 

 

 

参考:MSDN

              书籍:《CLR via C#(第三版)》

 

 


作者:滴答的雨
出处:http://www.cnblogs.com/heyuquan/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

 
   
 

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

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

b) 通过IAsyncResult的IsCompleted属性进行轮询。

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

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


你说的a、b、c方式恰好都是不推荐的调用方式,因为会阻塞到线程,并且还给人“在xxx之后完成必须要通过阻塞线程这种方式”的错误印象。恰好推荐的最常用的回调方式你又没说清楚,所以我认为你没懂。而且IAsnycResult实现的还比较繁琐,没有错误处理,并且会有资源泄露可能。而这几点正是CLR VIA C#所提及的。

另外,你要不就
1.阻塞线程,在主线程里调用End(不推荐)
IAsyncResult ar = cal.BeginCalculate(123, 456, null, null);

string resultStr = string.Empty;
int result = cal.EndCalculate(ref resultStr, ar);
//把写在call back的代码放在这里

2.要不就采用回调的方式(推荐)。

而不是像你的代码里这种既有回调,又阻塞线程的混合的使用方式,看起来就像没太懂的人写的代码。
IAsyncResult calculateResult = cal.BeginCalculate(123, 456, AfterCallback, cal);
string resultStr = string.Empty;
int result = cal.EndCalculate(ref resultStr, calculateResult);


关于APM我的看法可以看我的博客,关于IAsyncResult那一节的表述。

 
   
  
#18楼 [楼主] 2013-04-28 10:12 | 滴答的雨
   
 

@ 浪雪
这个问题我有这样说:我将展示三种阻塞式响应异步调用和一种无阻塞式委托响应异步调用。并且也在括号中说了阻塞方式不推荐,接着立马就说到回调方式的不阻塞形式了啊???(你提出的问题还是很好的,我博文中表达不清,现在我将推荐和不推荐用粗体强调出来)

对于即有回调又有阻塞这种代码可能给予了误解,实际上我是想在在这个示例中展现这些关键代码(并且模式冲突的代码我也已经注释掉了,只是为了展现),不想再另外贴出一个示例。。。

感谢你的提示,我将此处表达更改为如下:

 
   
 
 

@ 滴答的雨
IAsyncResult本身就带这几种模式给人选择的灵活性,并不意味着需要一起用。一起用反而让人看起来不但困扰,而且反而是错误用法。

这就是为什一定需要分开写,就算是像我一样偷懒也可以不写,但是就是不能混在一起写。

简单来说,你的代码中,
1.看起来展示的是直接调用的end的这种阻塞的方式。
2.同时定义了call back方法给人以困扰,让人以为这又是个回调方法。
3.因为直接调用end的方式根本不需要通过asyncState来辅助传递参数,避免转型的开销和可能产生的错误。

结果是你既没正确的展示End的用法,又把最常用的回调方法给注释掉了。
就算你多加一个例子,也不过多三五行的代码而已。所以就觉得你没懂。

CalculateLib cal = new CalculateLib();

展示End
IAsyncResult ar = cal.BeginCalculate(123, 456, null, null);
string resultStr = string.Empty;
int result = cal.EndCalculate(ref resultStr, ar);



展示call back
IAsyncResult calculateResult = cal.BeginCalculate(123, 456, AfterCallback, cal);

private static void AfterCallback(ref string resultStr, IAsyncResult ar)
{
CalculateLib cal = ar.AsyncState as CalculateLib;

int result = cal.EndCalculate(ref resultStr, calculateResult1);
if (result > 0) { }
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值