终止线程 运行 一个事件

Terminating Thread Running an Event

我写了一个 API 来自动化某个网站。但是,在测试阶段,我注意到(不太确定)我的线程没有被正确终止。

我正在使用 WebBrowser 对象在线程内导航,以便它与我的程序同步工作:

private void NavigateThroughTread(string url)
{
    Console.WriteLine("Defining thread...");

    var th = new Thread(() =>
    {   
        _wb = new WebBrowser();
        _wb.DocumentCompleted += PageLoaded;
        _wb.Visible = true;
        _wb.Navigate(url);
        Console.WriteLine("Web browser navigated.");
        Application.Run();
    });
    Console.WriteLine("Thread defined.");

    th.SetApartmentState(ApartmentState.STA);

    Console.WriteLine("Before thread start...");
    th.Start();
    Console.WriteLine("Thread started.");

    while (th.IsAlive) { }
    Console.WriteLine("Journey ends.");
}

private void PageLoaded(object sender, WebBrowserDocumentCompletedEventArgs e)
{
    Console.WriteLine("Pages loads...");
    .
    .
    .
    switch (_action)
    {
        .
        .
        .
        case ENUM.FarmActions.Idle:
           _wb.Navigate(new Uri("about:blank"));
           _action = ENUM.FarmActions.Exit;

           return;

        case ENUM.FarmActions.Exit:
            Console.WriteLine("Disposing wb...");
            _wb.DocumentCompleted -= PageLoaded;
            _wb.Dispose();

            break;
    }

    Application.ExitThread();   // Stops the thread
}

我是这样调用这个函数的:

public int Attack(int x, int y, ArmyBuilder army)
{
    // instruct to attack the village
    _action = ENUM.FarmActions.Attack;

    //get the army and coordinates
    _army = army;
    _enemyCoordinates[X] = x;
    _enemyCoordinates[Y] = y;

    //Place the attack command
    _errorFlag = true; // the action is not complated, the flag will set as false once action is complete
    _attackFlag = false; // attack is not made yet
    Console.WriteLine("Journey starts");

    NavigateThroughTread(_url.GetUrl(ENUM.Screens.RallyPoint));

    return _errorFlag ? -1 : CalculateDistance();
}

所以问题是,当我调用 Attack 函数时,像这样调用了几次:

_command.Attack(509, 355, new ArmyBuilder(testArmy_lc));
_command.Attack(509, 354, new ArmyBuilder(testArmy_lc));
_command.Attack(505, 356, new ArmyBuilder(testArmy_lc));
_command.Attack(504, 356, new ArmyBuilder(testArmy_lc));
_command.Attack(504, 359, new ArmyBuilder(testArmy_lc));
_command.Attack(505, 356, new ArmyBuilder(testArmy_lc));
_command.Attack(504, 356, new ArmyBuilder(testArmy_lc));
_command.Attack(504, 359, new ArmyBuilder(testArmy_lc));

我的应用程序大部分时间都卡在其中一个函数中(通常发生在第 4 或第 5 个之后)。当它卡住时,我看到的最后一条日志是

Web browser navigated.

我认为这与我的线程终止有关。有人可以告诉我如何 运行 一个线程 运行 是 DocumentCompleted 事件吗?

我没有看到任何明显的死锁原因,在测试代码时也没有重现。代码中有许多缺陷,但没有什么可以大声喊 "here!" 的。我只能提出建议:

  1. 认为您根本不需要线程。 while (th.IsAlive) { } 热循环会在您等待浏览器代码完成作业时阻塞您的主线程。这不是使用线程的有用方法,您还不如使用主线程。这立即消除了大量潜在的挂起原因。

  2. PageLoaded 中的状态逻辑有风险。我们无法看到所有内容,但一个明显的问题是您处理了两次 WebBrowser。如果您在没有调用 Navigate() 的情况下使用 return,那么您将按照描述挂起。无需取消订阅该事件,但同样的故事,如果您取消订阅但没有全部取消订阅 Application.Exit() 那么您将按照描述挂起。状态机很难调试,彻底的日志记录是必要的。通过移动 Dispose() 调用并取消订阅逻辑之外的事件来最小化风险,它不属于那里。并且您需要测试当任何 Navigate() 调用以失败告终时会发生什么,重定向到您没有预料到的页面。

  3. _wb.Dispose()调用有风险。请注意,您在 WebBrowser 的 DocumentCompleted 事件运行期间销毁了它。从技术上讲,可以 return 对不再存在或不存在的代码执行代码。这可能会触发浏览器中的竞争条件。除了在调试器中,还有一个专门的 MDA 来检查这个问题。通过将 Dispose() 调用移动到它所属的 Application.Run() 调用之后可以轻松避免这种情况。

  4. while 循环会消耗 100% 的核心,可能会使工作线程饿死。不是解释死锁的充分理由,但肯定是不必要的。请改用 Thread.Join()。

  5. 您在这段代码中创建了很多 WebBrowser 对象。它是一个非常重的对象,正如您可以想象的那样,您需要密切关注程序中的内存使用情况。尤其是不受管理的那种。如果浏览器发生泄漏,就像它们经常发生的那样,您可以在技术上创建一个 WB 初始化正常但没有足够的内存来加载页面的场景。强烈赞成只使用 one WB.

  6. 您需要考虑这很可能是一个环境问题。在该列表的顶部永远是反恶意软件和防火墙,他们总是有充分的理由特别对待浏览器,因为这是最常见的恶意软件注入向量。您需要 运行 在禁用反恶意软件和防火墙的情况下进行测试,以确保它不是挂起的原因。

  7. 另一个环境问题是我在测试这段代码时注意到的,Google 对我如此频繁地点击它感到生气并开始限制请求,大大减慢了代码速度。与网站所有者交谈,询问他是否有类似的阻止或限制对策,大多数人都有。您需要测试您的状态逻辑,以验证它在浏览器重定向到错误页面时是否仍然正常工作。

  8. 还有一个环境问题是 WB 在某些情况下会自己显示一个对话框。这可能会在第 3 方代码中出现死锁,很难诊断。您至少应该将 WebBrower.ScriptErrorsSuppressed 设置为 true 但要注意您加载的网页中的 Javascript 代码,它本身会创建新的 windows 或显示警报对话框。使用一个 WB 是解决方法。

  9. 请记住,您的程序只能与您的 Internet 连接和网页服务器一样可靠。这当然不是一个非常好的地方,两者都超出您的范围,并且您没有很好的异常来帮助您诊断此类故障。并考虑到您可能还没有足够好地测试您的程序,以检查它是否可以在这样的故障中幸存下来,它发生得还不够。

相当多的清单,首先关注消除不必要的线程并暂时抑制反恶意软件。很快,接下来只关注使用 one WebBrowser.

汉斯,谢谢你,我能够用你的一个想法解决这个问题。当你花时间给我一个长答案时,我想以同样的方式回应。

2 - 我仔细地构建了状态机结构并且有很多日志(你可以从我的git account中看到它)也做了很多调试。我确信在完成导航后,我只使用了一次 Application.ExitThread()wb.Dispose()

3 - 我尝试将 wb.Dispose() 放在事件之外,但是我找不到线程仍然存在的任何其他地方。如果我尝试在线程内创建的线程外处理 WebBrowser,应用程序会给我一个错误。

4 - 我将代码 while (th.IsAlive) { } 更改为 th.Join(2000) 这绝对是一个更好的主意,但没有改变任何东西。它优化了代码,正如你提到的,它防止了我的 CPU.

的 100% 核心烧毁

5 - 我尝试使用在构造函数中实例化的单个 WebBrowser 对象。但是,当我尝试在线程内导航时,应用程序甚至不再触发事件。出于某种原因,我无法使用单个 WB 对象 运行。

6,7 - 我用不同的 PC 和不同的网络(有防火墙和非防火墙保护)测试了我的应用程序。我也更改了 windows 防火墙选项,但没有费力。在我的原始代码中,我确实有 _wb.ScriptErrorsSuppressed = true;,所以这也不应该是问题所在。

8,9 - 如果是这些原因,我也无能为力。但我怀疑真正的问题是因为他们造成的。

1 - 这个建议很好。我尝试在不使用线程的情况下实现我的代码,它现在工作正常。这是它的样子(仍然需要大量优化)

// Constructer
public FarmActions(string token)
{
    // set the urls using the token
    _url = new URL(token);

    // define web browser properties
    _wb = new WebBrowser();
    _wb.DocumentCompleted += PageLoaded;
    _wb.Visible = true;
    _wb.AllowNavigation = true;
    _wb.ScriptErrorsSuppressed = true;
}

public int Attack(int x, int y, ArmyBuilder army)
{
    // instruct to attack the village
    _action = ENUM.FarmActions.Attack;

    //get the army and coordinates
    _army = army;
    _enemyCoordinates[X] = x;
    _enemyCoordinates[Y] = y;

    //Place the attack command
    _errorFlag = true; // the action is not complated, the flag will set as false once action is complete
    _attackFlag = false; // attack is not made yet
    _isAlive = true;

    Console.WriteLine("-------------------------");
    Console.WriteLine("Journey starts");

    NavigateThroughTread(_url.GetUrl(ENUM.Screens.RallyPoint));

    return _errorFlag ? -1 : CalculateDistance();
}

private void NavigateThroughTread(string url)
{
    Console.WriteLine("Defining thread...");
    _wb.Navigate(url);

    while (_isAlive) Application.DoEvents();
}

private void PageLoaded(object sender, WebBrowserDocumentCompletedEventArgs e)
{
    Console.WriteLine("Pages loads...");
    .
    .
    .
    switch (_action)
    {
        .
        .
        .
        case ENUM.FarmActions.Idle:
           _wb.Navigate(new Uri("about:blank"));
           _action = ENUM.FarmActions.Exit;

           return;

        case ENUM.FarmActions.Exit:

            break;
    }
    _isAlive = false;
}

这就是我在不使用线程的情况下能够等待的方式。

主要问题可能如您在第 3 或第 5 条中提到的那样。但是我花了几个小时无法解决问题。

无论如何,感谢您的帮助。