You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用PowerShell的New-ScriptBlockCallback实现HttpListener异步处理仍阻塞

问题分析与修复方案

我帮你排查了代码里的问题,导致异步失效、请求全部挂起的核心原因有两个,下面逐一拆解并给出修复后的完整代码:


核心问题根源

1. PowerShell事件处理的单线程瓶颈

你用Register-ObjectEvent注册的回调Action,所有请求都会在同一个共享线程上排队执行。当/timeout触发sleep 10时,这个线程会被完全阻塞10秒,后续所有请求的回调都得等这个线程释放才能执行,直接导致整个服务“假死”。

2. 主循环的轮询逻辑不合理

原来的主循环靠同步轮询$context.IsCompleted来发起新的监听,不仅效率低,而且时序上容易出现漏洞——一旦事件线程被阻塞,主循环的轮询虽然还在跑,但新的请求根本没机会被处理。


修复后的完整代码

function New-ScriptBlockCallback {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')]
    param(
        [parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [scriptblock]$Callback
    )
    if (-not ('CallbackEventBridge' -as [type])) {
        Add-Type @'
using System;
public sealed class CallbackEventBridge {
    public event AsyncCallback CallbackComplete = delegate { };
    private CallbackEventBridge() {}
    private void CallbackInternal(IAsyncResult result) {
        CallbackComplete(result);
    }
    public AsyncCallback Callback {
        get { return new AsyncCallback(CallbackInternal); }
    }
    public static CallbackEventBridge Create() {
        return new CallbackEventBridge();
    }
}
'@
    }
    $bridge = [callbackeventbridge]::create()
    # 保存事件句柄,防止被GC回收,方便后续清理
    $eventHandle = Register-ObjectEvent -InputObject $bridge -EventName callbackcomplete -Action $Callback -MessageData $args
    $bridge | Add-Member -MemberType NoteProperty -Name EventHandle -Value $eventHandle
    $bridge.Callback
}

$listener = New-Object System.Net.HttpListener
$listener.Prefixes.Add('http://+:8080/')
$listener.Start()
Write-Host 'Listening on http://localhost:8080/' -ForegroundColor Green

$requestListener = {
    [cmdletbinding()]
    param($result)
    [System.Net.HttpListener]$listener = $result.AsyncState;
    
    try {
        $context = $listener.EndGetContext($result);
        $request = $context.Request
        $response = $context.Response

        if ($request.Url -match '/timeout$') {
            $timeout = 10
            # 将耗时操作放入后台作业,彻底释放事件线程
            $job = Start-Job -ScriptBlock {
                param($context, $timeout)
                try {
                    Start-Sleep -Seconds $timeout
                    $message = "Timeout $timeout seconds completed at $(Get-Date -Format 'HH:mm:ss')"
                    [byte[]]$buffer = [System.Text.Encoding]::UTF8.GetBytes($message)
                    $context.Response.ContentLength64 = $buffer.Length
                    $context.Response.ContentType = 'text/html'
                    $output = $context.Response.OutputStream
                    $output.Write($buffer, 0, $buffer.Length)
                }
                finally {
                    # 确保资源被正确释放
                    $context.Response.OutputStream.Close()
                    $context.Response.Close()
                }
            } -ArgumentList $context, $timeout

            # 自动清理完成的后台作业,避免内存泄漏
            Register-ObjectEvent -InputObject $job -EventName StateChanged -Action {
                if ($sender.State -eq [System.Management.Automation.JobState]::Completed) {
                    $sender | Remove-Job -Force
                }
            } | Out-Null
        } else {
            # 快速响应逻辑保持不变
            $message = "No timeout - response at $(Get-Date -Format 'HH:mm:ss')"
            $response.ContentType = 'text/html'
            [byte[]]$buffer = [System.Text.Encoding]::UTF8.GetBytes($message)
            $response.ContentLength64 = $buffer.Length
            $output = $response.OutputStream
            $output.Write($buffer, 0, $buffer.Length)
            $output.Close()
            $response.Close()
        }
    }
    finally {
        # 无论请求处理成功/失败,都立即发起新的异步监听
        $null = $listener.BeginGetContext((New-ScriptBlockCallback -Callback $requestListener), $listener)
    }
}

# 启动第一个异步监听
$null = $listener.BeginGetContext((New-ScriptBlockCallback -Callback $requestListener), $listener)

# 主循环改为等待用户输入终止,替代低效的轮询
Write-Host 'Press any key to stop the listener...'
$null = $Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown')

# 清理所有资源
$listener.Stop()
$listener.Close()
Get-EventSubscriber | Unregister-Event -Force
Get-Job | Remove-Job -Force
Write-Host 'Listener terminated.' -ForegroundColor Yellow

关键修改说明

  1. 耗时操作后台化
    把原来阻塞线程的sleep 10放到Start-Job后台作业里,事件线程只需启动作业就立即返回,继续处理其他请求的回调,彻底解决单线程阻塞问题。

  2. 监听逻辑内移
    移除原来的轮询循环,改为在每个请求处理的finally块里主动发起新的BeginGetContext,确保服务始终处于监听状态,时序更可靠。

  3. 资源清理优化
    添加了事件句柄保存、后台作业自动清理的逻辑,避免长时间运行导致内存泄漏。


测试验证

启动修复后的脚本后:

  1. 打开第一个控制台,执行循环请求:
    while ($true) { Invoke-RestMethod http://localhost:8080/; Start-Sleep -Milliseconds 500 }
    
  2. 打开第二个控制台,执行超时请求:
    Invoke-RestMethod http://localhost:8080/timeout
    

你会看到第一个控制台的循环请求完全不受影响,继续正常响应;10秒后第二个控制台会收到超时结果。

内容的提问来源于stack exchange,提问作者coder9001

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.13 07:37:04