F#跨线程UI异常在WinForms应用程序中

3
我在开发一个简单的F#应用程序时遇到了问题,它只需要读取请求的HTML页面的长度。
当您开发UI应用程序时,似乎VB.NET/C#语言也会出现类似的错误。

enter image description here

但我对F#比较新,不太清楚如何在F#中确切地解决这样的问题。
F#源代码:

http://pastebin.com/e6WM0Sjw

open System
open System.Net
open Microsoft.FSharp.Control.WebExtensions
open System.Windows.Forms

let form = new Form()
let text = new Label()
let button = new Button()

let urlList = [ "Microsoft.com", "http://www.microsoft.com/" 
                "MSDN", "http://msdn.microsoft.com/" 
                "Bing", "http://www.bing.com"
              ]

let fetchAsync(name, url:string) =
    async { 
        try 
            let uri = new System.Uri(url)
            let webClient = new WebClient()
            let! html = webClient.AsyncDownloadString(uri)
            text.Text <- String.Format("Read %d characters for %s", html.Length, name)
        with
            | ex -> printfn "%s" (ex.Message);
    }

let runAll() =
    urlList
    |> Seq.map fetchAsync
    |> Async.Parallel 
    |> Async.RunSynchronously
    |> ignore

form.Width  <- 400
form.Height <- 300
form.Visible <- true
form.Text <- "Test download tool"

text.Width <- 200
text.Height <- 50
text.Top <- 0
text.Left <- 0
form.Controls.Add(text)

button.Text <- "click me"
button.Top <- text.Height
button.Left <- 0
button.Click |> Event.add(fun sender -> runAll() |> ignore)
form.Controls.Add(button)

[<STAThread>]
do Application.Run(form)

最好的祝愿,

谢谢!


在VB/C#中,您需要“Invoke”创建控件的线程以执行任何工作。当您收到错误时,它应该指向代码中的特定控件。您需要使用递归目标或委托调用InvokeBeginInvoke来处理控件上的工作。很遗憾,我对F#语法一无所知。 - Chris Barlow
3个回答

6

在更新 text.Text 属性之前,您必须将线程上下文从 Async ThreadPool 切换到 UI 线程。有关 F# Async 特定说明,请参见MSDN 链接

在捕获 UI 上下文后,修改您的片段:

let uiContext = System.Threading.SynchronizationContext()

在您的let form = new Form()语句之后放置,并将fetchAsync定义更改为

let fetchAsync(name, url:string) =
    async { 
        try 
            let uri = new System.Uri(url)
            let webClient = new WebClient()
            let! html = webClient.AsyncDownloadString(uri)
            do! Async.SwitchToContext(uiContext)
            text.Text <- text.Text + String.Format("Read {0} characters for {1}\n", html.Length, name)
        with
            | ex -> printfn "%s" (ex.Message);
    }

它可以没有任何问题地运行。

更新:与一位同事讨论了调试器的怪异之处后,他强调了清晰地操作UI上下文的必要性,因此以下修改现在对运行方式不加区分:

open System
open System.Net
open Microsoft.FSharp.Control.WebExtensions
open System.Windows.Forms
open System.Threading

let form = new Form()
let text = new Label()
let button = new Button()

let urlList = [ "Microsoft.com", "http://www.microsoft.com/"
                "MSDN", "http://msdn.microsoft.com/"
                "Bing", "http://www.bing.com"
              ]

let fetchAsync(name, url:string, ctx) =
    async {
        try
            let uri = new System.Uri(url)
            let webClient = new WebClient()
            let! html = webClient.AsyncDownloadString(uri)
            do! Async.SwitchToContext ctx
            text.Text <- text.Text + sprintf "Read %d characters for %s\n" html.Length name
        with
            | ex -> printfn "%s" (ex.Message);
    }

let runAll() =
    let ctx = SynchronizationContext.Current
    text.Text <- String.Format("{0}\n", System.DateTime.Now)
    urlList
    |> Seq.map (fun(site, url) -> fetchAsync(site, url, ctx))
    |> Async.Parallel
    |> Async.Ignore
    |> Async.Start

form.Width  <- 400
form.Height <- 300
form.Visible <- true
form.Text <- "Test download tool"

text.Width <- 200
text.Height <- 100
text.Top <- 0
text.Left <- 0
form.Controls.Add(text)

button.Text <- "click me"
button.Top <- text.Height
button.Left <- 0
button.Click |> Event.add(fun sender -> runAll() |> ignore)
form.Controls.Add(button)

[<STAThread>]
do Application.Run(form) 

我已经按照你的建议尝试了,但是我仍然得到了相同的错误。 - user2402179
我尝试使用VS2013旗舰版,这是证明:http://s15.postimg.org/yuzy9d6wr/image.png(在发布模式下出现异常),这里是带有F#运行时版本的.net框架版本:http://s30.postimg.org/y6pfuua1t/image.png。 - user2402179
@GeloVolro:你上一次的快照给了我一些线索...你有没有尝试使用 CTRL-F5 在 VS 中运行你的应用程序?请尝试一下,因为即使在 Release 模式下,你也是带着调试器运行它的。 - Gene Belitski
是的,确实。使用Ctrl + F5应该可以解决问题。对我来说很奇怪...需要谷歌一下F5和Ctrl + F5之间的区别,不过还是非常感谢。 - user2402179
NP;一个很好的参考是开始调试与开始无调试 - Gene Belitski
显示剩余2条评论

4
作为使用“Invoke”操作(或上下文切换)的替代方案,您也可以使用“Async.StartImmediate”开始计算。
这个原语在当前线程(同步上下文)上启动异步工作流程,然后确保所有连续调用都在同一同步上下文中调用。因此,它本质上自动处理了“Invoke”操作。
为了实现这一点,您不需要更改“fetchAsync”中的任何内容。您只需要更改在“runAll”中启动计算的方式即可。
let runAll() =
    urlList
    |> Seq.map fetchAsync
    |> Async.Parallel 
    |> Async.Ignore
    |> Async.StartImmediate

与以前一样,它将所有内容并行组成,然后忽略结果以获取类型为Async<unit>的计算,然后在当前线程上启动它。这是F#异步编程中的一个不错功能 :-)

尝试按照您建议的方式操作,但出现了相同的错误。 - user2402179

2

我曾经遇到过同样的问题,Async.SwitchToContext 没有切换到主 GUI 线程,而是切换到了其他线程。

最后我发现问题在于我如何获取 uiContext 。使用以下方法解决了问题:

let uiContext = System.Threading.SynchronizationContext.Current

但它无法与以下内容一起使用:

let uiContext = System.Threading.SynchronizationContext()

你已经恢复了我的项目! - DenisKolodin

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