Demystifying Concurrency: Simple Implementation of the Binding Properties Pattern in Go

Photo by Everson Mayer: https://www.pexels.com/photo/black-dj-controller-1481312/

Introduction

Especially in multi-threaded applications it can be necessary to synchronize properties between objects, or at least be notified of changes on certain properties. This is where the Binding Properties comes in, which is basically a form of the Observer pattern.

In this pattern, observers can subscribe to a special ‘event’-handler on an object. When a property changes, all the subscribers, if any, will be notified.

The flow is as follows:

  1. An object with bindable properties is created
  2. One or more observers subscribe to this object
  3. If a property changes in this object, the subscribers get notified and handle the change accordingly.

Implementation in Go

In this example we will build a simple Person struct. In our world, a person just has a name and an age. This struct has a number, or as the case may be no, observers which will be notified any time a property changes:

package main

import (
	"fmt"
	"strconv"
	"sync"
)

type Observer func(propertyName, newValue string)

type Person struct {
	name      string
	age       int
	observers []Observer
	mu        sync.Mutex
}

func NewPerson(name string, age int) *Person {
	return &Person{
		name: name,
		age:  age,
	}
}

func (p *Person) GetName() string {
	p.mu.Lock()
	defer p.mu.Unlock()
	return p.name
}

func (p *Person) SetName(name string) {
	p.mu.Lock()
	p.name = name
	p.mu.Unlock()
	p.notifyObservers("name", name)
}

func (p *Person) GetAge() int {
	p.mu.Lock()
	defer p.mu.Unlock()
	return p.age
}

func (p *Person) SetAge(age int) {
	p.mu.Lock()
	p.age = age
	p.mu.Unlock()
	p.notifyObservers("age", strconv.Itoa(age))
}

func (p *Person) Subscribe(observer Observer) {
	p.mu.Lock()
	defer p.mu.Unlock()
	p.observers = append(p.observers, observer)
}

func (p *Person) notifyObservers(property, value string) {
	p.mu.Lock()
	defer p.mu.Unlock()
	for _, observer := range p.observers {
		observer(property, value)
	}
}

A few notes:

  1. We use sync.Mutex, a standard library function in Go, to protect shared data from concurrent access.
  2. The observers slice holds the list of observers, and we use the mutex to synchronize access to this slice.
  3. In the two Get lock the mutex to guarantee exclusive access. We then use the defer keyword to make sure the mutex is unlocked, even in the case of a panic.
  4. In the two Set methods we lock the mutex as well, set the appropiate value, and then unlock the mutex. We do this before calling the notifyObservers() method, informing the observers about the change. We do this to avoid a possible deadlock, by not holding a lock while we call the observers.
  5. In the Subscribe() method we add an observer in a thread-safe manner, by locking and unlocking our mutex.
  6. For the notifyObservers() method, which is called from the two Set methods, we lock the mutex to ensure the notification is thread-safe. Again, we use the defer method to unlock the mutex, even if the method panics.

Testing time

Now we can test our setup:

func main() {
	person := NewPerson("Test", 55)


	person.Subscribe(func(property, value string) {
		fmt.Printf("main thread: %s changed to %s\n", property, value)
	})


	go func() {
		person.SetName("Jane")
		person.SetAge(21)

		person.Subscribe(func(property, value string) {
			fmt.Printf("goroutine: %s changed to %s\n", property, value)
		})
	}()


	var wg sync.WaitGroup
	wg.Add(1)
	go func() {
		defer wg.Done()
		person.SetName("John")
	}()
	person.SetAge(32)
	wg.Wait()
}

Here you can see we simulate concurrent updates to the Person struct, by using a goroutine. We use a sync.WaitGroup to wait for the goroutine to finish execution before the program terminates.

Conclusion

Implementing this pattern in Go is quite straightforward, using the sync package. The code is also quite clear. One possible enhancement could be to set a different subscriber for each property, so that our pattern becomes a bit more fine-grained. However, that is the subject for a next post.

Leave a Reply

Your email address will not be published. Required fields are marked *