62

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

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であること:ServiceClientOperationContextScope

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) を呼び出さないことです。

custom を使用してを伝播する方法も見つけました。しかし、それが多くの「危険な」コードであるという事実に加えて、彼は次のように述べています。OperationContextScopeSynchronizationContext

操作コンテキストスコープの破棄に関していくつかの小さな問題があることに注意する価値があります (呼び出しスレッドでのみ破棄できるため) が、これは問題ではないようです (少なくともDispose() を実装しますが、Finalize() は実装しません。

それで、私たちはここで運が悪いのですか?async/await を使用して WCF サービスを呼び出し、ServiceClientとの両方を破棄するための実証済みのパターンはありOperationContextScopeますか? たぶん、Microsoft の誰か (おそらく教祖 Stephen Toub :)) が助けてくれるでしょう。

ありがとう!

[アップデート]

ユーザー Noseratio の多くの助けを借りて、機能するものを思いつきました: 使用しないでくださいOperationContextScopeこれらの理由のいずれかで使用している場合は、シナリオに合った回避策を見つけてください。それ以外の場合、本当に必要な場合は、それをキャプチャするOperationContextScopea の実装を考え出す必要がありますが、それは非常に難しいようです(可能であれば、これがデフォルトの動作ではない理由があるはずです)。 )。SynchronizationContext

したがって、完全な作業コードは次のとおりです。

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を使用してから閉じる (または中止する)ServiceClient必要があります。そうしないと、サービスを呼び出した直後に (バイトがネットワーク経由で送信されている間に) 閉じることになります。

OK、それを機能させる方法はありますが、Noseratio が述べているように、信頼できる情報源から回答を得られるとよいでしょう。

4

8 に答える 8

37

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

FlowingOperationContextScopeandの実装を次に示します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);
        }
    }
}
于 2014-03-31T03:12:53.213 に答える
10

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

public Task<Document> GetDocumentAsync(string docId)
{
    var docClient = CreateDocumentServiceClient();
    using (new OperationContextScope(docClient.InnerChannel))
    {
        var task = docClient.GetDocumentAsync(docId);
    }
    return await task;
}
于 2016-12-15T03:37:03.500 に答える
-1

これが役立つかどうかはわかりませんが、検索でこの質問を見て同じ質問に答えた後、これに出会いまし

それを踏まえると、コードは次のようになるはずです。

public async Task<HomeInfo> GetHomeInfoAsync(DateTime timestamp)
{
    using (var client = CreateDocumentServiceClient())
    {
        await client.BeginGetHomeInfoAsync(timestamp);
    }
}

私の答えはかなり遅れていることに気づきました:Pしかし、それは他の誰かを助けるかもしれません.

于 2014-08-06T10:53:13.990 に答える