0

System.InvalidOperationException: 'The calling thread cannot access this object because a different thread owns it.'

I have a WPF GUI with a button that when clicked does:

  1. starts a control animation (on the GUI), and
  2. starts a background process to obtain the local printer queues.

I do not want to block the main thread (GUI). However, the code I have gives the above error when I try to update the main thread with the results of the background process.

How do I have a background async process update the main thread without a context violation and not blocking the main thread?

open System.Printing

let GetPrinters = 
      let LocalPrintServer = new PrintServer()
      let printQueues = LocalPrintServer.GetPrintQueues [|EnumeratedPrintQueueTypes.Local; EnumeratedPrintQueueTypes.Connections|]
      let printerList =
          printQueues
              |> Seq.cast<PrintQueue>
              |> Seq.toList
      printerList


let GetPrintersAsync() = 
       async { 
               let! token = Async.StartChild(GetPrinters)
               let! p = token
               return p }

This is the update procedure I'm using:

let asyncUpper  =
        async {
               let! printerQues = GetPrintersAsync ()
               return printerQues
            }


// This is where the error is being displayed.
let getprinters (printers:PrintQueue list) =  
       printers 
          |> List.map (fun pq ->  {fullname = pq.FullName; comment = pq.Comment; defaultPrintTicket= Some pq.DefaultPrintTicket; 
                                                                         description= pq.Description; isInError=pq.IsInError; isOffline=pq.IsOffline; Id= Guid.NewGuid()  } ) 
                                                     
{ m with Printers = getprinters; IsRefreshing = false }

Edit #1: The above is a short version of the complete listing. Please see https://github.com/awaynemd/AsyncAndElmish for the complete source code using Elmish.wpf. Thank you.

Mark Pattison
  • 2,964
  • 1
  • 22
  • 42
Alan Wayne
  • 5,122
  • 10
  • 52
  • 95
  • 1
    Is this Elmish.WPF? If so, you should state that in the question. – Bent Tranberg May 26 '21 at 20:04
  • I have a hard time making sense of the code snippets. GetPrinters seems to be a value, not a function. Same with asyncUpper. These two will then be evaluated before the main function. Function getprinters is assigned to Printers rather than called with an argument. I could be wrong, but without source that can be compiled, I can't verify much. – Bent Tranberg May 26 '21 at 20:34
  • @BentTranberg It is code I took from my Elmish.wpf project, but it seems primarily an F# issue. Async is new to me. I'll put together code in GitHub if it will help. Stay tuned :) Thanks. – Alan Wayne May 26 '21 at 21:44
  • @BentTranberg The full source code for this (non-working) async process can be found at: [link](https://github.com/awaynemd/AsyncAndElmish). Thanks for any suggestions. (Async is still quit confusing to me.) – Alan Wayne May 27 '21 at 02:30
  • You should also tag the question with elmish-wpf. I think that will draw the attention of the experts in this field. – Bent Tranberg May 27 '21 at 06:31
  • Simultaneously cross-posted here https://github.com/elmish/Elmish.WPF/issues/394 – Tyson Williams Jun 26 '21 at 10:56

3 Answers3

1

I haven't looked at your code, but I think the basic answer to your question for WPF is the Dispatcher class. You can also use F#'s Async.SwitchToContext. See this SO question, for example.

Brian Berns
  • 15,499
  • 2
  • 30
  • 40
  • 2
    These were my first thoughts too, but examining the complete source on GitHub reveals that this Q is not at all about updating while working, but simply getting the final result back to the GUI without threading issues. – Bent Tranberg May 27 '21 at 14:15
1

I've had a chance to look at your source on GitHub now, and even run it.

The problem is that the print queues are retrieved in an async function, which means another thread than the GUI thread. Then the list of queues are returned to the GUI thread, and accessed from there. That's why you get the error message. When the queues are returned to the GUI thread, then they are mapped to the Printer type. This is too late. If you move that mapping into the async instead, then it won't be too late. The data returned to the GUI thread will be Printer list, which is perhaps fine. At least there's no crash. I am not one hundred percent sure if it's ok, because there's a field of type PrintTicket in there, and the question is whether it's safe to pull this across to another thread. If you need data from that object, maybe this too should be mapped to a record in the async before being returned to the GUI thread.

While trying to get it running without the error, this is the code I ended up with. I am not that knowledgeable about async either, and I'm not sure whether there's any point using async for this case. But maybe you're just trying out stuff.

| GetPrintersMsg ->
    let getPrinters () = async {
        use ps = new PrintServer()
        return
            ps.GetPrintQueues [| EnumeratedPrintQueueTypes.Local; EnumeratedPrintQueueTypes.Connections |]
            |> Seq.cast<PrintQueue>
            |> Seq.toList
            |> List.map (fun pq ->
                {
                    Id = Guid.NewGuid()
                    fullname = pq.FullName
                    comment = pq.Comment
                    defaultPrintTicket = Some pq.DefaultPrintTicket
                    description = pq.Description
                    isInError = pq.IsInError
                    isOffline = pq.IsOffline
                })
        }
    m, Cmd.OfAsync.either getPrinters () OnPrintersResult OnPrintersError
| OnPrintersResult printers ->
    { m with Printers = printers; IsRefreshing = false }, Cmd.none
Bent Tranberg
  • 3,445
  • 26
  • 35
  • 1
    Awards to you Sir! This works great! Thank you. I actually have two reasons for using async in this question: 1). Obtaining the printer ques on my laptop was taking much longer then I felt was reasonable, and 2) to start learning Async methods as F# and Elmish.wpf uses them. The way you restructured the solution and use of "use" is greatly helpful to my understanding. Much for me to learn. Thanks! – Alan Wayne May 27 '21 at 18:29
  • 1
    Hmmm.... Examination of the above code does show that it does not crash; However, studying the thread Id's shows that all computations are staying on the main thread ( printfn "%i" Thread.CurrentThread.ManagedThreadId ) -- which is why it is not crashing, but freezes up just before publishing the new printers. :(. So, how do I run the Async on a separate thread? Thanks. – Alan Wayne May 27 '21 at 21:47
0

@BentTranberg actually answered the hard part of this question. I post this as the completed answer since editing the question seems redundant. The below code is Bent's answer with a few modifications. The printers are now being read on a separate thread as seen with the printfn statements:

| GetPrintersMsg ->
    let getPrinters () = async {
        printfn "1: %i" Thread.CurrentThread.ManagedThreadId  

        let getprinters = async {
            printfn "11: %i" Thread.CurrentThread.ManagedThreadId  
            use ps = new PrintServer()
            return
                ps.GetPrintQueues [| EnumeratedPrintQueueTypes.Local; EnumeratedPrintQueueTypes.Connections |]
                |> Seq.cast<PrintQueue>
                |> Seq.toList
                |> List.map (fun pq ->
                    {
                        Id = Guid.NewGuid()
                        fullname = pq.FullName
                        comment = pq.Comment
                        defaultPrintTicket = Some pq.DefaultPrintTicket
                        description = pq.Description
                        isInError = pq.IsInError
                        isOffline = pq.IsOffline
                    }) }
        let! d = getprinters |> Async.StartChild
        return! d
        }
    m, Cmd.OfAsync.either getPrinters () OnPrintersResult OnPrintersError
Alan Wayne
  • 5,122
  • 10
  • 52
  • 95
  • 1
    Ok, so I was right about the cause in your original code, but wrong about my solution offloading the GUI thread? Now I understand why you used StartChild, I think. I've mostly used MailboxProcessor and various actor systems, and haven't really needed to know much about async. Nice to learn something. I've only recently started using Cmd.OfAsync in Elmish.WPF, and it simplifies things for me. Since what I do is send messages and wait for replies, I don't need to offload the GUI thread they way you did here. – Bent Tranberg May 28 '21 at 18:44
  • 1
    @BentTranberg Ha, so I did something right for a change...good to know :) Thanks. – Alan Wayne May 28 '21 at 18:48
  • I guess the way it works is that when `let!` or `do!` or `!match` is used inside async, then it uses other resources (IO or threads) than the GUI thread. I didn't have that in my answer, and I assume that means it works like a plain old coroutine. Which is also useful - and useful for me to understand. Hope I'm right. – Bent Tranberg May 28 '21 at 18:54
  • @BentTranberg The way I look at it is much simplier (and probably wrong). But in effect the similar C# code with Async, is that let! bounces program flow immediately back to the outer Async processes caller and continues immediately from there until the routine bound to the let! gives a response, at which time program flow continues to the line after let! (This is probably wrong, but conceptually easier for me to visualize). – Alan Wayne May 28 '21 at 19:01
  • Yes. And I now think I was wrong saying my use of `either` was still useful, because one might simply call a normal function directly instead. – Bent Tranberg May 28 '21 at 19:18
  • 1
    I wish some expert would chime in, but they seem to just sit there and laugh at us! – Bent Tranberg May 28 '21 at 19:19