Skip to content

crmathieu/gosem

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

25 Commits
 
 
 
 
 
 
 
 

Repository files navigation

gosem

Semaphores are tools that help manage concurrent accesses to a common resource (the resource is usually one or more data structures with a set of indexes or pointers used to manipulate the data). In order to achieve this, a semaphore has a counter that indicates the level of availability the resource has at a given time.

In the context of Golang, a semaphore will be represented as a data structure using a channel as a mean to provide goroutines synchronization. The channel has a dimension (its capacity) that corresponds to the dimension of the sharable resource, and an initial count corresponding to its initial availability.

Using gosem in your project

import (
  sem "github.com/crmathieu/gosem/pkg/semaphore"
)

Imagine we want to share a buffer of 512 integers between 1 producer and 1 consumer. The producer wants to write to the buffer and the consumer wants to read from it.

var buffer [512]int
var tail, head int = 0, 0

In order to protect this buffer and synchronize read and write operations, we are going to need 2 semaphores: one for reads and one for writes. The read semaphore is used to find out if there is anything to read from the buffer. The write semaphore is used to find out if there is any space available in the buffer so that we can write into it.

readsem =  sem.Createsem("readsem",  512, 0)
writesem = sem.Createsem("writesem", 512, 512)

Note that both readsem and writesem have the same dimension: 512, but readsem has an initial value of 0 (because initially there is nothing to read) and writesem has an initial value of 512 (because initially the whole buffer is available).

you may also use the CreateReadSemaphore or CreateWriteSemaphore that will abstract the initial value given to the semaphore:

readsem =  sem.CreateReadSemaphore("readsem",   512)
writesem = sem.CreateWriteSemaphore("writesem", 512)

The code of the producer looks like this:

func producer() {
  i := 0
  for {
    writesem.Wait()
    buffer[head] = i
    i = (i + 1) % 4096
    head = (head+1) % 512
    readsem.Signal()
  }
}

In its loop, the producer first makes sure there is available space in the buffer by calling writesem.Wait(). This call will return immediately if space is available but will block if the buffer is full. In the latter case, the call will return only after the consumer goroutine reads an entry from the buffer and performs a writesem.Signal() call to indicate that one entry is now available.

Similarly, once a value was written in the buffer, the producer calls readsem.Signal() to indicate that one entry is available for consumption.

The code of the consumer looks like that:

func consumer() {
  for {
    readsem.Wait()
    item = buffer[tail]
    tail = (tail+1) % 512
    writesem.Signal()
    fmt.Println(item)
  }
}

In its loop, the consumer first makes sure there is something to read from the buffer by calling readsem.Wait(). This call will return immediately if data is available but will block if the buffer is empty. In the latter case, the call will return only after the producer goroutine writes an entry to the buffer and performs a readsem.Signal() call to indicate that one entry is ready to be consumed.

Similarly, once a value has been read from the buffer, the consumer calls writesem.Signal() to indicate that space is available for production.

Multiple consumers and producers

If we want to synchronize several consumers and producers accessing the same buffer, the code for both consumers and producers needs to handle concurrent access to the head and tail buffer indexes, because their value can be updated by multiple goroutines simultaneously (which was not the case in the previous example).

In order to do that, goroutines will need to have an exclusive access to these indexes when they update them. This is accomplished with the use of mutex semaphores. A mutex semaphore is like a normal semaphore with a capacity and an initial count of 1:

mutex = sem.Createmutex("mymutex")

We are going to need a mutex to protect the head index used by multiple producers and another mutex to protect the tail index used by multiple consumers:

headmutex = sem.Createmutex("head-mutex")
tailmutex = sem.Createmutex("tail-mutex")

The producer code becomes:

func producer() {
  i := 0
  for {
    writesem.Wait()
    headmutex.Enter()
    buffer[head] = i
    i = (i + 1) % 4096    
    head = (head+1) % 512
    headmutex.Leave()
    readsem.Signal()
  }
}

and the consumer code becomes:

func consumer() {
  for {
    readsem.Wait()
    tailmutex.Enter()
    item = buffer[tail]
    tail = (tail+1) % 512
    tailmutex.Leave()
    writesem.Signal()
    fmt.Println(item)
  }
}

Semaphore API

First, import the gosem package:

import (
  sem "github.com/crmathieu/gosem/pkg/semaphore"
)

Variable declaration

To declare a semaphore or a mutex:

var mysem *semaphore.Sem

-or-

var mymutex *semaphore.Mutex

Createsem: creates a counter semaphore

func Createsem(name string, capacity int, initialcount int) *Sem

To create a semaphore with a capacity of 64, and an initial count of 0:

mysem := sem.Createsem("mySemaphore", 64, 0)

-or- to create a semaphore with a capacity of 64, and an initial count of 64:

mysem := sem.Createsem("mySemaphore", 64, 64)

Createmutex: creates a mutex

func Createmutex(name string) *Mutex

mymutex := sem.Createmutex("myMutex")

Following a semaphore creation, there are a certain number of methods available to manipulate semaphores:

Reset

func (s *Sem) Reset()

mysem.Reset()

-or- for a mutex

func (m *Mutex) Reset()

mymutex.Reset()

This will flush the semaphore internal channel and resets its counter to its original value.

Signal -or- V (-or- Leave)

func (s *Sem) Signal()

mysem.Signal()

-or-

func (s *Sem) V()

mysem.V()

-or- for a mutex

func (m *Mutex) Leave()

mymutex.Leave()

Signal and V accomplish the same thing which is to increase by 1 the level of availability of the resource. Leave is identical but reserved for mutex.

Wait -or- P (-or- Enter)

func (s *Sem) Wait()

mysem.Wait()

-or-

func (s *Sem) P()

mysem.P()

-or- for a mutex

func (m *Mutex) Enter()

mymutex.Enter()

Wait and P accomplish the same thing which is to decrease by 1 the level of availability of the resource. Enter is identical but reserved for mutex. When the semaphore counter reaches 0, the resource is no longer available, until a Signal (-or- a V) call is made by another goroutine.

Notes:

  • The P / V notation comes from Edsger Dijkstra, who introduced the concept of semaphores in 1963. The letters are from the Dutch words Probeer (try) and Verhoog (increment).

  • The terms Enter and Leave for a mutex refer to Entering and Leaving critical sections in your code. A critical section is a region in your code that can be executed only by one goroutine at a time. Typically, you will need to define a critical section everytime you need to access a resource that can potentially be modified by multiple goroutines. Once in the critical section, a goroutine is guaranteed to have exclusive access to the shared resource.

About

A semaphore package with examples for Golang

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages