The fact was that the mutex acts more like a flag, and encapsulating code in a lock()/unlock() bound to a particular mutex variable seemed to be a way of isolating code from running at the same time (struct or no struct.)
Is that a more proper way to think about mutexes in Go? Let's try a simple application to find out.
// More testing on Mutex use package main import ( "fmt" "sync" "time" ) func main() { // The mutex to lock var MyLock sync.Mutex // Save the current time StartTime := time.Now() // Create a channel to organize text messages to the user chanText := make(chan string) // Simple counter for formatting purposes var boolNewline bool = false go func() { // For the next 5 seconds, output a period every tick chanText <- "For ~5 seconds, Process 1 is printing a '.' every 100 milliseconds!" // Set a simple flag var TriggerMessage bool = true // A ticker ticker := time.NewTicker(time.Millisecond * 100) // Output something with each tick for range ticker.C { // Check the time since the program started if time.Since(StartTime).Seconds() <= 5 { // It was <= 5 seconds. Print the . fmt.Print(".") } if time.Since(StartTime).Seconds() >= 5 && TriggerMessage { // A simple notification chanText <- "For the next ~5 seconds, the lock is going to be set for Process 1!" TriggerMessage = false } if time.Since(StartTime).Seconds() > 5 && time.Since(StartTime).Seconds() <= 10 { // Now it's time for the locked portion of the demo MyLock.Lock() // Ticker within a ticker ticker2 := time.NewTicker(time.Millisecond * 100) // What follows is a label, OutOfTicker, which will be used for a nested break statement // to escape the embedded ticker/if statement OutOfTicker: for range ticker2.C { // Run until the final runtime total (of 10 seconds) if time.Since(StartTime).Seconds() <= 10 { fmt.Print(".") } else { break OutOfTicker } } // Unlock and notify the user MyLock.Unlock() chanText <- "Process 1 unlocked mutex!" } } }() go func() { chanText <- "For ~5 seconds, Process 2 is printing a '*' every 100 milliseconds!" var TriggerMessage bool = true ticker := time.NewTicker(time.Millisecond * 100) for range ticker.C { if time.Since(StartTime).Seconds() <= 5 { fmt.Print("*") } if time.Since(StartTime).Seconds() >= 5 && TriggerMessage { chanText <- "For the next ~5 seconds, the lock is going to be set for Process 2!" TriggerMessage = false } if time.Since(StartTime).Seconds() > 5 && time.Since(StartTime).Seconds() <= 10 { MyLock.Lock() ticker2 := time.NewTicker(time.Millisecond * 100) OutOfTicker: for range ticker2.C { if time.Since(StartTime).Seconds() <= 10 { fmt.Print("*") } else { break OutOfTicker } } MyLock.Unlock() chanText <- "Process 2 unlocked mutex!" } } }() // And now we kill main() after a set period of time, which is 2 seconds after everything has // hopefully finished without issue timer := time.NewTimer(time.Second * 12) for { select { case <-timer.C: // Time to stop // Send a formatting newline and exit main() fmt.Print("\n") return case strMessage := <-chanText: // Message from the goroutines // boolNewLine is checked so every other line is fed a newline, just to make the text // to the user more readable/neat, then flips the state of boolNewLine for the next iteration if !boolNewline { fmt.Print("\n") fmt.Println(strMessage) boolNewline = true } else { fmt.Println(strMessage) boolNewline = false } } } }
I think the code is pretty straightforward; the two goroutines are virtually identical, and the first one is heavily commented. Main() starts off with a declaration of a mutex called MyLock, then grabs the current time (as a time.Time type) stored as StartTime, since the application is going to run for X number of seconds from the start of execution.
For the sake of simplifying formatting, I opted to create a channel for the two goroutines to send information back to main() and let main() be responsible for serializing and formatting the text. Because goroutines output is indeterministic, controlled by the scheduler, letting them dump text to fmt.Print() can be less predictable and a little less readable. Channels kind of filter the information; text is sent into the channel, they will be kind of serialized until the reader on the other side (in main()) pulls the message off the channel and processes it. One thing to note - you can affect the indeterministic nature of goroutine execution by not reading from the channel in main(); since it's a blocking operation (unless you use a channel with buffers), if you were to do something like time.Sleep() in main() where it should read more from the channel, you'll stop the goroutines from working when they try to send something down the channel again.
The last thing I set up was a small boolean variable to again help with some formatting of text.
After the spawning of the goroutines, main() declares a timer that will run for 12 seconds because the demo from the goroutines should finish in 10. After that is the loop that checks for either the timer to tick ("We're done!") or a message from the channel. The message flips the boolean between true and false because if I didn't, some of the text would get tacked on the end of the line of "." or "*" from the goroutines and it made it look bad. Because there's two goroutines, it was fairly simple to use the boolean to say "add a newline before printing this" and flipping the state so the next message was just, "Print the message."
The goroutines are fairly simple too; most of their logic is a matter of evaluating time so they knew when to lock and unlock. They notify the channel that they're going to run for 5 seconds and create a boolean flag for their change in message later, then create a periodic 100ms ticker.
Then it's time to listen to the ticker. If the time is less than 5 seconds since launch, it just sends a character to the console.
If the time is greater than 5 seconds and my boolean flag is set, it sends the message that for the next 5 seconds it's going to set the lock, then turns off that message from reappearing using the boolean flag since otherwise it may keep printing it every 100 milliseconds, or once the later logic is through it might occasionally spill over again.
(At this point I could probably have turned off the ticker within the next inner loop so that wouldn't be re-evaluated or set some conditional that would have invalidated it in later runs. I used the flag because it was relatively simple while I iterated through my tests for the blog post. Point is, there's more than one way to have achieved this, probably every way has some merit, but for this purpose it worked.)
The next bit runs if the time is between 5 and 10 seconds. The goroutines set the lock and create a new timer, then I use a label (OutOfTicker) above an "inner evaluation" loop for the new timer. That small inner lop runs until 10 seconds since the launch of the program, then breaks out of the inner loops to the scope of the label. At that point they'll unlock the mutex and send a message down the channel that the mutex is unlocked.
What does the output look like?
go build && ./mutex_testering2
For ~5 seconds, Process 1 is printing a '.' every 100 milliseconds!
For ~5 seconds, Process 2 is printing a '*' every 100 milliseconds!
.*.*.*.*.*.**..*.**.*..*.*.**..**.*..*.*.*.*.*.*.**.*.*.*..**..**..**..**.*..**..**..*.*.*.*.*.*.*
For the next ~5 seconds, the lock is going to be set for Process 1!
For the next ~5 seconds, the lock is going to be set for Process 2!
.................................................
Process 1 unlocked mutex!
Process 2 unlocked mutex!
go build && ./mutex_testering2
For ~5 seconds, Process 1 is printing a '.' every 100 milliseconds!
For ~5 seconds, Process 2 is printing a '*' every 100 milliseconds!
.*.*.*.**..*.*.*.*.**..**..*.*.**.*..**.*.*..**..**.*.*..*.**..**.*..**..*.*.*.*.*.*.**.*..**.*..*
For the next ~5 seconds, the lock is going to be set for Process 2!
For the next ~5 seconds, the lock is going to be set for Process 1!
*************************************************
Process 2 unlocked mutex!
Process 1 unlocked mutex!
I included two runs to show that the scheduling was random enough that sometimes the first goroutine has the lock and runs, and sometimes the second one gets to run with the lock. Because they're time restricted, they stop trying to run around the same time, so there's no blip from the opposite goroutine when the mutexes are released. Although it might be possible? Maybe?
Conclusion
Thinking of mutexes as a way to lock a struct worked for my particular purpose when trying to imagine the workflow in my particular application at the time, but probably better to think of mutexes as a way of having your code check a variable to see if it's okay to run anything contained between the Lock() and Unlock(). The goroutines here were independently running, and once the mutex was locked only one of them was allowed to work; kind of like, "Whoever has the speaking stick may address the group" around a campfire. Mutexes are the speaking stick of Go applications!
No comments:
Post a Comment