如何让来自线程通道的消息触发owlkettle中的更新?

问题描述 投票:0回答:1

在 owlkettle (https://github.com/can-lehmann/owlkettle) 中对“客户端-服务器”架构进行了一些研究后(参见here),我试图弄清楚如何具体编写一个对于 owlkettle 应用程序。

这个想法是有 2 个线程:

  1. 运行 owlkettle 构建的 GTK 应用程序的“客户端”线程
  2. 一个“服务器”线程,可以执行任何需要的繁重计算。

为了让他们沟通,您需要 2 个渠道:

  1. 用于来自客户端 => 服务器的消息
  2. 来自客户的消息<= server

现在的问题是,来自服务器的消息不会并且不能触发owlkettle中的更新它会卡在通道中,直到owlkettle本身决定触发更新(例如,当单击按钮时),在此期间它会读取消息。没有方便的方法或挂钩来表示“新服务器消息到达,用新数据更新 UI”。

这在下面的例子中很明显。单击按钮时,它会向服务器发送一条消息(通过通道 1),服务器发送响应(通过通道 2)。

您不会立即在用户界面中看到该更新。仅当您单击按钮时,因为按钮单击会自行触发常规 UI 更新。

        import owlkettle, owlkettle/[playground, adw]
        import std/[options, os]
         
        var counter: int = 1
         
        type ChannelHub = ref object
          serverChannel: Channel[string]
          clientChannel: Channel[string]
         
        proc sendToServer(hub: ChannelHub, msg: string): bool =
          echo "send client => server: ", msg
          hub.clientChannel.trySend(msg)
         
        proc sendToClient(hub: ChannelHub, msg: string): bool =
          echo "send client <= server: ", msg
          hub.serverChannel.trySend(msg)
         
        proc readClientMsg(hub: ChannelHub): Option[string] =
          let response: tuple[dataAvailable: bool, msg: string] = hub.clientChannel.tryRecv()
         
          return if response.dataAvailable:
              echo "read client => server: ", response.repr
              some(response.msg)
            else:
              none(string)
         
        proc readServerMsg(hub: ChannelHub): Option[string] =
          let response: tuple[dataAvailable: bool, msg: string] = hub.serverChannel.tryRecv()
         
          return if response.dataAvailable:
              echo "read client <= server: ", response.repr
              some(response.msg)
            else:
              none(string)
         
        proc setupServer(channels: ChannelHub): Thread[ChannelHub] =
          proc serverLoop(hub: ChannelHub) =
            while true:
              let msg = hub.readClientMsg()
              if msg.isSome():
                discard hub.sendToClient("Received Message " & $counter)
         
         
                counter.inc
              sleep(0) # Reduces stress on CPU when idle, increase when higher latency is acceptable for even better idle efficiency
         
          createThread(result, serverLoop, channels)
         
        viewable App:
          hub: ChannelHub
          backendMsg: string = ""
         
        method view(app: AppState): Widget =
          let msg: Option[string] = app.hub.readServerMsg()
          if msg.isSome():
            app.backendMsg = msg.get()
           
          result = gui:
            Window:
              defaultSize = (500, 150)
              title = "Client Server Example"
             
              Box:
                orient = OrientY
                margin = 12
                spacing = 6
               
                Button {.hAlign: AlignCenter, vAlign: AlignCenter.}:
                  Label(text = "Click me")
                 
                  proc clicked() =
                    discard app.hub.sendToServer("Frontend message!")
                   
         
                Label(text = "Message sent by Backend: ")
                Label(text = app.backendMsg)
         
        proc setupClient(channels: ChannelHub): Thread[ChannelHub] =
          proc startOwlkettle(hub: ChannelHub) =
            adw.brew(gui(App(hub = hub)))
         
          createThread(result, startOwlkettle, channels)
         
        proc main() =
          var serverToClientChannel: Channel[string]
          var clientToServerChannel: Channel[string]
          serverToClientChannel.open()
          clientToServerChannel.open()
         
          let hub = ChannelHub(serverChannel: serverToClientChannel, clientChannel: clientToServerChannel)
         
          let client = setupClient(hub)
          let server = setupServer(hub)
          joinThreads(server, client)
         
        main()

当服务器发送消息时,如何让它触发前端更新?

multithreading gtk client-server nim-lang
1个回答
0
投票

这个问题的解决方案是

g_idle_add_full
proc(或其他类似这样的proc)。它们的作用是向 GTK 注册一个函数,只要 GTK 主线程空闲,该函数就会被调用。

用它来检查通道中是否有从服务器接收消息的消息。如果有,则触发 owlkettle 中的更新。

您可以在启动时在 afterBuild

 小组件的 
App
 钩子中注册该进程。

看起来像这样:

type ListenerData = object
  hub: ChannelHub[string, string]
  app: Viewable

proc addServerListener(app: Viewable, hub: ChannelHub[string, string], priority: int = 200) =
  proc listener(cell: pointer): cbool {.cdecl.} =
    let data = cast[ptr ListenerData](cell)[]
    
    if data.hub.hasServerMsg():
      discard data.app.redraw()
  
    sleep(0) # Reduces stress on CPU when idle, increase when higher latency is acceptable for even better idle efficiency
    
    const KEEP_LISTENER_ACTIVE = true
    return KEEP_LISTENER_ACTIVE.cbool

  let data = allocSharedCell(ListenerData(hub: hub, app: app))
  discard g_idle_add_full(priority.cint, listener, data, nil)

viewable App:
  hub: ChannelHub[string, string]
  backendMsg: string = ""

  hooks:
    afterBuild:
      addServerListener(state, state.hub)

这是完整的示例(还有一些清理):


import owlkettle, owlkettle/[widgetutils, adw]
import owlkettle/bindings/gtk
import std/[options, os]

var counter: int = 0

## Communication
type ChannelHub[SMsg, CMsg] = ref object
  serverChannel: Channel[SMsg]
  clientChannel: Channel[CMsg]

proc new[SMsg, CMsg](t: typedesc[ChannelHub[SMsg, CMsg]]): ChannelHub[SMsg, CMsg] =
  result = ChannelHub[SMsg, CMsg]()
  result.serverChannel.open()
  result.clientChannel.open()
  
proc destroy[SMsg, CMsg](hub: ChannelHub[SMsg, CMsg]) =
  hub.serverChannel.close()
  hub.clientChannel.close()

proc sendToServer[SMsg, CMsg](hub: ChannelHub[SMsg, CMsg], msg: string): bool =
  echo "send client => server: ", msg
  hub.clientChannel.trySend(msg)
  
proc sendToClient[SMsg, CMsg](hub: ChannelHub[SMsg, CMsg], msg: string): bool =
  echo "send client <= server: ", msg
  hub.serverChannel.trySend(msg)

proc readClientMsg[SMsg, CMsg](hub: ChannelHub[SMsg, CMsg]): Option[string] =
  let response: tuple[dataAvailable: bool, msg: string] = hub.clientChannel.tryRecv()
  
  result = if response.dataAvailable:
      echo "read client => server: ", response.repr
      some(response.msg)
    else:
      none(string)

proc readServerMsg[SMsg, CMsg](hub: ChannelHub[SMsg, CMsg]): Option[string] =
  let response: tuple[dataAvailable: bool, msg: string] = hub.serverChannel.tryRecv()

  result = if response.dataAvailable:
      echo "read client <= server: ", response.repr
      some(response.msg)
    else:
      none(string)
  
proc hasServerMsg[SMsg, CMsg](hub: ChannelHub[SMsg, CMsg]): bool =
  hub.serverChannel.peek() > 0



## Server
proc setupServer(channels: ChannelHub[string, string]): Thread[ChannelHub[string, string]] =
  proc serverLoop(hub: ChannelHub[string, string]) =
    while true:
      let msg = hub.readClientMsg()
      if msg.isSome():
        discard hub.sendToClient("Received Message " & $counter)

        counter.inc
      sleep(0) # Reduces stress on CPU when idle, increase when higher latency is acceptable for even better idle efficiency
  
  createThread(result, serverLoop, channels)



## Client
type ListenerData = object
  hub: ChannelHub[string, string]
  app: Viewable

proc addServerListener(app: Viewable, hub: ChannelHub[string, string], priority: int = 200) =
  proc listener(cell: pointer): cbool {.cdecl.} =
    let data = cast[ptr ListenerData](cell)[]
    
    if data.hub.hasServerMsg():
      discard data.app.redraw()
  
    sleep(0) # Reduces stress on CPU when idle, increase when higher latency is acceptable for even better idle efficiency
    
    const KEEP_LISTENER_ACTIVE = true
    return KEEP_LISTENER_ACTIVE.cbool

  let data = allocSharedCell(ListenerData(hub: hub, app: app))
  discard g_idle_add_full(priority.cint, listener, data, nil)

viewable App:
  hub: ChannelHub[string, string]
  backendMsg: string = ""

  hooks:
    afterBuild:
      addServerListener(state, state.hub)

method view(app: AppState): Widget =
  let msg: Option[string] = app.hub.readServerMsg()
  if msg.isSome():
    app.backendMsg = msg.get()
    
  result = gui:
    Window:
      defaultSize = (500, 150)
      title = "Client Server Example"
      
      Box:
        orient = OrientY
        margin = 12
        spacing = 6
        
        Button {.hAlign: AlignCenter, vAlign: AlignCenter.}:
          Label(text = "Click me")
          
          proc clicked() =
            discard app.hub.sendToServer("Frontend message!")
            

        Label(text = "Message sent by Backend: ")
        Label(text = app.backendMsg)

proc setupClient(channels: ChannelHub): Thread[ChannelHub] =
  proc clientLoop(hub: ChannelHub) =
    adw.brew(gui(App(hub = hub)))
  
  createThread(result, clientLoop, channels)



## Main
proc main() =
  let hub = new(ChannelHub[string, string])
  let client = setupClient(hub)
  let server = setupServer(hub)
  
  joinThreads(server, client)
  
  hub.destroy()
main()
© www.soinside.com 2019 - 2024. All rights reserved.