タスクベースの操作 でプロキシを生成しました。
Async/awaitを使用して、このサービスをどのように適切に呼び出す必要がありますか(ServiceClient
およびOperationContext
を後で破棄する)。
私の最初の試みは:
public async Task<HomeInfo> GetHomeInfoAsync(DateTime timestamp)
{
using (var helper = new ServiceHelper<ServiceClient, ServiceContract>())
{
return await helper.Proxy.GetHomeInfoAsync(timestamp);
}
}
ServiceHelper
であり、ServiceClient
およびOperationContextScope
を作成し、その後それらを破棄するクラス:
try
{
if (_operationContextScope != null)
{
_operationContextScope.Dispose();
}
if (_serviceClient != null)
{
if (_serviceClient.State != CommunicationState.Faulted)
{
_serviceClient.Close();
}
else
{
_serviceClient.Abort();
}
}
}
catch (CommunicationException)
{
_serviceClient.Abort();
}
catch (TimeoutException)
{
_serviceClient.Abort();
}
catch (Exception)
{
_serviceClient.Abort();
throw;
}
finally
{
_operationContextScope = null;
_serviceClient = null;
}
ただし、次のエラーで2つのサービスを同時に呼び出すと、これは悲惨に失敗しました。「このOperationContextScopeは、作成されたスレッドとは異なるスレッドに配置されています。」
[〜#〜] msdn [〜#〜] 言います:
OperationContextScopeブロック内で非同期の「待機」パターンを使用しないでください。継続が発生すると、別のスレッドで実行される場合があり、OperationContextScopeはスレッド固有です。非同期呼び出しのために「await」を呼び出す必要がある場合は、OperationContextScopeブロックの外側で使用します。
それが問題です!しかし、どうすれば適切に修正できますか?
private async void DoStuffWithDoc(string docId)
{
var doc = await GetDocumentAsync(docId);
if (doc.YadaYada)
{
// more code here
}
}
public Task<Document> GetDocumentAsync(string docId)
{
var docClient = CreateDocumentServiceClient();
using (new OperationContextScope(docClient.InnerChannel))
{
return docClient.GetDocumentAsync(docId);
}
}
彼のコードに関する私の問題は、彼がServiceClientでClose(またはAbort)を呼び出さないことです。
また、カスタムOperationContextScope
を使用してSynchronizationContext
を伝播する a way を見つけました。しかし、多くの「危険な」コードであるという事実に加えて、彼は次のように述べています。
操作コンテキストスコープの破棄に関していくつかの小さな問題があることに注意する価値があります(呼び出しスレッドでのみ破棄できるため)。しかし、これは(少なくとも逆アセンブリ)、Dispose()を実装しますが、Finalize()は実装しません。
だから、私たちはここで運が悪いのですか? async/awaitを使用してWCFサービスを呼び出し、ServiceClient
とOperationContextScope
の両方を破棄する実証済みのパターンはありますか?おそらくマイクロソフトの誰か(おそらくグルのStephen Toub :))が助けてくれるでしょう。
ありがとう!
[更新]
ユーザーNoseratioからの多くの助けを借りて、機能するものを思い付きました:OperationContextScope
を使用しないでください。 these の理由のいずれかで使用している場合は、シナリオに合った回避策を見つけてください。それ以外の場合、本当に本当に本当にOperationContextScope
が必要な場合は、それをキャプチャするSynchronizationContext
の実装を考え出す必要があり、 非常に難しい (if可能な限り-これがデフォルトの動作ではない理由がなければなりません)。
したがって、完全に機能するコードは次のとおりです。
public async Task<HomeInfo> GetHomeInfoAsync(DateTime timestamp)
{
using (var helper = new ServiceHelper<ServiceClient, ServiceContract>())
{
return await helper.Proxy.GetHomeInfoAsync(timestamp);
}
}
ServiceHelper
である場合:
public class ServiceHelper<TServiceClient, TService> : IDisposable
where TServiceClient : ClientBase<TService>, new()
where TService : class
{
protected bool _isInitialized;
protected TServiceClient _serviceClient;
public TServiceClient Proxy
{
get
{
if (!_isInitialized)
{
Initialize();
_isInitialized = true;
}
else if (_serviceClient == null)
{
throw new ObjectDisposedException("ServiceHelper");
}
return _serviceClient;
}
}
protected virtual void Initialize()
{
_serviceClient = new TServiceClient();
}
// Implement IDisposable.
// Do not make this method virtual.
// A derived class should not be able to override this method.
public void Dispose()
{
Dispose(true);
// Take yourself off the Finalization queue
// to prevent finalization code for this object
// from executing a second time.
GC.SuppressFinalize(this);
}
// Dispose(bool disposing) executes in two distinct scenarios.
// If disposing equals true, the method has been called directly
// or indirectly by a user's code. Managed and unmanaged resources
// can be disposed.
// If disposing equals false, the method has been called by the
// runtime from inside the finalizer and you should not reference
// other objects. Only unmanaged resources can be disposed.
protected virtual void Dispose(bool disposing)
{
// If disposing equals true, dispose all managed
// and unmanaged resources.
if (disposing)
{
try
{
if (_serviceClient != null)
{
if (_serviceClient.State != CommunicationState.Faulted)
{
_serviceClient.Close();
}
else
{
_serviceClient.Abort();
}
}
}
catch (CommunicationException)
{
_serviceClient.Abort();
}
catch (TimeoutException)
{
_serviceClient.Abort();
}
catch (Exception)
{
_serviceClient.Abort();
throw;
}
finally
{
_serviceClient = null;
}
}
}
}
クラスは拡張機能をサポートしていることに注意してください。おそらく、資格情報を継承して提供する必要があります。
唯一の可能な「落とし穴」は、GetHomeInfoAsync
で、プロキシから取得したTask
を返すことはできないということです(これは当然のように思えますが、既にTask
を作成している場合持っている)。さて、この場合は、await
プロキシTask
とthenを閉じる(または中止する)ServiceClient
が必要です。それ以外の場合は、サービスを呼び出した後、すぐにそれを閉じてください(バイトはネットワーク経由で送信されます)!
OK、それを機能させる方法はありますが、Noseratioが述べているように、信頼できるソースから答えを得るのは良いことです。
実行可能な解決策は、カスタムawaiterを使用して、OperationContext.Current
を介して新しい操作コンテキストを流すことです。 OperationContext
の実装 自体は、スレッドアフィニティを必要としないようです。パターンは次のとおりです。
async Task TestAsync()
{
using(var client = new WcfAPM.ServiceClient())
using (var scope = new FlowingOperationContextScope(client.InnerChannel))
{
await client.SomeMethodAsync(1).ContinueOnScope(scope);
await client.AnotherMethodAsync(2).ContinueOnScope(scope);
}
}
FlowingOperationContextScope
とContinueOnScope
の実装は次のとおりです(わずかにテストされています)。
public sealed class FlowingOperationContextScope : IDisposable
{
bool _inflight = false;
bool _disposed;
OperationContext _thisContext = null;
OperationContext _originalContext = null;
public FlowingOperationContextScope(IContextChannel channel):
this(new OperationContext(channel))
{
}
public FlowingOperationContextScope(OperationContext context)
{
_originalContext = OperationContext.Current;
OperationContext.Current = _thisContext = context;
}
public void Dispose()
{
if (!_disposed)
{
if (_inflight || OperationContext.Current != _thisContext)
throw new InvalidOperationException();
_disposed = true;
OperationContext.Current = _originalContext;
_thisContext = null;
_originalContext = null;
}
}
internal void BeforeAwait()
{
if (_inflight)
return;
_inflight = true;
// leave _thisContext as the current context
}
internal void AfterAwait()
{
if (!_inflight)
throw new InvalidOperationException();
_inflight = false;
// ignore the current context, restore _thisContext
OperationContext.Current = _thisContext;
}
}
// ContinueOnScope extension
public static class TaskExt
{
public static SimpleAwaiter<TResult> ContinueOnScope<TResult>(this Task<TResult> @this, FlowingOperationContextScope scope)
{
return new SimpleAwaiter<TResult>(@this, scope.BeforeAwait, scope.AfterAwait);
}
// awaiter
public class SimpleAwaiter<TResult> :
System.Runtime.CompilerServices.INotifyCompletion
{
readonly Task<TResult> _task;
readonly Action _beforeAwait;
readonly Action _afterAwait;
public SimpleAwaiter(Task<TResult> task, Action beforeAwait, Action afterAwait)
{
_task = task;
_beforeAwait = beforeAwait;
_afterAwait = afterAwait;
}
public SimpleAwaiter<TResult> GetAwaiter()
{
return this;
}
public bool IsCompleted
{
get
{
// don't do anything if the task completed synchronously
// (we're on the same thread)
if (_task.IsCompleted)
return true;
_beforeAwait();
return false;
}
}
public TResult GetResult()
{
return _task.Result;
}
// INotifyCompletion
public void OnCompleted(Action continuation)
{
_task.ContinueWith(task =>
{
_afterAwait();
continuation();
},
CancellationToken.None,
TaskContinuationOptions.ExecuteSynchronously,
SynchronizationContext.Current != null ?
TaskScheduler.FromCurrentSynchronizationContext() :
TaskScheduler.Current);
}
}
}
簡単な方法は、待機をusingブロックの外に移動することです
public Task<Document> GetDocumentAsync(string docId)
{
var docClient = CreateDocumentServiceClient();
using (new OperationContextScope(docClient.InnerChannel))
{
var task = docClient.GetDocumentAsync(docId);
}
return await task;
}
これに役立つ独自のコードを作成し、これが誰にも役立つ場合に投稿することにしました。上記のSimpleAwaiter実装と比べて、間違って(予想外のレースなど)失敗する可能性が少し少ないようですが、あなたは裁判官です:
public static class WithOperationContextTaskExtensions
{
public static ContinueOnOperationContextAwaiter<TResult> WithOperationContext<TResult>(this Task<TResult> @this, bool configureAwait = true)
{
return new ContinueOnOperationContextAwaiter<TResult>(@this, configureAwait);
}
public static ContinueOnOperationContextAwaiter WithOperationContext(this Task @this, bool configureAwait = true)
{
return new ContinueOnOperationContextAwaiter(@this, configureAwait);
}
public class ContinueOnOperationContextAwaiter : INotifyCompletion
{
private readonly ConfiguredTaskAwaitable.ConfiguredTaskAwaiter _awaiter;
private OperationContext _operationContext;
public ContinueOnOperationContextAwaiter(Task task, bool continueOnCapturedContext = true)
{
if (task == null) throw new ArgumentNullException("task");
_awaiter = task.ConfigureAwait(continueOnCapturedContext).GetAwaiter();
}
public ContinueOnOperationContextAwaiter GetAwaiter() { return this; }
public bool IsCompleted { get { return _awaiter.IsCompleted; } }
public void OnCompleted(Action continuation)
{
_operationContext = OperationContext.Current;
_awaiter.OnCompleted(continuation);
}
public void GetResult()
{
OperationContext.Current = _operationContext;
_awaiter.GetResult();
}
}
public class ContinueOnOperationContextAwaiter<TResult> : INotifyCompletion
{
private readonly ConfiguredTaskAwaitable<TResult>.ConfiguredTaskAwaiter _awaiter;
private OperationContext _operationContext;
public ContinueOnOperationContextAwaiter(Task<TResult> task, bool continueOnCapturedContext = true)
{
if (task == null) throw new ArgumentNullException("task");
_awaiter = task.ConfigureAwait(continueOnCapturedContext).GetAwaiter();
}
public ContinueOnOperationContextAwaiter<TResult> GetAwaiter() { return this; }
public bool IsCompleted { get { return _awaiter.IsCompleted; } }
public void OnCompleted(Action continuation)
{
_operationContext = OperationContext.Current;
_awaiter.OnCompleted(continuation);
}
public TResult GetResult()
{
OperationContext.Current = _operationContext;
return _awaiter.GetResult();
}
}
}
使用法(少し手作業とネストはテストされていません...):
/// <summary>
/// Make a call to the service
/// </summary>
/// <param name="action"></param>
/// <param name="endpoint"> </param>
public async Task<ResultCallWrapper<TResult>> CallAsync<TResult>(Func<T, Task<TResult>> action, EndpointAddress endpoint)
{
using (ChannelLifetime<T> channelLifetime = new ChannelLifetime<T>(ConstructChannel(endpoint)))
{
// OperationContextScope doesn't work with async/await
var oldContext = OperationContext.Current;
OperationContext.Current = new OperationContext((IContextChannel)channelLifetime.Channel);
var result = await action(channelLifetime.Channel)
.WithOperationContext(configureAwait: false);
HttpResponseMessageProperty incomingMessageProperty = (HttpResponseMessageProperty)OperationContext.Current.IncomingMessageProperties[HttpResponseMessageProperty.Name];
string[] keys = incomingMessageProperty.Headers.AllKeys;
var headersOrig = keys.ToDictionary(t => t, t => incomingMessageProperty.Headers[t]);
OperationContext.Current = oldContext;
return new ResultCallWrapper<TResult>(result, new ReadOnlyDictionary<string, string>(headersOrig));
}
}
非同期フローは.Net 4.6.2からサポートされています。
.Net 4.6で実行されているASP.Net WebApiアプリケーションがあり、受け入れられた回答を使用しました。 TaskScheduler.FromCurrentSynchronizationContext()
は、現在の同期コンテキストがAspNetSynchronizationContext
のときにデッドロックの問題を引き起こしました。
継続タスクは実際のタスクの後にキューに入れられたため、実際のタスクは継続を待機しているのに対して、継続タスクは実際のタスクを完了する必要があります。つまり、タスクは両方とも互いに待機しています。
そこで、継続タスクを使用してTaskAwaiterを使用するように変更することで問題を修正しました。参照: https://blogs.msdn.Microsoft.com/lucian/2012/12/11/how-to-write-a-custom-awaiter/
これについてはしばらく経ちましたが、私は自分で自家製のソリューションをご紹介します。
OperationContextScope
なしでやる気がなければ、これらの線に沿って何かを考えるかもしれません:
拡張メソッド
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Security;
using System.Text;
using System.Threading.Tasks;
namespace Intexx.ServiceModel
{
public static class WcfExtensions
{
[DebuggerStepThrough]
public static void Call<TChannel>(this TChannel Client, Action<TChannel> Method) where TChannel : ICommunicationObject
{
try
{
Method.Invoke(Client);
}
finally
{
Cleanup(Client);
}
}
[DebuggerStepThrough]
public static TResult Call<TChannel, TResult>(this TChannel Client, Func<TChannel, TResult> Method) where TChannel : ICommunicationObject
{
try
{
return Method.Invoke(Client);
}
finally
{
Cleanup(Client);
}
}
[DebuggerStepThrough]
public async static Task CallAsync<TChannel>(this TChannel Client, Func<TChannel, Task> Method) where TChannel : ICommunicationObject
{
try
{
await Method.Invoke(Client);
}
finally
{
Cleanup(Client);
}
}
[DebuggerStepThrough]
public async static Task<TResult> CallAsync<TChannel, TResult>(this TChannel Client, Func<TChannel, Task<TResult>> Method) where TChannel : ICommunicationObject
{
try
{
return await Method.Invoke(Client);
}
finally
{
Cleanup(Client);
}
}
private static void Cleanup<TChannel>(TChannel Client) where TChannel : ICommunicationObject
{
try
{
if (Client.IsNotNull)
{
if (Client.State == CommunicationState.Faulted)
Client.Abort();
else
Client.Close();
}
}
catch (Exception ex)
{
Client.Abort();
if (!ex is CommunicationException && !ex is TimeoutException)
throw new Exception(ex.Message, ex);
}
finally
{
Client = null;
}
}
}
}
クライアントクラス
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Security;
using System.Text;
using System.Threading.Tasks;
namespace Reader
{
public class Client
{
public static CemReaderClient Create()
{
Tuple<Channels.Binding, EndpointAddress, double> oService;
try
{
oService = Main.Services(typeof(ICemReader));
return new CemReaderClient(oService.Item1, oService.Item2);
}
catch (KeyNotFoundException ex)
{
return null;
}
}
}
}
使用法(VBでは、コードが変換されないため)
Using oReader As Reader.CemReaderClient = Reader.Client.Create
If oReader.IsNotNothing Then
Dim lIsReading = Await oReader.CallAsync(Function(Reader As Reader.CemReaderClient)
Me.ConfigFilePath = If(Me.ConfigFilePath, Reader.GetConfigFilePath)
Me.BackupDrive = If(Me.BackupDrive, Reader.GetBackupDrive)
Me.SerialPort = If(Me.SerialPort, Reader.GetSerialPort)
Me.LogFolder = If(Me.LogFolder, Reader.GetLogFolder)
Return Reader.GetIsReadingAsync
End Function)
End If
End Using
これは、クライアント側で約15コール/秒の周波数負荷のもとで、本番環境で確実に実行されました(シリアル処理が許す限り高速です)。ただし、これは単一のスレッドで行われました。これは、スレッドの安全性について厳密にテストされていません。 YMMV。
私の場合、拡張メソッドを独自のプライベートNuGetパッケージにまとめることにしました。構造全体がかなり便利であることが判明しました。
もちろん、OperationContextScope
が必要になった場合、これを再評価する必要があります。
Tuple
クラスのClient
のビットは、サービスディスカバリサポート用です。誰かがそのコードも見たい場合は、叫んでください。答えを更新します。
私は少し混乱していますが、このブログを見つけました: WCFのタスクベースの非同期操作
非同期wcf通信があります:
[ServiceContract]
public interface iMessage
{
[OperationContract]
Task<string> GetMessages(string msg);
}
public class MessageService : iMessage
{
async Task<string> iMessage.GetMessages(string msg)
{
var task = Task.Factory.StartNew(() =>
{
Thread.Sleep(10000);
return "Return from Server : " + msg;
});
return await task.ConfigureAwait(false);
}
}
クライアント:
var client = new Proxy("BasicHttpBinding_iMessage");
var task = Task.Factory.StartNew(() => client.GetMessages("Hello"));
var str = await task;
これも良い方法ですか??