I pushed back the publication of this entry because I wasn’t so happy with the implementation I had.

It’s a new chapter in the “The Little Book of Semaphores”: not so-classical problems. The first problem there is called “search-insert-delete” and it deals with linked lists.

Three kinds of threads share access to a singly-linked list: searchers, inserters and deleters. Searchers merely examine the list; hence they can execute concurrently with each other. Inserters add new items to the end of the list; insertions must be mutually ex- clusive to preclude two inserters from inserting new items at about the same time. However, one insert can proceed in parallel with any number of searches. Finally, deleters remove items from any- where in the list. At most one deleter process can access the list at a time, and deletion must also be mutually exclusive with searches and insertions.

What the problem is calling for is categorical mutexes, just like the readers-writers problem from a while ago. Back then I argued that while it’s possible to implement categorical mutexes without using the standard library’s sync.RWMutex you also have to keep in mind that just because in Go you can use other primitives to build up synchronization constructions, that doesn’t mean that you shouldn’t reach for what is the simplest solution to a problem.

In this case, the simplest solution happens to be using the built-in categorical mutexes. Instead of going for container/list, I chose to implement my own (limited) linked list in order to favor the exposition over quantity of code. We can start by looking at the List type:

type List struct {
	searchMutex sync.RWMutex
	insertMutex sync.Mutex
	root        Element
	n           int32
}

There are two sync.RWMutex fields in this struct: one protects search operations and the other protects insertions. The mutexes are acquired like this:

searchMutex insertMutex
Search R
Insert R X
Delete RW X

Why?

The goal of searchMutex is to exclude deleters when either a search or an insert operation is active. Since searchers and inserters can operate concurrently, Insert does not need to acquire a read-write mutex to exclude searchers. On the other hand, the goal of insertMutex is to prevent multiple inserters from running concurrently, since this cannot be done using the searchMutex alone.

Both inserters and deleters try to acquire insertMutex first and then they try to acquire searchMutex. In this way only one inserter or one deleter can operate on the list at a time. When a deleter acquires insertMutex it proceeds to request searchMutex. In this way it prevents any more searchers from acquiring it and starve the deleters.

func (l *List) Delete(v interface{}) bool {
	l.insertMutex.Lock()
	l.searchMutex.Lock()

	// ...

	l.searchMutex.Unlock()
	l.insertMutex.Unlock()

	// ...
}

In practical terms inserters do not need to acquire the searchMutex because by the point where they try to acquire it they have already excluded the deleters, but I think the logic in the implementation is easier to understand if they do.

func (l *List) Insert(v interface{}) {
	l.insertMutex.Lock()
	l.searchMutex.RLock()

	// ...

	l.searchMutex.RUnlock()
	l.insertMutex.Unlock()
}

The full source is here.

The next problem is another categorical exclusion problem, with a twist.