【问题标题】:Correct use of a ConcurrentQueue within a HttpModule?在 HttpModule 中正确使用 ConcurrentQueue?
【发布时间】:2026-02-15 13:00:02
【问题描述】:

我正在尝试为使用异步编程处理图像的 HttpModule 添加速度提升。

虽然看起来我的性能确实得到了提升,但我想检查一下我是否正确使用了提供的工具。

我特别担心我处理队列不正确。

我正在采取的方法。

  1. 初始化并发队列
  2. 将 ProcessImage 方法添加到队列中 AddOnBeginRequestAsync 中的 BeginEventHandler
  3. 在 EndEventHandler 上处理队列 AddOnBeginRequestAsync

代码很多,所以我提前道歉,但异步编程很难:

字段

/// <summary>
/// The thread safe fifo queue.
/// </summary>
private static ConcurrentQueue<Action> imageOperations;

/// <summary>
/// A value indicating whether the application has started.
/// </summary>
private static bool hasAppStarted = false;

httpmodule 初始化

/// <summary>
/// Initializes a module and prepares it to handle requests.
/// </summary>
/// <param name="context">
/// An <see cref="T:System.Web.HttpApplication"/> that provides 
/// access to the methods, properties, and events common to all 
/// application objects within an ASP.NET application
/// </param>
public void Init(HttpApplication context)
{
    if (!hasAppStarted)
    {
        lock (SyncRoot)
        {
            if (!hasAppStarted)
            {
                imageOperations = new ConcurrentQueue<Action>();
                DiskCache.CreateCacheDirectories();
                hasAppStarted = true;
            }
        }
    }

    context.AddOnBeginRequestAsync(OnBeginAsync, OnEndAsync);
    context.PreSendRequestHeaders += this.ContextPreSendRequestHeaders;

}

事件处理程序

/// <summary>
/// The <see cref="T:System.Web.BeginEventHandler"/>  that starts 
/// asynchronous processing 
/// of the <see cref="T:System.Web.HttpApplication.BeginRequest"/>.
/// </summary>
/// <param name="sender">The source of the event.</param>
/// <param name="e">
/// An <see cref="T:System.EventArgs">EventArgs</see> that contains 
/// the event data.
/// </param>
/// <param name="cb">
/// The delegate to call when the asynchronous method call is complete. 
/// If cb is null, the delegate is not called.
/// </param>
/// <param name="extraData">
/// Any additional data needed to process the request.
/// </param>
/// <returns></returns>
IAsyncResult OnBeginAsync(
object sender, EventArgs e, AsyncCallback cb, object extraData)
{
    HttpContext context = ((HttpApplication)sender).Context;
    EnqueueDelegate enqueueDelegate = new EnqueueDelegate(Enqueue);

    return enqueueDelegate.BeginInvoke(context, cb, extraData);

}

/// <summary>
/// The method that handles asynchronous events such as application events.
/// </summary>
/// <param name="result">
/// The <see cref="T:System.IAsyncResult"/> that is the result of the 
/// <see cref="T:System.Web.BeginEventHandler"/> operation.
/// </param>
public void OnEndAsync(IAsyncResult result)
{
    // An action to consume the ConcurrentQueue.
    Action action = () =>
    {
        Action op;

        while (imageOperations.TryDequeue(out op))
        {
            op();
        }
    };

    // Start 4 concurrent consuming actions.
    Parallel.Invoke(action, action, action, action);
}

委托和处理

/// <summary>
/// The delegate void representing the Enqueue method.
/// </summary>
/// <param name="context">
/// the <see cref="T:System.Web.HttpContext">HttpContext</see> object that 
/// provides references to the intrinsic server objects 
/// </param>
private delegate void EnqueueDelegate(HttpContext context);

/// <summary>
/// Adds the method to the queue.
/// </summary>
/// <param name="context">
/// the <see cref="T:System.Web.HttpContext">HttpContext</see> object that 
/// provides references to the intrinsic server objects 
/// </param>
private void Enqueue(HttpContext context)
{
    imageOperations.Enqueue(() => ProcessImage(context));
}

【问题讨论】:

    标签: c# asp.net multithreading concurrency httpmodule


    【解决方案1】:

    看起来您的ProcessImage 方法在HttpContext 上有效,每次调用您的HttpModule 都会是一个实例。您的 HttpModule 的 OnBeginAsync 会根据需要在每个 Web 请求中被调用,并且您的委托已经为您提供了执行异步操作的逻辑。这意味着,您不需要 4 个并发线程,因为无论如何您只有一个 context 实例可以处理。而且我们不需要ConcurrentQueue,因为context 上的所有工作都应该在请求-响应的生命周期内完成。

    总而言之,您不需要ConcurrentQueue,因为:

    1. 通过 HttpModule 的请求已经是并发的(来自 Web 主机架构)。
    2. 每个请求都在单个 context 实例上运行。
    3. 在从 OnEndAsync 返回之前,您需要在 context 上完成 ProcessImage 的工作。

    相反,您只想在OnBeginAsync 方法中开始您的ProcessImage 的后台工作,然后确保在您的OnEndAsync 方法中完成该工作。此外,由于所有更改都是直接在context 实例上进行的(我假设,因为ProcessImage 没有返回类型,所以它正在更新context),你不需要做任何事情从您的处理中获取结果对象的进一步工作。

    您可以放弃 ConcurrentQueue 并简单地使用:

    IAsyncResult OnBeginAsync(object sender, EventArgs e, 
                              AsyncCallback cb, object extraData)
    {
        HttpContext context = ((HttpApplication)sender).Context;
        EnqueueDelegate enqueueDelegate = new EnqueueDelegate(ProcessImage);
    
        return enqueueDelegate.BeginInvoke(context, cb, extraData);
    }
    
    public void OnEndAsync(IAsyncResult result)
    {
        // Ensure our ProcessImage has completed in the background.
        while (!result.IsComplete)
        {
            System.Threading.Thread.Sleep(1); 
        }
    }
    

    您可以删除 ConcurrentQueue&lt;Action&gt; imageOperationsEnqueue,也可以将 EnqueueDelegate 重命名为 ProcessImageDelegate,因为它现在可以直接使用该方法。

    注意:OnBeginAsync 时,您的context 可能还没有为ProcessImage 做好准备。如果是这种情况,您必须将ProcessImage 作为OnEndAsync 中的简单同步调用移动。然而,话虽如此,ProcessImage 确实有可能通过一些并发来改进。

    我要提出的另一个挑剔点是 hasAppStarted 可以重命名为 hasModuleInitialized 以减少歧义。

    【讨论】:

    • 谢谢您,是的,我一定会更改字段名称。 :) 你几乎已经了解了我在OnEndAsync 中所困扰的事情,我从 MSDN 上的一个示例中获得了 4 个线程位,但我不确定它对我的过程是有益还是有害。话虽如此,将进程减少到一个线程似乎使我的测试页面加快了约 20%。只保留一个是否有意义,或者您能否提出一种以更有益的方式使用更多线程的方法?
    • 谢谢你...不幸的是你的编辑似乎破坏了一些东西。这些图像肯定会被处理并保存到文件系统中,但现在会以未经处理的原始形式返回给浏览器。
    • 是的,这是有道理的。如果我想要更快的速度,请回到绘图板:/
    • @JamesSouth 经过审查,似乎 ConcurrentQueue 甚至没有必要,因为(1)通过 HttpModule 的请求已经并发,(2)每个请求只有一个 context ,以及 (3) 在从 OnEndAsync 返回之前,您需要处理一个 context
    • 看起来它正在工作。非常感谢,我开始担心了。