web-dev-qa-db-ja.com

async / awaitを使用してWCFサービスを呼び出すパターン

タスクベースの操作 でプロキシを生成しました。

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ブロックの外側で使用します。

それが問題です!しかし、どうすれば適切に修正できますか?

この男はMSDNの言うことをやった

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サービスを呼び出し、ServiceClientOperationContextScopeの両方を破棄する実証済みのパターンはありますか?おそらくマイクロソフトの誰か(おそらくグルの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プロキシTaskthenを閉じる(または中止する)ServiceClientが必要です。それ以外の場合は、サービスを呼び出した後、すぐにそれを閉じてください(バイトはネットワーク経由で送信されます)!

OK、それを機能させる方法はありますが、Noseratioが述べているように、信頼できるソースから答えを得るのは良いことです。

57
gabrielmaldi

実行可能な解決策は、カスタム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);
    }
}

FlowingOperationContextScopeContinueOnScopeの実装は次のとおりです(わずかにテストされています)。

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);
        }
    }
}
30
noseratio

簡単な方法は、待機をusingブロックの外に移動することです

public Task<Document> GetDocumentAsync(string docId)
{
    var docClient = CreateDocumentServiceClient();
    using (new OperationContextScope(docClient.InnerChannel))
    {
        var task = docClient.GetDocumentAsync(docId);
    }
    return await task;
}
4
James Wang

これに役立つ独自のコードを作成し、これが誰にも役立つ場合に投稿することにしました。上記の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));
        }
    }
2
jamespconnor

非同期フローは.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/

2
weichch

これについてはしばらく経ちましたが、私は自分で自家製のソリューションをご紹介します。

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のビットは、サービスディスカバリサポート用です。誰かがそのコードも見たい場合は、叫んでください。答えを更新します。

1
InteXX

私は少し混乱していますが、このブログを見つけました: 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;

これも良い方法ですか??

0
Franki1986