注意:虽然这个问题包含来自 Selenium WebDriver 和 Chrome DevTools 的详细信息,但我相信这个问题在更一般的线程同步上下文中也相关。
我有代码可以捕获来自浏览器的所有网络请求并对其执行某些操作(日志记录是一个典型的示例,但有时我会用它执行其他更复杂的操作)。为此,我为
Network.RequestPaused
事件创建了一个事件处理程序。当处理程序完成所需的操作时,它会调用 Network.ContinueRequestWithoutModification
继续从浏览器发送请求。在我的实际使用中,我有时也会调用 Network.ContinueRequestWithResponse
,修改请求等等,但在下面的示例中,为了简单起见,我只调用 Network.ContinueRequestWithoutModification
。
请注意,Network.RequestPaused
事件是在与运行测试的线程不同的线程上触发的。
一切似乎都工作正常,但偶尔我会在事件处理程序调用
Network.ContinueRequestWithoutModification
时收到以下异常:
System.Net.WebSockets.WebSocketException : The WebSocket is in an invalid state ('CloseSent') for this operation. Valid states are: 'Open, CloseReceived'
当我调查这个问题时,我意识到我在
Dispose
DevToolsSession
对象(也拥有 Network
对象)的同时得到了这个异常。发生异常是有道理的,因为处理程序是在不同的线程上执行的,并且我可能在执行 Network.ContinueRequestWithoutModification
期间甚至稍后一点调用 DevToolsSession.Dispose
。我设法通过以下测试重现问题(尽管仅使用 [Repeat(20)])
[Test, Repeat(20)]
public void EventHandlersCanRunAfterDevToolsIsDisposed()
{
using var driver = new ChromeDriver();
var caughtExceptions = new List<Exception>();
using (var devToolsSession = driver.GetDevToolsSession(new DevToolsOptions { ProtocolVersion = 127 }))
{
var networkAdapter = new NetworkAdapter(devToolsSession);
var fetch = new FetchAdapter(devToolsSession);
var network = new V127Network(networkAdapter, fetch);
var enableCommandSettings = new EnableCommandSettings
{
Patterns = new RequestPattern[]
{
new()
{
RequestStage = RequestStage.Request,
UrlPattern = "*"
}
}
};
fetch.Enable(enableCommandSettings);
network.RequestPaused += async (_, args) =>
{
try
{
// Do some stuff...
await network.ContinueRequestWithoutModification(args.RequestData);
}
catch (Exception ex)
{
caughtExceptions.Add(ex);
}
};
driver.Url = "https://www.google.com";
} // Disposing DevToolsSession
if (caughtExceptions.Count != 0)
throw new AggregateException(caughtExceptions);
}
注意:即使我在处置之前取消注册事件处理程序
我仍然可能会遇到异常,因为处理程序可能已经启动了。DevToolsSession
如何避免这些异常情况?试图思考各种线程同步机制,但没有找到任何简单且健壮的解决方案。
我不知道你的简单和稳健的标准,但这对我来说似乎是一个解决方案:
public class Network {
public event EventHandler RequestPaused;
public void FireEvent() {
RequestPaused.Invoke(null, null);
}
}
public class Session : IDisposable {
public void Dispose() {
Console.WriteLine("Disposing");
}
}
var session = new Session();
var network = new Network();
int count = 0;
const int stopValue = -1;
network.RequestPaused += async (sender, args) => {
if (Volatile.Read(ref count) == stopValue) {
Console.WriteLine("Disposing called");
return;
}
Interlocked.Increment(ref count);
await Task.Delay(1);
Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
Interlocked.Decrement(ref count);
};
for (int i = 1; i <= 10; i++) {
var captured = i;
Task.Run(() => {
Thread.Sleep(captured * 250);
network.FireEvent();
});
}
Thread.Sleep(500);
// wait til we dont have active operations, i.e. count is 0
// then set the stopValue so no new operations are started
while (0 != Interlocked.CompareExchange(ref count, stopValue, 0)) {
Console.WriteLine("operations in progress");
Thread.Sleep(100); // or spin
}
session.Dispose();