web-dev-qa-db-ja.com

Lock Web APIコントローラーメソッド

C#と.Net Framework 4.7を使用してASP.NET Web Apiアプリケーションを開発しています。

一度に1つのスレッドだけで実行したいメソッドがコントローラーにあります。つまり、誰かがこのメソッドを呼び出した場合、別の呼び出しはメソッドが終了するまで待機する必要があります。

私はこれを見つけました SO答え それは仕事をすることができました。しかし、ここではキューを使用しており、そのキューを消費する方法を知りません。その答えでは、キューを消費するWindowsサービスを作成できますが、ソリューションに別のアプリケーションを追加したくないと説明しています。

私は次のようにWeb APIメソッド内にロックを追加することを考えました:

[HttpPut]
[Route("api/Public/SendCommissioning/{serial}/{withChildren}")]
public HttpResponseMessage SendCommissioning(string serial, bool withChildren)
{
    lock
    {
        string errorMsg = "Cannot set commissioning.";

        HttpResponseMessage response = null;
        bool serverFound = true;

        try
        {
            [ ... ]
        }
        catch (Exception ex)
        {
            _log.Error(ex.Message);

            response = Request.CreateResponse(HttpStatusCode.InternalServerError);
            response.ReasonPhrase = errorMsg;
        }

        return response;
    }
}

しかし、メソッドの実行に問題があり、保留中の呼び出しがすべて失われるか、間違っている可能性があり、呼び出し(スレッド)が待機する場合、保留中の呼び出しの多くをブロックする可能性があるため、これは良い解決策ではないと思います他の人が終わるまで。つまり、これを使用すると、デッドロックに陥る可能性があると思います。

受け取ったのと同じ順序で呼び出しを実行する必要があるため、これを試しています。このアクションログを見てください。

2017-06-20 09:17:43,306 DEBUG [12] WebsiteAction - ENTERING PublicController::SendCommissioning , serial : 38441110778119919475, withChildren : False
2017-06-20 09:17:43,494 DEBUG [13] WebsiteAction - ENTERING PublicController::SendCommissioning , serial : 38561140779115949572, withChildren : False
2017-06-20 09:17:43,683 DEBUG [5] WebsiteAction - ENTERING PublicController::SendCommissioning , serial : 38551180775118959070, withChildren : False
2017-06-20 09:17:43,700 DEBUG [12] WebsiteAction - EXITING PublicController::SendCommissioning 
2017-06-20 09:17:43,722 DEBUG [5] WebsiteAction - EXITING PublicController::SendCommissioning 
2017-06-20 09:17:43,741 DEBUG [13] WebsiteAction - EXITING PublicController::SendCommissioning 

それらのいずれかが終了する前に3つの呼び出しを受け取ります:スレッド[12], [13] and [5]。しかし、最後のものは2番目のものの前に終わります[12], [5] and [13]

これを許可しないメカニズムが必要です。

呼び出しが自分と同じ順序で処理されるようにするにはどうすればよいですか?

12
VansFannel

ロックソリューションは正常に機能するはずです。要求が失敗すると、ロックが解放され、保留中の他の要求がロックに入ることができます。デッドロックは発生しません。

このソリューションの唯一の問題は、Webリクエストがおそらく長時間ハングアップすることです(クライアントエンドからのタイムアウトが発生する可能性があります)。

public class MyApi : ApiController
{
    public static readonly object LockObject = new object();

    [HttpPut]
    [Route("api/Public/SendCommissioning/{serial}/{withChildren}")]
    public HttpResponseMessage SendCommissioning(string serial, bool withChildren)
    {
        lock ( LockObject )
        {
            //Do stuff
        }
    }
}

リクエストのハングに関する問題を解決するには、キューを利用して、ジョブが完了するまでバックエンドをポーリングする必要があります(または、必要であれば、SignalRを試してください)。例えば:

//This is a sample with Request/Result classes (Simply implement as you see fit)
public static class MyBackgroundWorker
{
    private static ConcurrentQueue<KeyValuePair<Guid, Request>> _queue = new ConcurrentQueue<KeyValuePair<Guid, Result>>()
    public static ConcurrentDictionary<Guid, Result> Results = new ConcurrentDictionary<Guid, Result>();

    static MyBackgroundWorker()
    {
         var thread = new Thread(ProcessQueue);
         thread.Start();
    }

    private static void ProcessQueue()
    {
         KeyValuePair<Guid, Request> req;
         while(_queue.TryDequeue(out req))
         {
             //Do processing here (Make sure to do it in a try/catch block)
             Results.TryAdd(req.Key, result);
         }
    }

    public static Guid AddItem(Request req)
    {
        var guid = new Guid();
        _queue.Enqueue(new KeyValuePair(guid, req));
        return guid;
    }
}


public class MyApi : ApiController
{
    [HttpPut]
    [Route("api/Public/SendCommissioning/{serial}/{withChildren}")]
    public HttpResponseMessage SendCommissioning(string serial, bool withChildren)
    {
        var guid = MyBackgroundWorker.AddItem(new Request(serial, withChildren));
        return guid;
    }

    [HttpGet]
    [Route("api/Public/GetCommissioning/{guid}")]
    public HttpResponseMessage GetCommissioning(string guid)
    {
        if ( MyBackgroundWorker.Results.TryRemove(new Guid(guid), out Result res) )
        {
            return res;
        }
        else
        {
            //Return result not done
        }
    }
}
8
FrankerZ

アプローチは1つなので、さまざまなレベルでロックできると思います。

外部サービスとしてredisを利用するシステム(またはWebアプリ)を見つけました。 redisでは、リクエストのキーを保存します。あなたの場合、それはメソッドの名前かもしれません。このアプローチでは、最初にロックが存在する(redisと通信する)かどうかをチェックし、要求をブロックするアクションフィルターを用意しています。

Redisの良い点は、非常に高速であり、キーが消えるタイムアウトを指定してみましょう。これにより、ロックが永久に動かなくなることを防ぎます。

1
Jim Aho