EventHandle.WaitOne + WebBrowser = 等待文档完成时死锁

5
我有一个关于在C#程序中自动化WebBrowsing的问题。 我之前在一个BHO中使用过这段代码,而在纯C#程序中似乎存在一种死锁。我已经指示程序点击搜索按钮,然后等待(通过ManualResetEvent)documentcomplete信号。但现在似乎点击搜索按钮直到ManualResetEvent信号超时才会被处理。此后,点击会被执行,并且DocumentComplete事件也会触发。
关于程序结构: WebBrowser-Control是WindowsForm的一部分。WebBrowser Control被传递给运行线程的类。这个类再次将控件传递给另一个类,在这个类中编写了根据加载的WebBrowser具体行为的程序。
所以在代码中看起来是这样的:
  1. Setup of the thread

    _runner = new Thread(runner);
    _runner.SetApartmentState(ApartmentState.STA);
    _runner.IsBackground = true;
    _runner.Start();
    
  2. Processing of withingthe Runner-Method

    b.placeTipp(workStructure);
    
  3. The PlaceTipp-Method

    public void placeTipp(ref OverallTippStructure tipp)
        {
            _expectedUrl = String.Empty;
            _betUrl = String.Empty;
            _status = BookieStatusType.CHECKLOGIN;
    
            while (true)
            {
                _mreWaitForAction.Reset();
                checkIETab();
    
                switch (_status)
                {
                    case BookieStatusType.REQUESTWEBSITE:
                        ConsoleWriter.writeToConsole(String.Format("Bookie {0}: Keine IE-Tab vorhanden. Fordere eine an", this.BookieName));
                        //if (RequestIETabEvent != null)
                        //    RequestIETabEvent(this, new EventArgs());
                        _status = BookieStatusType.NAVIGATETOWEBSITE;
                        _mreWaitForAction.Set();
                        break;
                    case BookieStatusType.NAVIGATETOWEBSITE:                                                
                        _webBrowser.Navigate(@"http://www.nordicbet.com");
                        break;
                    case BookieStatusType.CHECKLOGIN:
                        checkLogin();
                        break;
                    case BookieStatusType.LOGINNEEDED:
                        doLogin();
                        break;
                    case BookieStatusType.LOGGEDIN:
                        _status = BookieStatusType.SEARCHTIPP;
                        _mreWaitForAction.Set();
                        break;
                    case BookieStatusType.SEARCHTIPP:
                        searchTipp(tipp);
                        break;
                    case BookieStatusType.NAVTOSB:
                        NavToSB();
                        break;
                    case BookieStatusType.GETMARKET:
                        getMarket(tipp);
                        break;
                    case BookieStatusType.PLACEBET:
                        placeBet(tipp);
                        break;
                    case BookieStatusType.CONFIRMBET:
                        confirmBet();
                        break;
                    case BookieStatusType.EXTRACTBETDATA:
                        extractBetId(ref tipp);
                        break;
                    case BookieStatusType.FINISHED:
                        return;
                }
    
    
                if (!_mreWaitForAction.WaitOne(60000))
                {
                    //Sonderüberpüfung be LoginNeeded
                    if (_status == BookieStatusType.LOGINNEEDED)
                    {
                        //checkLogin();
                        throw new BookieLoginFailedExcpetion();
                    }
                    //TIMEOUT!
                    ConsoleWriter.writeToConsole(String.Format("Bookie {0}: Timeout bei warten auf nächsten Schritt. Status war {1}", this.BookieName, this._status.ToString()));
                    throw new BookieTimeoutExcpetion(String.Format("Bookie {0}: Timeout bei dem Warten auf Ereignis", this.BookieName));
                }
            }
        }
    
  4. The SearchTipp-Method where the Deadlock is happening:

    private void searchTipp(OverallTippStructure tipp)
        {
            if (_webBrowser.InvokeRequired)
            {
                _webBrowser.Invoke(new delegatePlaceBet(searchTipp), new object[] { tipp });
            }
            else
            {
                ConsoleWriter.writeToConsole(String.Format("Bookie {0}: Suche Tipp {1}", this.BookieName, tipp.BookieMatchName));
                _expectedUrl = String.Empty;
                if (!_webBrowser.Url.ToString().StartsWith("https://www.nordicbet.com/eng/sportsbook"))
                {
                    ConsoleWriter.writeToConsole(String.Format("Bookie {0}: Nicht auf Sportsbookseite. Navigiere", this.BookieName));
                    _status = BookieStatusType.NAVTOSB;
                    _mreWaitForAction.Set();
                    return;
                }
                _searchCompleted = false;
                HtmlDocument doc = _webBrowser.Document;
    
                if (doc != null)
                {
    
                    mshtml.IHTMLInputElement elemSearch = (mshtml.IHTMLInputElement)doc.GetElementById("query").DomElement;
                    if (elemSearch != null)
                    {
                        Thread.Sleep(Delayer.delay(2000, 10000));
                        elemSearch.value = tipp.BookieMatchName;
    
                        mshtml.IHTMLElement elemSearchButton = (mshtml.IHTMLElement)doc.GetElementById("search_button").DomElement;
                        if (elemSearchButton != null)
                        {
                            Thread.Sleep(Delayer.delay(900, 4000));
    
                            elemSearchButton.click();
                            //elemSearchButton.InvokeMember("click");
    
                            if (!_mreWaitForAction.WaitOne(10000)) //Here The Deadlock happens
                            {
                                //Now the click event and therefor the search will be executed
                                ConsoleWriter.writeToConsole(String.Format("Bookie {0}: Suche ist auf Timeout gelaufen", this.BookieName));
                                throw new BookieTimeoutExcpetion(String.Format("Bookie {0}: Suche ist auf Timeout gelaufen", this.BookieName));
                            }                                                        
    
                            _mreWaitForAction.Reset();
                            HtmlElement spanResult = doc.GetElementById("total_ocs_count");
                            while (spanResult == null)
                            {
                                Thread.Sleep(500);
                                spanResult = doc.GetElementById("total_ocs_count");
                            }
    
                            int total_ocs_count = 0;
                            if (!Int32.TryParse(spanResult.InnerHtml, out total_ocs_count))
                            {
                                ConsoleWriter.writeToConsole(String.Format("Bookie {0}: Tip {1} nicht gefunden", this.BookieName, tipp.BookieMatchName));
                                throw new BookieTippNotFoundExcpetion(String.Format("Bookie {0}: Tip {1} nicht gefunden", this.BookieName, tipp.BookieMatchName));
                            }
    
                            if (total_ocs_count <= 0)
                            {
                                ConsoleWriter.writeToConsole(String.Format("Bookie {0}: Tip {1} nicht gefunden", this.BookieName, tipp.BookieMatchName));
                                throw new BookieTippNotFoundExcpetion(String.Format("Bookie {0}: Tip {1} nicht gefunden", this.BookieName, tipp.BookieMatchName));
                            }
                            /*
                        else if (total_ocs_count > 1)
                        {
                            throw new BookieMoreThanOneFoundExcpetion(String.Format("Bookie {0}: Tipp {1} nicht eindeutig", this.BookieName, tipp.BookieMatchName));
                        }
                        */
                            ConsoleWriter.writeToConsole(String.Format("Bookie {0}: Tip {1} gefunden", this.BookieName, tipp.BookieMatchName));
                            _status = BookieStatusType.GETMARKET;
                        }
                    }
                }
                _mreWaitForAction.Set();
            }
        }
    
有人知道这里发生了什么吗?感谢您,lichtbringer。

2
在STA线程上调用WaitOne()是违反COM规则的。CLR允许这样做,因为如果你很幸运的话,就有可能不会出现死锁。但是这里没有这样的运气。大量无关的代码阻碍了一个合理的解决方案。建议将代码移动到DocumentCompleted事件处理程序中。 - Hans Passant
改变了STA位,但效果仍然相同。为什么我应该将它移动到EventHandler中?从流程的角度来看,它更适合放在原处。为什么在BHO中使用编码时可以工作? - lichtbringer
1个回答

3
通过执行_webBrowser.Invoke(new delegatePlaceBet(searchTipp), new object[] { tipp }),您使得searchTipp方法在主UI线程上同步执行。这是可以理解的,因为您不能从其他线程访问除了控件被创建的原始线程之外的任何线程的WebBrowser API。
然而,通过这样做,_mreWaitForAction.WaitOne(10000)调用也将在主UI线程上执行,从而阻塞消息循环(该消息循环由 Application.Run启动)。WebBrowser需要一个功能性的消息循环,它不断地推送Windows消息,否则DocumentCompleted不会被触发,您将陷入死锁。
我的理解是,您只是在这里创建另一个线程来组织自动化场景的工作流程。您真的不需要使用另一个线程。这里有一个示例可以异步地在主UI线程上使用async/await,以及这里有一个示例可以在控制台应用中使用WebBrowser
此外,作为解决方法,您可以使用这里WaitWithDoEvents,像这样:_mreWaitForAction.WaitWithDoEvents(10000)。它在等待句柄的同时仍在推送消息。您应该注意使用Application.DoEvents()创建嵌套消息循环的潜在影响
请注意,如果使用嵌套消息循环,则仍无需单独使用线程。它将为您提供主UI线程上的线性代码工作流程。示例:
private void buttonStart_Click(object sender, EventArgs e)
{
    using (var mre = new ManualResetEvent(false))
    {
        WebBrowserDocumentCompletedEventHandler handler = (s, args) => 
            mre.Set();
        this.webBrowser.DocumentCompleted += handler;
        try
        {
            this.webBrowser.Navigate("http://www.example.com");
            mre.WaitWithDoEvents(10000);
        }
        finally
        {
            this.webBrowser.DocumentCompleted -= handler;
        }
    }
    MessageBox.Show(this.webBrowser.Document.Body.OuterHtml);
}

虽然我之前提到过,你可以使用 async/await 更加自然地实现相同的功能,而且不需要嵌套消息循环,这是我认为更好的方式。


1
好的,谢谢,问题解决了。我还将线程转换为一个由定时器触发的async/await,因为这是一个每隔x小时重复执行的任务。 - lichtbringer
@lichtbringer,很高兴能帮到你。使用async/await,你可以有一个比定时器更好的选择:await Task.Delay(delay)。例如,你可以在循环中执行相同的任务以周期性地完成它。 - noseratio - open to work

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接