top
Loading...
ASP.NET創建Web服務之異步Web服務
為了改善調用阻礙線程的長期運行的方法的XML Web服務方法的性能,你應該考慮把它們作為異步的XML Web服務方法發布。實現一個異步XML Web服務方法允許線程在返回線程池的時候執行其他的代碼。這允許增加一個線程池中的有限數目的線程,這樣提高了整體性能和系統的可伸縮性。

通常,調用執行輸入/輸出操作的方法的XML Web服務方法適于作為異步實現。這樣的方法的例子包括和其他的XML Web服務通訊、訪問遠程數據庫、執行網絡輸入/輸出和讀寫大文件方法。這些方法都花費大量時間執行硬件級操作,而把線程留著用來執行XML Web服務方法程序塊。如果XML Web服務方法異步實現,那么線程可以被釋放來執行其他的代碼。

不管一個XML Web服務方法是否異步實現,客戶端都可以與之異步通訊。使用Web服務描述語言工具(WSDL.EXE)生成的.NET客戶端中的代理類來實現異步通信,即使XML Web服務方法是同步實現。代理類包含用于與每個XML Web服務方法異步通信的Begin和End方法。因此,決定一個XML Web服務方法到底是異步還是同步要取決于性能。

注意:實現一個異步的XML Web服務方法對客戶端和服務器上的XML Web服務之間的HTTP連接沒有影響。HTTP連接既不不會關閉也不用連接池化。

實現一個異步的XML Web服務方法

實現一個異步的XML Web服務方法遵循NET Framework異步設計模式

把一個同步的XML Web服務方法分解為兩個方法;其中每個都帶有相同的基名--一個帶有以Begin開頭的名稱,另一個帶有以End開頭的名稱。

Begin方法的參數表包含方法的功能中的所有的in和by引用參數。

By引用參數是作為輸入參數列出的。

倒數第二個參數必須是AsyncCallback。AsyncCallback參數允許客戶端提供一個委托,在方法調用完成的時候調用。當一個異步XML Web服務方法調用另一個異步方法,這個參數可以被傳入那個方法的倒數第二個參數。最后一個參數是一個對象。對象參數允許一個調用者提供狀態信息給方法。當一個異步XML Web服務方法調用另一個異步方法,這個參數可以被傳入那個方法的最后一個參數。

返回值必須是IAsyncResult類型的。

下面的代碼示例是一個Begin方法,有一個方法函數特定的String參數。

[C#]
[WebMethod]
public IAsyncResult BeginGetAuthorRoyalties(String Author,
AsyncCallback callback, object asyncState)
[Visual Basic]
<WebMethod()> _
Public Function BeginGetAuthorRoyalties(ByVal Author As String, _
ByVal callback As AsyncCallback, ByVal asyncState As Object) _
As IAsyncResult

End方法的參數表由一個IAsyncResult類型的out和by引用參數組成。

返回值與一個同步的XML Web服務方法的返回值類型相同。

By引用參數是作為輸出參數列出的。

下面的代碼示例是一個End方法,返回一個AuthorRoyalties用戶定義的模式。

[C#]
[WebMethod]
public AuthorRoyalties EndGetAuthorRoyalties(IAsyncResult
asyncResult)

[Visual Basic]
<WebMethod()> _
Public Function EndGetAuthorRoyalties(ByVal asyncResult As _
IAsyncResult) As AuthorRoyalties

下面的代碼示例是一個和另一個XML Web服務方法異步通訊的異步XML Web服務方法。

[C#]
using System;
using System.Web.Services;
[WebService(Namespace="http://www.contoso.com/")]
public class MyService : WebService {
public RemoteService remoteService;
public MyService() {
// Create a new instance of proxy class for
// the XML Web service to be called.
remoteService = new RemoteService();
}
// Define the Begin method.
[WebMethod]
public IAsyncResult BeginGetAuthorRoyalties(String Author,AsyncCallback callback, object asyncState) {
// Begin asynchronous communictation with a different XML Web
// service.
return remoteService.BeginReturnedStronglyTypedDS(Author,
callback,asyncState);
}
// Define the End method.
[WebMethod]
public AuthorRoyalties EndGetAuthorRoyalties(IAsyncResultasyncResult) {
// Return the asynchronous result from the other XML Web service.
return remoteService.EndReturnedStronglyTypedDS(asyncResult);
}
}

[Visual Basic]
Imports System.Web.Services
<WebService(Namespace:="http://www.contoso.com/")> _
Public Class MyService
Inherits WebService
Public remoteService As RemoteService

Public Sub New()
MyBase.New()
' Create a new instance of proxy class for
' the XML Web service to be called.
remoteService = New RemoteService()
End Sub

' Define the Begin method.
<WebMethod()> _
Public Function BeginGetAuthorRoyalties(ByVal Author As String, _
ByVal callback As AsyncCallback, ByVal asyncState As Object) _
As IAsyncResult
' Begin asynchronous communictation with a different XML Web
' service.
Return remoteService.BeginReturnedStronglyTypedDS(Author, _
callback, asyncState)
End Function
' Define the End method.
<WebMethod()> _
Public Function EndGetAuthorRoyalties(ByVal asyncResult As _
IAsyncResult) As AuthorRoyalties
' Return the asynchronous result from the other XML Web service.
Return remoteService.EndReturnedStronglyTypedDS(asyncResult)
End Function
End Class

下面的代碼示例顯示當一個XML Web服務方法產生了一個以上的異步調用并且這些調用必須連續執行時如何連接這些異步調用。BeginGetAuthorRoyalties方法產生一個異步調用用來判斷傳入的作者名是否有效,并設置一個名為AuthorRoyaltiesCallback的中間回調來接收結果。如果作者名有效,那么那個中間回調異步調用來獲得作者的版稅。

[C#]
using System.Web.Services;
using System.Data;
using System;
// This imports the proxy class for the XML Web services
// that the sample communicates with.
using AsyncWS.localhost;

namespace AsyncWS
{
[WebService(Namespace="http://www.contoso.com/")]
public class MyService : System.Web.Services.WebService
{
public RemoteService remoteService;
public MyService()
{
remoteService = new RemoteService();
}

[WebMethod]
public IAsyncResult BeginGetAuthorRoyalties(String Author,
AsyncCallback callback, Object asyncState)
{
// Saves the current state for the call that gets the author's
// royalties.
AsyncStateChain state = new AsyncStateChain();
state.originalState = asyncState;
state.Author = Author;
state.originalCallback = callback;

// Creates an intermediary callback.
AsyncCallback chainedCallback = new
AsyncCallback(AuthorRoyaltiesCallback);
return remoteService.BeginGetAuthors(chainedCallback,state);
}
// Intermediate method to handle chaining the
// asynchronous calls.
public void AuthorRoyaltiesCallback(IAsyncResult ar)
{
AsyncStateChain state = (AsyncStateChain)ar.AsyncState;
RemoteService rs = new RemoteService();

// Gets the result from the call to GetAuthors.
Authors allAuthors = rs.EndGetAuthors(ar);

Boolean found = false;
// Verifies that the requested author is valid.
int i = 0;
DataRow row;
while (i < allAuthors.authors.Rows.Count && !found)
{
row = allAuthors.authors.Rows[i];
if (row["au_lname"].ToString() == state.Author)
{
found = true;
}
i++;
}
if (found)
{
AsyncCallback cb = state.originalCallback;
// Calls the second XML Web service, because the author is
// valid.
rs.BeginReturnedStronglyTypedDS(state.Author,cb,state);
}
else
{
// Cannot throw the exception in this function or the XML Web
// service will hang. So, set the state argument to the
// exception and let the End method of the chained XML Web
// service check for it.
ArgumentException ex = new ArgumentException(
"Author does not exist.","Author");
AsyncCallback cb = state.originalCallback;
// Call the second XML Web service, setting the state to an
// exception.
rs.BeginReturnedStronglyTypedDS(state.Author,cb,ex);
}
}

[WebMethod]
public AuthorRoyalties EndGetAuthorRoyalties(IAsyncResult asyncResult)
{
// Check whehter the first XML Web service threw an exception.
if (asyncResult.AsyncState is ArgumentException)
throw (ArgumentException) asyncResult.AsyncState;
else
return remoteService.EndReturnedStronglyTypedDS(asyncResult);
}
}
// Class to wrap the callback and state for the intermediate
// asynchronous operation.
public class AsyncStateChain
{
public AsyncCallback originalCallback;
public Object originalState;
public String Author;
}
}

[Visual Basic]

Imports System.Web.Services
Imports System.Data
Imports System
' This imports the proxy class for the XML Web services
' that the sample communicates with.
Imports AsyncWS_VB.localhost

Namespace AsyncWs

<WebService(Namespace:="http://www.contoso.com/")> _
Public Class MyService
Inherits WebService
Public remoteService As remoteService
Public Sub New()
MyBase.New()
remoteService = New localhost.RemoteService()
End Sub
' Defines the Begin method.
<WebMethod()> _
Public Function BeginGetAuthorRoyalties(ByVal Author As String, _
ByVal callback As AsyncCallback, ByVal asyncState As Object) _
As IAsyncResult
' Saves the current state for the call that gets the author's
' royalties.
Dim state As AsyncStateChain = New AsyncStateChain()
state.originalState = asyncState
state.Author = Author
state.originalCallback = callback

' Creates an intermediary callback.
Dim chainedCallback As AsyncCallback = New AsyncCallback( _
AddressOf AuthorRoyaltiesCallback)
' Begin asynchronous communictation with a different XML Web
' service.
Return remoteService.BeginGetAuthors(chainedCallback, state)
End Function

' Intermediate method to handle chaining the asynchronous calls.
Public Sub AuthorRoyaltiesCallback(ByVal ar As IAsyncResult)
Dim state As AsyncStateChain = CType(ar.AsyncState, _
AsyncStateChain)
Dim rs As RemoteService = New RemoteService()

' Gets the result from the call to GetAuthors.
Dim allAuthors As Authors = rs.EndGetAuthors(ar)
Dim found As Boolean = False

' Verifies that the requested author is valid.
Dim i As Integer = 0
Dim row As DataRow
While (i < allAuthors.authors.Rows.Count And (Not found))
row = allAuthors.authors.Rows(i)
If (row("au_lname").ToString() = state.Author) Then
found = True
End If
i = i + 1
End While

If (found) Then
Dim cb As AsyncCallback = state.originalCallback
' Calls the second XML Web service, because the author is
' valid.
rs.BeginReturnedStronglyTypedDS(state.Author, cb, state)
Else
' Cannot throw the exception in this function or the XML Web
' service will hang. So, set the state argument to the
' exception and let the End method of the chained XML Web
' service check for it.
Dim ex As ArgumentException = New ArgumentException( "Author does not exist.", "Author")
Dim cb As AsyncCallback = state.originalCallback
' Call the second XML Web service, setting the state to an
' exception.
rs.BeginReturnedStronglyTypedDS(state.Author, cb, ex)
End If
End Sub

' Define the End method.
<WebMethod()> _
Public Function EndGetAuthorRoyalties(ByVal asyncResult As _
IAsyncResult) As localhost.AuthorRoyalties
' Return the asynchronous result from the other XML Web service.
Return remoteService.EndReturnedStronglyTypedDS(asyncResult)
End Function

End Class

' Class to wrap the callback and state for the intermediate asynchronous
' operation.
Public Class AsyncStateChain
Public originalCallback As AsyncCallback
Public originalState As Object
Public Author As String
End Class
End Namespace

和XML Web服務異步地通訊

和一個XML Web服務異步通訊遵循被Microsoft.NET Framework其它部分使用的異步設計模式。然而,在你取得那些細節之前,重要的是注意一個XML Web服務不必特意的寫來處理用于異步調用的異步請求。你使用Wsdl.exe為你的客戶端創建的代理類自動地創建用于異步調用XML Web服務方法的方法。即使只有一個XML Web服務方法的同步實現也是這樣的。

.NET Framework異步方法調用設計模式

用于調用異步方法的設計模式,尤其是用于.NET Framework,針對每個同步方法分別有兩個異步方法。對每個同步方法,都有一個Begin異步方法和一個End異步方法。Begin方法被客戶端調用來開始方法調用。也就是說,客戶端指示這個方法來開始處理方法調用,但是立即返回。End方法被客戶端調用來取得XML Web服務方法調用執行的處理結果。

一個客戶端如何知道何時調用End方法?.NET Framework定義了兩種方法來實現客戶端判斷其時間。第一種是傳送一個回調函數到Begin方法,當方法已經完成處理的時候調用。第二個方法是使用WaitHandle類的一個方法來導致客戶端等待方法完成。當一個客戶端實現第二個方法,并且調用Begin方法,返回值不是XML Web服務方法指定的數據類型,而是一個實現IAsyncResult接口的類型。IAsyncResult接口包含一個WaitHandle類型的AsyncWaitHandle屬性,實現支持等待同步對象變為帶有WaitHandle.WaitOne、WaitAny和WaitAll標記的方法。當一個同步對象被標記的時候,它指示等待特定的資源的線程可以訪問資源的。如果一個XML Web服務客戶端使用wait方法僅僅異步地調用一個XML Web服務方法,那么它可以調用WaitOne來等待XML Web服務方法完成處理。

重要的是注意不管客戶端選擇來與XML Web服務異步通訊的兩種方法中的哪一種,SOAP消息發送和接收都與同步通信時吻合。也就是說,只有一個SOAP請求和SOAP響應通過網絡發送和接收。代理類通過使用一個不同的線程而不是客戶端用來調用Begin方法的線程來處理SOAP響應。因此,客戶端可以繼續執行線程上的其它的工作,而代理類處理接收和操作SOAP響應。

實現一個產生異步的方法調用的XML Web服務客戶端

用于從使用ASP.NET創建的XML Web服務客戶端產生一個到XML Web服務的異步調用的體系結構被嵌入.NET Framework和由Wsdl.exe構造的代理類中。用于異步調用的設計模式被.NET Framework定義,代理類提供和一個XML Web服務異步通信的機制。當一個用于XML Web服務的代理類被使用Wsdl.exe構造的時候,有三個方法分別被創建,用于XML Web服務中的公共XML Web服務方法。下面的表格描述那三個方法。

代理類中的方法名 描述

<NameOfWebServiceMethod> 同步發送用于名為<NameOfWebServiceMethod>的XML Web服務方法的消息。

Begin<NameOfWebServiceMethod> 開始與名為<NameOfWebServiceMethod>的XML Web服務方法的異步消息通信。

End<NameOfWebServiceMethod> 結束與名為<NameOfWebServiceMethod>的XML Web服務方法的異步消息通信,從XML Web服務方法中取得完成的消息。

下面的代碼示例是一個XML Web服務方法,它可能花費相對長的時間來完成處理。因此,當你應該設置你的XML Web服務客戶端來異步地調用XML Web服務方法的時候,它是一個很好的示例。

[C#]
<%@ WebService Language="C#" Class="PrimeFactorizer" %>

using System;
using System.Collections;
using System.Web.Services;

class PrimeFactorizer {

[WebMethod]
public long[] Factorize(long factorizableNum){
ArrayList outList = new ArrayList();
long i = 0;
int j;
try{
long Check = factorizableNum;

//Go through every possible integer
//factor between 2 and factorizableNum / 2.
//Thus, for 21, check between 2 and 10.
for (i = 2; i < (factorizableNum / 2); i++){
while(Check % i == 0){
outList.Add(i);
Check = (Check/i);
}
}
//Double-check to see how many prime factors have been added.
//If none, add 1 and the number.
j = outList.Count;
if (j == 0) {
outList.Add(1);
outList.Add(factorizableNum);
}
j = outList.Count;

//Return the results and
//create an array to hold them.
long[] primeFactor = new long[j];
for (j = 0; j < outList.Count; j++){
//Pass the values one by one, making sure
//to convert them to type ulong.
primeFactor[j] = Convert.ToInt64(outList[j]);
}
return primeFactor;
}
catch (Exception) {
return null;
}
}
}

[Visual Basic]
<%@ WebService Class="PrimeFactorizer" Language="VB" %>
Imports System
Imports System.Collections
Imports System.Web.Services

Public Class PrimeFactorizer
<WebMethod> _
Public Function Factorize(factorizableNum As Long) As Long()
Dim outList As New ArrayList()
Dim i As Long = 0
Dim j As Integer
Try
Dim Check As Long = factorizableNum

'Go through every possible integer
'factor between 2 and factorizableNum / 2.
'Thus, for 21, check between 2 and 10.
For i = 2 To CLng(factorizableNum / 2) - 1
While Check Mod i = 0
outList.Add(i)
Check = CLng(Check / i)
End While
Next i
'Double-check to see how many prime factors have been added.
'If none, add 1 and the number.
j = outList.Count
If j = 0 Then
outList.Add(1)
outList.Add(factorizableNum)
End If
j = outList.Count

'Return the results and
'create an array to hold them.
Dim primeFactor(j - 1) As Long
For j = 0 To outList.Count - 1
'Pass the values one by one, making sure
'to convert them to type ulong.
primeFactor(j) = CLng(outList(j))
Next j
Return primeFactor
Catch
Return Nothing
End Try
End Function
End Class

下面的代碼示例是一個Wsdl.exe生成的代理類的一部分,用于上述XML Web服務方法。注意BeginFactorize和EndFactorize方法,因為它們被用來與Factorize XML Web服務方法異步通信。

public class PrimeFactorizer : System.Web.Services.Protocols.SoapHttpClientProtocol {

public long[] Factorize(long factorizableNum) {
object[] results = this.Invoke("Factorize", new object[] { factorizableNum});
return ((long[])(results[0]));
}

public System.IAsyncResult BeginFactorize(long factorizableNum, System.AsyncCallback callback, object asyncState) {
return this.BeginInvoke("Factorize", new object[] {
factorizableNum}, callback, asyncState);
}

public long[] EndFactorize(System.IAsyncResult asyncResult) {
object[] results = this.EndInvoke(asyncResult);
return ((long[])(results[0]));
}
}

有兩個方法用來和XML Web服務方法異步通信。下面的代碼示例說明了如何與一個XML Web服務方法異步通信,并且使用回調函數來取得XML Web服務方法的結果。

[C#]
using System;
using System.Runtime.Remoting.Messaging;
using MyFactorize;

class TestCallback
{
public static void Main(){
long factorizableNum = 12345;
PrimeFactorizer pf = new PrimeFactorizer();

//Instantiate an AsyncCallback delegate to use as a parameter
//in the BeginFactorize method.
AsyncCallback cb = new AsyncCallback(TestCallback.FactorizeCallback);

// Begin the Async call to Factorize, passing in our
// AsyncCalback delegate and a reference
// to our instance of PrimeFactorizer.
IAsyncResult ar = pf.BeginFactorize(factorizableNum, cb, pf);

// Keep track of the time it takes to complete the async call
// as the call proceeds.
int start = DateTime.Now.Second;
int currentSecond = start;
while (ar.IsCompleted == false){
if (currentSecond < DateTime.Now.Second) {
currentSecond = DateTime.Now.Second;
Console.WriteLine("Seconds Elapsed..." + (currentSecond - start).ToString() );
}
}
// Once the call has completed, you need a method to ensure the
// thread executing this Main function
// doesn't complete prior to the call-back function completing.
Console.Write("Press Enter to quit");
int quitchar = Console.Read();
}
// Set up a call-back function that is invoked by the proxy class
// when the asynchronous operation completes.
public static void FactorizeCallback(IAsyncResult ar)
{
// You passed in our instance of PrimeFactorizer in the third
// parameter to BeginFactorize, which is accessible in the
// AsyncState property.
PrimeFactorizer pf = (PrimeFactorizer) ar.AsyncState;
long[] results;

// Get the completed results.
results = pf.EndFactorize(ar);

//Output the results.
Console.Write("12345 factors into: ");
int j;
for (j = 0; j<results.Length;j++){
if (j == results.Length - 1)
Console.WriteLine(results[j]);
else
Console.Write(results[j] + ", ");
}
}
}

[Visual Basic]
Imports System
Imports System.Runtime.Remoting.Messaging
Imports MyFactorize

Public Class TestCallback
Public Shared Sub Main()
Dim factorizableNum As Long = 12345
Dim pf As PrimeFactorizer = new PrimeFactorizer()

'Instantiate an AsyncCallback delegate to use as a parameter
' in the BeginFactorize method.
Dim cb as AsyncCallback
cb = new AsyncCallback(AddressOf TestCallback.FactorizeCallback)

' Begin the Async call to Factorize, passing in the
' AsyncCallback delegate and a reference to our instance
' of PrimeFactorizer.
Dim ar As IAsyncResult = pf.BeginFactorize(factorizableNum, cb, pf)

' Keep track of the time it takes to complete the async call as
' the call proceeds.
Dim start As Integer = DateTime.Now.Second
Dim currentSecond As Integer = start
Do while (ar.IsCompleted = false)
If (currentSecond < DateTime.Now.Second) Then
currentSecond = DateTime.Now.Second
Console.WriteLine("Seconds Elapsed..." + (currentSecond - start).ToString() )
End If
Loop

' Once the call has completed, you need a method to ensure the
' thread executing this Main function
' doesn't complete prior to the callback function completing.
Console.Write("Press Enter to quit")
Dim quitchar As Integer = Console.Read()
End Sub

' Set up the call-back function that is invoked by the proxy
' class when the asynchronous operation completes.
Public Shared Sub FactorizeCallback(ar As IAsyncResult)

' You passed in the instance of PrimeFactorizer in the third
' parameter to BeginFactorize, which is accessible in the
' AsyncState property.

Dim pf As PrimeFactorizer = ar.AsyncState
Dim results() as Long

' Get the completed results.
results = pf.EndFactorize(ar)

'Output the results.
Console.Write("12345 factors into: ")
Dim j as Integer
For j = 0 To results.Length - 1
If j = (results.Length - 1) Then
Console.WriteLine(results(j) )
Else
Console.Write(results(j).ToString + ", ")
End If
Next j
End Sub
End Class

下面的代碼示例說明了如何與一個XML Web服務方法異步通信,然后使用一個同步對象來等待處理結束。

[C#]
// -----------------------------------------------------------------------// Async Variation 2.
// Asynchronously invoke the Factorize method,
//without specifying a call back.
using System;
using System.Runtime.Remoting.Messaging;
// MyFactorize, is the name of the namespace in which the proxy class is
// a member of for this sample.
using MyFactorize;

class TestCallback
{
public static void Main(){
long factorizableNum = 12345;
PrimeFactorizer pf = new PrimeFactorizer();

// Begin the Async call to Factorize.
IAsyncResult ar = pf.BeginFactorize(factorizableNum, null, null);

// Wait for the asynchronous operation to complete.
ar.AsyncWaitHandle.WaitOne();

// Get the completed results.
long[] results;
results = pf.EndFactorize(ar);

//Output the results.
Console.Write("12345 factors into: ");
int j;
for (j = 0; j<results.Length;j++){
if (j == results.Length - 1)
Console.WriteLine(results[j]);
else
Console.Write(results[j] + ", ");
}
}
}

[Visual Basic]
Imports System
Imports System.Runtime.Remoting.Messaging
Imports MyFactorize ' Proxy class namespace

Public Class TestCallback
Public Shared Sub Main()
Dim factorizableNum As Long = 12345
Dim pf As PrimeFactorizer = new PrimeFactorizer()

' Begin the Async call to Factorize.
Dim ar As IAsyncResult = pf.BeginFactorize(factorizableNum, Nothing, Nothing)

' Wait for the asynchronous operation to complete.
ar.AsyncWaitHandle.WaitOne()

' Get the completed results.
Dim results() as Long
results = pf.EndFactorize(ar)

'Output the results.
Console.Write("12345 factors into: ")
Dim j as Integer
For j = 0 To results.Length - 1
If j = (results.Length - 1) Then
Console.WriteLine(results(j) )
Else
Console.Write(results(j).ToString + ", ")
End If
Next j
End Sub
End Class

注意:如果FactorizeCallback是一個需要同步化/線成親和上下文的上下文綁定類,那么回調被通過上下文分配體系結構來分配。換句話說,相對于它的對這樣的上下文的調用者,回調可能異步的執行。在方法標記上有單向修飾詞的精確的語義。這指的是任何這樣的方法調用可能同步地或異步地執行,相對于調用者,并且在執行控制返回給它的時候,調用者不能產生任何關于完成這樣一個調用的假設。
而且,在異步操作完成之前調用EndInvoke將阻塞調用者。使用相同的AsyncResult再次調用它的行為是不確定的。

Cancel方法是一個在過去一段特定時間之后取消方法處理的請求。注意它是一個客戶端的請求,并且最好服務器對此有所承諾。在接收到方法已經被取消的消息之后,客戶端就不必做服務器是否已經停止處理的假設了。客戶端最好不要破壞資源,例如文件對象,因為服務器可能仍然需要使用它們。IAsyncResult實例的IsCompleted屬性在服務器結束它的處理之后將被設置為true,不再使用任何客戶端提供的資源。因此,IsCompleted屬性設置為true之后,客戶端就可以安全的銷毀資源了。
作者:http://www.zhujiangroad.com
來源:http://www.zhujiangroad.com
北斗有巢氏 有巢氏北斗