前言

最近改了个老项目,当初异步是用APM写的,于是重温了下APM

你知道APM吗

APM即异步编程模型的简写(Asynchronous Programming Model),写代码的时候或者查看.NET的类库的时候肯定会经常看到和使用以BeginXXX和EndXXX类似的方法,其实你在使用这些方法的时候,你就再使用异步编程模型来编写程序。
对于给定XXX同步操作,异步版本的就是BeginXXX和EndXXX,BeginXXX启动操作,EdnXXX获取操作结果,此时如果操作未完成,则阻塞线程等待,变成同步方法。

异步模型

从.NET1.0开始就支持的异步编程模型,整个过程是围绕IAsyncResult对象进行的,异步操作通过Begin操作和End操作这两个方法实现。

IAsyncResult对象存储有关异步操作的信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public interface IAsyncResult
{
bool IsCompleted
{
get;
}
WaitHandle AsyncWaitHandle
{
get;
}
object AsyncState
{
get;
}
bool CompletedSynchronously
{
get;
}
}
  • AsyncState : 应用程序的可选对象,其中包含有关异步操作的信息。
  • AsyncWaitHandle : 可用来在异步操作完成之前阻止应用程序执行。
  • CompletedSynchronously : 指示异步操作是否是在用于调用Begin操作的线程上完成,而不是在单独的 ThreadPool 线程上完成。
  • IsCompleted : 指示异步操作是否已完成

异步操作

在调用Begin操作后,应用程序可以继续在调用线程上执行指令,同时异步操作在另一个线程上执行。 每次调用Begin操作时,应用程序还应调用End操作来获取结果。

开始异步操作

Begin操作方法参数跟同步保持一致,另外增加两个参数。第一个参数是定义一个AsyncCallback委托,该委托是用于异步操作完成时调用的方法。另一个是参数自定义对象,用于传递给回调方法。

  • 以System.Net.Dns类为例

BeginGetHostEntry异步方法前面参数是Ip地址对象,后面是回调方法和自定义对象,GetHostEntry是同步方法

1
2
public static IAsyncResult BeginGetHostEntry(IPAddress address, AsyncCallback requestCallback, object stateObject);
public static IPHostEntry GetHostEntry (IPAddress address);

调用Begin操作方法立即返回当前线程的控制,将同步方法交由CLR在线程池中排队执行,主线程继续执行后续的过程

下面的代码示例展示了如何使用Dns类中的异步方法,检索用户指定计算机的域名系统(DNS)信息。此示例创建引用AsyncCallback方法的 ProcessDnsInformation委托。每次异步请求获取DNS信息,都会调用一次此方法。

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
static int requestCounter;
static ArrayList hostData = new ArrayList();
static StringCollection hostNames = new StringCollection();
static void UpdateUserInterface()
{
// Print a message to indicate that the application
// is still working on the remaining requests.
Console.WriteLine("{0} requests remaining.", requestCounter);
}

public static void Main()
{
// Create the delegate that will process the results of the
// asynchronous request.
AsyncCallback callBack = new AsyncCallback(ProcessDnsInformation);
string host;
do
{
Console.Write(" Enter the name of a host computer or <enter> to finish: ");
host = Console.ReadLine();
if (host.Length > 0)
{
// Increment the request counter in a thread safe manner.
Interlocked.Increment(ref requestCounter);
// Start the asynchronous request for DNS information.
Dns.BeginGetHostEntry(host, callBack, host);
}
} while (host.Length > 0);
// The user has entered all of the host names for lookup.
// Now wait until the threads complete.
while (requestCounter > 0)
{
UpdateUserInterface();
}
// Display the results.
for (int i = 0; i< hostNames.Count; i++)
{
object data = hostData[i];
string message = data as string;
// A SocketException was thrown.
if (message != null)
{
Console.WriteLine("Request for {0} returned message: {1}",
hostNames[i], message);
continue;
}
// Get the results.
IPHostEntry h = (IPHostEntry) data;
string[] aliases = h.Aliases;
IPAddress[] addresses = h.AddressList;
if (aliases.Length > 0)
{
Console.WriteLine("Aliases for {0}", hostNames[i]);
for (int j = 0; j < aliases.Length; j++)
{
Console.WriteLine("{0}", aliases[j]);
}
}
if (addresses.Length > 0)
{
Console.WriteLine("Addresses for {0}", hostNames[i]);
for (int k = 0; k < addresses.Length; k++)
{
Console.WriteLine("{0}",addresses[k].ToString());
}
}
}
}

结束异步操作

结束异步操作必须要调用End操作方法,可以在回调方法里调用,也可以在调用线程里调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
static void ProcessDnsInformation(IAsyncResult result)
{
string hostName = (string) result.AsyncState;
hostNames.Add(hostName);

try
{
// Get the results.
IPHostEntry host = Dns.EndGetHostEntry(result);
hostData.Add(host);
}
// Store the exception message.
catch (SocketException e)
{
hostData.Add(e.Message);
}
finally
{
// Decrement the request counter in a thread-safe manner.
Interlocked.Decrement(ref requestCounter);
}
}

使用异步方法

以操作FileSteam为例,FileSteam支持了异步操作,所以可以把调用过程从原来的同步改成异步

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
static void Main(string[] args)
{
// open filestream for asynchronous read
FileStream fs = new FileStream("somedata.dat", FileMode.Open,
FileAccess.Read, FileShare.Read, 1024,
FileOptions.Asynchronous);
// byte array to hold 100 bytes of data
Byte[] data = new Byte[100];

// initiate asynchronous read operation, reading first 100 bytes
IAsyncResult ar = fs.BeginRead(data, 0, data.Length, null, null);

// could do something in here which would run alongside file read...

// check for file read complete
while (!ar.IsCompleted)
{
Console.WriteLine("Operation not completed");
Thread.Sleep(10);
}

// get the result
int bytesRead = fs.EndRead(ar);
fs.Close();

Console.WriteLine("Number of bytes read={0}", bytesRead);
Console.WriteLine(BitConverter.ToString(data, 0, bytesRead));
}

通过委托实现异步编程

微软文档
使用委托可通过异步方式调用同步方法。 如果同步调用委托,Invoke 方法将在当前线程上直接调用目标方法。 如果调用 BeginInvoke 方法,公共语言运行时 (CLR) 将对请求进行排队并立即返回给调用方。 目标方法将在线程池中的某个线程上异步调用。 提交请求的原始线程可以不受限制地继续与目标方法并行执行。 如果已在对 BeginInvoke 方法的调用中指定回叫方法,则目标方法结束时,将调用回叫方法。 在回叫方法中,EndInvoke 方法将获取返回值和所有输入/输出或仅输出参数。 如果调用 BeginInvoke 时未指定回叫方法,则可能从调用 BeginInvoke 的线程上调用 EndInvoke。

注意:.Net Core已经不支持委托异步调用

有些同步方法并未实现异步操作,这种时候可以通过委托来实现异步编程

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
private delegate string DemoDelegate(int num);
static void Main(string[] args)
{
DemoDelegate demoDelegate = new DemoDelegate(SyncMethod);

int num = 19;

demoDelegate.BeginInvoke(num, CallBack, null);

//do some things;

Console.ReadKey();
}

static string SyncMethod(int num)
{
return $"{num}";
}

static void CallBack(IAsyncResult ar)
{
var result = ar as AsyncResult;
var demoDelegate = result.AsyncDelegate as DemoDelegate;
var num = demoDelegate.EndInvoke(result);
}

小结

异步编程模型这个模式,就是微软利用委托和线程池帮助我们实现的一个模式

该模式利用一个线程池线程去执行一个操作,在FileStream类BeginRead方法中就是执行一个读取文件操作,该线程池线程会立即将控制权返回给调用线程,此时线程池线程在后台进行这个异步操作;异步操作完成之后,通过回调函数来获取异步操作返回的结果。此时就是利用委托的机制。所以说异步编程模式时利用委托和线程池线程搞出来的模式,包括后面的基于事件的异步编程和基于任务的异步编程,还有C# 5中的async和await关键字,都是利用这委托和线程池搞出来的。他们的本质其实都是一样的,只是后面提出来的使异步编程更加简单罢了。