Golang program to implement a concurrent hash trie


Concurrency is crucial to modern programming for enabling efficient use of resources in multi-core systems. Hash tries are associative data structures which provide a scalable and thread-safe handling of large amounts of data concurrently. In this article, we are going to learn to implement a concurrent hash trie in go, implementation here means we are going to demonstrate the operations like insertion, updation and deletion on a hash trie data structure.

Explanation

A concurrent hash trie as a data structure combines the benefits of hash maps and tries to allow multiple threads simultaneous access and modification rights associated with the concerning data. It can efficiently handle data distribution and also provide a balance between storage efficiency and concurrency control.

Here is a representation of a simple concurrent hash trie:

Root
├─ Hash Bucket 1
│  ├─ Key: "apple", Value: 5
│  └─ Key: "apricot", Value: 7
├─ Hash Bucket 2
│  ├─ Key: "banana", Value: 10
└─ Hash Bucket 3
   ├─ Key: "cherry", Value: 3
   ├─ Key: "coconut", Value: 8
   └─ Key: "grape", Value: 6

The representation above shows the tree structure with 1 root and 3 hash buckets. There is a key value pair in each hash bucket. The key represents a unique name and has a value associated with it, For example: key apple is associated with value 5. This structure is used to implement a hash table making the retrieval of data efficient with the help of keys.

Syntax

func displayTrie(node *Node, indent string)

The syntax represents a method called displayTrie which recursively prints the values and keys of nodes in a trie-like structure, aiding in visualizing the structure of the data.

Algorithm

  • Start by initializing the root of the hash trie.

  • For insertion, hash the key to find the appropriate node and then lock the node's mutex.

  • For lookup, hash the key to locate the node, read its value, and then release the mutex.

  • To update a key-value pair, hash the key, lock the node, and then modify the value.

  • For deletion, hash the key, lock the node, and then remove it from the trie.

Example 1

In this example,we will implement a concurrent hash trie in Go, we build a hash trie data structure with concurrency control using the `Node` struct to represent a trie node with an integer value and child nodes. The `sync.RWMutex` ensures concurrent-safe access, while the `displayTrie` functions print the trie's structure. In `main`, a root node is created and then using locks, nodes are inserted, the values looked up, updated, and deleted.

package main
import (
	"fmt"
	"sync"
)
type Node struct {
	value	int
	children map[string]*Node
	mutex	sync.RWMutex
}
func displayTrie(root *Node) {
	fmt.Println("Hash Trie:")
    displayTrieHelper(root, "")
}
func displayTrieHelper(node *Node, indent string) {
    fmt.Printf("%sValue: %d\n", indent, node.value)
    for key, child := range node.children {
    	fmt.Printf("%sKey: %s\n", indent+"  ", key)
    	displayTrieHelper(child, indent+"  ")
    }
}
func main() {
	root := &Node{children: make(map[string]*Node)}
 	root.mutex.Lock()
    root.children["apple"] = &Node{value: 5}
	root.mutex.Unlock()
	displayTrie(root)
	root.mutex.RLock()
	if node, ok := root.children["apple"]; ok {
        fmt.Println("Lookup - Value of 'apple':", node.value)
    }
	root.mutex.RUnlock()
	root.mutex.Lock()
	if node, ok := root.children["apple"]; ok {
    	node.value = 10
    	fmt.Println("Update - Value of 'apple' updated to:", node.value)
    }
    root.mutex.Unlock()
	displayTrie(root)
	root.mutex.Lock()
    delete(root.children, "apple")
	fmt.Println("Deletion - 'apple' node removed")
	root.mutex.Unlock()
    displayTrie(root)
}

Output

Hash Trie:
Value: 0
  Key: apple
  Value: 5
Lookup - Value of 'apple': 5
Update - Value of 'apple' updated to: 10
Hash Trie:
Value: 0
  Key: apple
  Value: 10
Deletion - 'apple' node removed
Hash Trie:
Value: 0

Example 2

In this example, we build a concurrency enabled hash trie structure with the Node struct which defines nodes with integer values and child nodes. Here, we achieve concurrency using channels for insert, lookup, update, and delete operations. Goroutines concurrently insert nodes, then lookup its value, update it, and finally delete it. In the main function, a root node is initialised to demonstrate this entire process.

package main
import (
    "fmt"
)
type Node struct {
	value	int
    children map[string]*Node
}
func displayTrie(node *Node, indent string) {
	fmt.Printf("%sValue: %d\n", indent, node.value)
    for key, child := range node.children {
        fmt.Printf("%sKey: %s\n", indent+"  ", key)
    	displayTrie(child, indent+"  ")
    }
}
func main() {
    root := &Node{children: make(map[string]*Node)}
    insertCh := make(chan *Node)
	lookupCh := make(chan *Node)
	updateCh := make(chan *Node)
	deleteCh := make(chan *Node)
	go func() { root.children["apple"] = &Node{value: 5}; insertCh <- root }()
	<-insertCh
	displayTrie(root, "Hash Trie:\n")
	go func() { lookupCh <- root }()
    lookedUpNode := <-lookupCh
	fmt.Println("Lookup - Value of 'apple':", lookedUpNode.children["apple"].value)
	go func() {
    	node := lookedUpNode.children["apple"]
        node.value = 10
    	updateCh <- root
    }()
    <-updateCh
    fmt.Println("Update - Value of 'apple' updated to:", lookedUpNode.children["apple"].value)
	displayTrie(root, "Hash Trie:\n")
 	go func() { delete(root.children, "apple"); deleteCh <- root }()
	<-deleteCh
	fmt.Println("Deletion - 'apple' node removed")
	displayTrie(root, "Hash Trie:\n")
}

Output

Hash Trie:
Value: 0
Hash Trie:
  Key: apple
Hash Trie:
  Value: 5
Lookup - Value of 'apple': 5
Update - Value of 'apple' updated to: 10
Hash Trie:
Value: 0
Hash Trie:
  Key: apple
Hash Trie:
  Value: 10
Deletion - 'apple' node removed
Hash Trie:
Value: 0

Real Life Implementation

  • Parallel Computing: Concurrent hash tries are often employed in parallel computing frameworks, which are widely used in scientific simulations and data processing pipelines, to properly manage shared data structures. This feature enables the execution of many processes or threads on discrete parts of data at the same time, resulting in improved performance and more effective resource use.

  • Dynamic Language Runtimes: In the implementation of dynamic programming languages like as Python, Ruby, and JavaScript, dynamic language runtimes often utilize concurrent hash attempts to handle data structures such as dictionaries or associative arrays. The aforementioned programming languages may support multithreading and leverage concurrent hash attempts to efficiently manage shared data while assuring concurrent execution safety.

Conclusion

Synchronisation and concurrency are crucial to modern computing and hash trie are capable of providing the best facilities for such. In this article, we implement a concurrent hash trie in golang using two different approaches. In the first approach, each operation is protected using locks for prevention of concurrent conflicts, and in the second example this objective is achieved by using channels through the help of Goroutines.

Updated on: 18-Oct-2023

38 Views

Kickstart Your Career

Get certified by completing the course

Get Started
Advertisements