[GH-ISSUE #784] Deadlock when waiting for draw inside goroutines #576

Closed
opened 2026-03-04 01:06:10 +03:00 by kerem · 4 comments
Owner

Originally created by @vaaleyard on GitHub (Dec 20, 2022).
Original GitHub issue: https://github.com/rivo/tview/issues/784

So, I have a scenario where I need to wait for something to happen to be able to continue the code. Take the code below as an example:

package main

import (
	"fmt"
	"time"

	"github.com/gdamore/tcell/v2"
	"github.com/rivo/tview"
)

func update(input1 string) bool {
    time.Sleep(3 * time.Second)

    tviewApp.QueueUpdateDraw(func ()  {
        fmt.Println("draw")
    })

    counter = 3

    if input1 == "true" {
        return true
    } else {
        return false
    }
}

var input string
var counter int
var tviewApp *tview.Application

func main() {
        tviewApp = tview.NewApplication()

        counter = 0
	form := tview.NewForm()
        form.AddInputField("Input: ", "", 15, nil, nil).
		SetFieldTextColor(tcell.ColorBlack.TrueColor()).
		AddButton("Save", func() {
                        input = form.GetFormItem(0).(*tview.InputField).GetText()
                }).
		AddButton("Process", func() {
			var done bool = false
			go func() {
				for !done {
					done = update(input)
				}
			}()
			fmt.Println(counter)
		})

	if err := tviewApp.SetRoot(form, true).SetFocus(form).Run(); err != nil {
		panic(err)
	}
}

With "true" as input, I'd like to get "3" when printing the counter right after executing the goroutine, but it prints "0", because the goroutine didn't finished. In order to make this, the first thing I thought is to create a waiting group to wait for this goroutine. So, I did this:

        AddButton("Process", func() {
	    var done bool = false
            wg := new(sync.WaitGroup)
            wg.Add(1)
	    go func() {
	        for !done {
		    done = update(input)
		}
                wg.Done()
	    }()
            wg.Wait()
	    fmt.Println(counter)
	})

But this ends in a deadlock, and I don't know why. How can I achieve this?

Disclaimer: I've created this code purely to simulate my scenario, I know it's weird.

Originally created by @vaaleyard on GitHub (Dec 20, 2022). Original GitHub issue: https://github.com/rivo/tview/issues/784 So, I have a scenario where I need to wait for something to happen to be able to continue the code. Take the code below as an example: ``` package main import ( "fmt" "time" "github.com/gdamore/tcell/v2" "github.com/rivo/tview" ) func update(input1 string) bool { time.Sleep(3 * time.Second) tviewApp.QueueUpdateDraw(func () { fmt.Println("draw") }) counter = 3 if input1 == "true" { return true } else { return false } } var input string var counter int var tviewApp *tview.Application func main() { tviewApp = tview.NewApplication() counter = 0 form := tview.NewForm() form.AddInputField("Input: ", "", 15, nil, nil). SetFieldTextColor(tcell.ColorBlack.TrueColor()). AddButton("Save", func() { input = form.GetFormItem(0).(*tview.InputField).GetText() }). AddButton("Process", func() { var done bool = false go func() { for !done { done = update(input) } }() fmt.Println(counter) }) if err := tviewApp.SetRoot(form, true).SetFocus(form).Run(); err != nil { panic(err) } } ``` With "true" as input, I'd like to get "3" when printing the counter right after executing the goroutine, but it prints "0", because the goroutine didn't finished. In order to make this, the first thing I thought is to create a waiting group to wait for this goroutine. So, I did this: ``` AddButton("Process", func() { var done bool = false wg := new(sync.WaitGroup) wg.Add(1) go func() { for !done { done = update(input) } wg.Done() }() wg.Wait() fmt.Println(counter) }) ``` But this ends in a deadlock, and I don't know why. How can I achieve this? _**Disclaimer:**_ _I've created this code purely to simulate my scenario, I know it's weird._
kerem closed this issue 2026-03-04 01:06:10 +03:00
Author
Owner

@rivo commented on GitHub (Dec 21, 2022):

The deadlock happens because your "Process" callback function blocks the workflow by not returning right away. When you call QueueUpdateDraw(), somewhere it still waits for this callback to finally finish so everything can keep going. From the Wiki:

Any event handlers you install [...] are invoked from the main goroutine.

It's hard to tell from this example what you really want to achieve. But if you want to kick off some concurrent process when the user clicks on the "Process" button, you might want to wrap it all in a goroutine:

AddButton("Process", func() {
	go func() {
		var done bool
		for !done {
			done = update(input)
		}
		fmt.Println(counter)
	}()
})

(Also, you should synchronize access to the input variable, to avoid race conditions. Or use channels instead.)

<!-- gh-comment-id:1361606396 --> @rivo commented on GitHub (Dec 21, 2022): The deadlock happens because your "Process" callback function blocks the workflow by not returning right away. When you call `QueueUpdateDraw()`, somewhere it still waits for this callback to finally finish so everything can keep going. From the [Wiki](https://github.com/rivo/tview/wiki/Concurrency): > Any event handlers you install [...] are invoked from the main goroutine. It's hard to tell from this example what you really want to achieve. But if you want to kick off some concurrent process when the user clicks on the "Process" button, you might want to wrap it all in a goroutine: ```go AddButton("Process", func() { go func() { var done bool for !done { done = update(input) } fmt.Println(counter) }() }) ``` (Also, you should synchronize access to the `input` variable, to avoid race conditions. Or use channels instead.)
Author
Owner

@vaaleyard commented on GitHub (Dec 26, 2022):

Hi @rivo , thanks for your response. With your idea, I've wrapped all of it in a goroutine and it worked.

What I wanted to do is to draw an animated array and display a widget after it's finished. The code is here.

Thank you for your time.

<!-- gh-comment-id:1364818865 --> @vaaleyard commented on GitHub (Dec 26, 2022): Hi @rivo , thanks for your response. With your idea, I've wrapped all of it in a goroutine and it worked. What I wanted to do is to draw an animated array and display a widget after it's finished. The code is [here](https://github.com/vaaleyard/turing-machine/blob/main/ui/ui.go#L74). Thank you for your time.
Author
Owner

@spacez320 commented on GitHub (Jan 7, 2024):

For anyone stumbling on this, the wrapping of callback functions in a goroutine also seems to work for keystroke handler functions in tview.Table. For example (trying to simplify from the actual code):

var (
    interrupt = make(chan bool)
)
 
func main() {
    app = tview.NewApplication()
    resultsView = tview.NewTable()
    resultsView.SetDoneFunc(keyboardHandler)
    // Continue display set-up ...
    
    // Start updating the table regularly.
    go func() {
        for {
            select {
            case <- interrupt:
                // User wants to stop updating.
                return
            default:
                // Dynamically update the table ...
                app.QueueUpdateDraw(func() {
                    row := resultsView.InsertRow()
                    row.SetCellSimple(...)
                })
            }
        }
    }()
}

func keyboardHandler(key tcell.Key) {
    switch key {
    case tcell.KeyTab:
         // NO WORKY. APPLICATION DEADLOCKS.
         interrupt <- true
        
         // WORKS.
         go func() {
             interrupt <- true
         )()
     }
 }

Feel free to yell at me if anything above deserves it, but the main point is that the callback function on the main thread should return right away and a goroutine works well for that.

Thanks for the help and hard work @rivo .

Edit: Added the call to QueueUpdateDraw which is necessary when updating primitives in goroutines.

<!-- gh-comment-id:1880056909 --> @spacez320 commented on GitHub (Jan 7, 2024): For anyone stumbling on this, the wrapping of callback functions in a goroutine also seems to work for keystroke handler functions in tview.Table. For example (trying to simplify from the actual code): ```go var ( interrupt = make(chan bool) ) func main() { app = tview.NewApplication() resultsView = tview.NewTable() resultsView.SetDoneFunc(keyboardHandler) // Continue display set-up ... // Start updating the table regularly. go func() { for { select { case <- interrupt: // User wants to stop updating. return default: // Dynamically update the table ... app.QueueUpdateDraw(func() { row := resultsView.InsertRow() row.SetCellSimple(...) }) } } }() } func keyboardHandler(key tcell.Key) { switch key { case tcell.KeyTab: // NO WORKY. APPLICATION DEADLOCKS. interrupt <- true // WORKS. go func() { interrupt <- true )() } } ``` Feel free to yell at me if anything above deserves it, but the main point is that the callback function on the main thread should return right away and a goroutine works well for that. Thanks for the help and hard work @rivo . **Edit:** Added the call to `QueueUpdateDraw` which is necessary when updating primitives in goroutines.
Author
Owner

@spacez320 commented on GitHub (Jan 11, 2024):

@digitallyserviced Thanks for the suggestion. Unfortunately wrapping interrupt <- true in a select to make it non-blocking doesn't really work very well because it looks like sometimes the interrupt doesn't actually successfully send, so the updating goroutine doesn't consistently stop updating as intended.

<!-- gh-comment-id:1887323134 --> @spacez320 commented on GitHub (Jan 11, 2024): @digitallyserviced Thanks for the suggestion. Unfortunately wrapping `interrupt <- true` in a `select` to make it non-blocking doesn't really work very well because it looks like sometimes the interrupt doesn't actually successfully send, so the updating goroutine doesn't consistently stop updating as intended.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
starred/tview#576
No description provided.