Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a lock in the search functions of atomic trees. #32

Merged
merged 5 commits into from Jan 26, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/linting.yml
Expand Up @@ -18,7 +18,7 @@ jobs:
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install -r dev-requirements.txt
- name: Format with black
uses: psf/black@stable
- name: Linting with flake8
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/testing.yml
Expand Up @@ -16,7 +16,7 @@ jobs:
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: pip install -r requirements.txt
run: pip install -r dev-requirements.txt
- name: Test with pytest
run: |
pytest --cov=./ --cov-report=xml
Expand Down
2 changes: 1 addition & 1 deletion README.rst
Expand Up @@ -28,7 +28,7 @@ Requirements

Installation
------------
There are a few ways to install ``forest-python``.
There are a few ways to install ``forest-python``.

- Install the latest release from PyPI

Expand Down
9 changes: 9 additions & 0 deletions dev-requirements.txt
@@ -0,0 +1,9 @@
black
flake8
mypy
pydocstyle
pytest
pytest-cov

# Install all the dependencies of the release version
-r requirements.txt
2 changes: 1 addition & 1 deletion examples/multithreading_not_safe.py
@@ -1,4 +1,4 @@
"""Demonstrate the binary tree data structures are not thread-safe."""
"""Demonstrate the trees are not thread-safe in write contention situation."""
import threading
import sys

Expand Down
59 changes: 59 additions & 0 deletions examples/multithreading_not_safe_read_write.py
@@ -0,0 +1,59 @@
"""Demonstrate the trees are not thread-safe in read-write contention situation."""
import threading
import sys

from typing import Any, List

from forest.binary_trees import avl_tree


# Use a very small thread switch interval to increase the chance that
# we can reveal the multithreading issue easily.
sys.setswitchinterval(0.0000001)


flag = False # Flag to determine if the read thread stops or continues.


def delete_data(tree: avl_tree.AVLTree, data: List) -> None:
"""Delete data from a tree."""
for key in data:
tree.delete(key=key)


def find_node(tree: avl_tree.AVLTree, key: Any) -> None:
"""Search a specific node."""
while flag:
if not tree.search(key):
print(f" Fail to find node: {key}")


def multithreading_simulator(tree: avl_tree.AVLTree, tree_size: int) -> None:
"""Use one thread to delete data and one thread to query at the same time."""
global flag
flag = True
delete_thread = threading.Thread(
target=delete_data, args=(tree, [item for item in range(20, tree_size)])
)
query_node_key = 17
query_thread = threading.Thread(target=find_node, args=(tree, query_node_key))

delete_thread.start()
query_thread.start()

delete_thread.join()
flag = False
query_thread.join()
print(f"Check if the node {query_node_key} exist?")
if tree.search(key=query_node_key):
print(f"{query_node_key} exists")


if __name__ == "__main__":
print("Build an AVL Tree")
tree = avl_tree.AVLTree()
tree_size = 200
for key in range(tree_size):
tree.insert(key=key, data=str(key))
print("Multithreading Read/Write Test")
multithreading_simulator(tree=tree, tree_size=tree_size)
18 changes: 3 additions & 15 deletions examples/multithreading_performance.py
Expand Up @@ -2,21 +2,9 @@
import threading
import time

from typing import Any, List, Optional
from typing import List
from forest.binary_trees import avl_tree


class TestAVLTree(avl_tree.AVLTree):
"""Test AVL Tree with an unnecessary lock."""

def __init__(self) -> None:
avl_tree.AVLTree.__init__(self)
self._lock = threading.Lock()

def search(self, key: Any) -> Optional[avl_tree.Node]:
"""Query a node with an unnecessary lock."""
with self._lock:
return avl_tree.AVLTree.search(self, key=key)
from forest.binary_trees import atomic_trees


def query_data(tree: avl_tree.AVLTree, data: List) -> None:
Expand Down Expand Up @@ -73,7 +61,7 @@ def multithreading_simulator(tree: avl_tree.AVLTree, total_nodes: int) -> float:
print(f"Time in seconds: {delta_with_threads}")

# Multithread with lock case
avl_tree_with_lock = TestAVLTree()
avl_tree_with_lock = atomic_trees.AVLTree()
for key in range(total_nodes):
avl_tree_with_lock.insert(key=key, data=str(key))

Expand Down
2 changes: 1 addition & 1 deletion examples/multithreading_safe.py
@@ -1,4 +1,4 @@
"""Example to show atomic trees are thread-safe."""
"""Example to show atomic trees are thread-safe in write contention situation."""
import threading
import sys

Expand Down
61 changes: 61 additions & 0 deletions examples/multithreading_safe_read_write.py
@@ -0,0 +1,61 @@
"""Example to show atomic trees are thread-safe in read-write contention situation."""
import threading
import sys

from typing import Any, List

from forest.binary_trees import atomic_trees


# Use a very small thread switch interval to increase the chance that
# we can reveal the multithreading issue easily.
sys.setswitchinterval(0.0000001)


flag = False


def delete_data(tree: atomic_trees.AVLTree, data: List) -> None:
"""Delete data from a tree."""
for key in data:
tree.delete(key=key)


def find_node(tree: atomic_trees.AVLTree, key: Any) -> None:
"""Search a specific node."""
global flag
while flag:
if not tree.search(key):
print(f" Fail to find node: {key}")


def multithreading_simulator(tree: atomic_trees.AVLTree, tree_size: int) -> None:
"""Use one thread to delete data and one thread to query at the same time."""
global flag
flag = True
delete_thread = threading.Thread(
target=delete_data, args=(tree, [item for item in range(20, tree_size)])
)
query_node_key = 17
query_thread = threading.Thread(target=find_node, args=(tree, query_node_key))

delete_thread.start()
query_thread.start()

delete_thread.join()
flag = False
query_thread.join()
print(f"Check if the node {query_node_key} exist?")
if tree.search(key=query_node_key):
print(f"{query_node_key} exists")


if __name__ == "__main__":

print("Build an AVL Tree")
tree = atomic_trees.AVLTree()
tree_size = 200
for key in range(tree_size):
tree.insert(key=key, data=str(key))
print("Multithreading Read/Write Test")
multithreading_simulator(tree=tree, tree_size=tree_size)
36 changes: 36 additions & 0 deletions forest/binary_trees/atomic_trees.py
Expand Up @@ -23,6 +23,11 @@ def __init__(self, registry: Optional[metrics.MetricRegistry] = None) -> None:
avl_tree.AVLTree.__init__(self, registry=registry)
self._lock = threading.Lock()

def search(self, key: Any) -> Optional[avl_tree.Node]:
"""Thread-safe search."""
with self._lock:
return avl_tree.AVLTree.search(self, key=key)

def insert(self, key: Any, data: Any) -> None:
"""Thread-safe insert."""
with self._lock:
Expand All @@ -41,6 +46,11 @@ def __init__(self, registry: Optional[metrics.MetricRegistry] = None) -> None:
binary_search_tree.BinarySearchTree.__init__(self, registry=registry)
self._lock = threading.Lock()

def search(self, key: Any) -> Optional[binary_search_tree.Node]:
"""Thread-safe search."""
with self._lock:
return binary_search_tree.BinarySearchTree.search(self, key=key)

def insert(self, key: Any, data: Any) -> None:
"""Thread-safe insert."""
with self._lock:
Expand All @@ -59,6 +69,11 @@ def __init__(self, registry: Optional[metrics.MetricRegistry] = None) -> None:
red_black_tree.RBTree.__init__(self, registry=registry)
self._lock = threading.Lock()

def search(self, key: Any) -> Optional[red_black_tree.Node]:
"""Thread-safe search."""
with self._lock:
return red_black_tree.RBTree.search(self, key=key)

def insert(self, key: Any, data: Any) -> None:
"""Thread-safe insert."""
with self._lock:
Expand All @@ -77,6 +92,13 @@ def __init__(self) -> None:
double_threaded_binary_tree.DoubleThreadedBinaryTree.__init__(self)
self._lock = threading.Lock()

def search(self, key: Any) -> Optional[double_threaded_binary_tree.Node]:
"""Thread-safe search."""
with self._lock:
return double_threaded_binary_tree.DoubleThreadedBinaryTree.search(
self, key=key
)

def insert(self, key: Any, data: Any) -> None:
"""Thread-safe insert."""
with self._lock:
Expand All @@ -97,6 +119,13 @@ def __init__(self) -> None:
single_threaded_binary_trees.LeftThreadedBinaryTree.__init__(self)
self._lock = threading.Lock()

def search(self, key: Any) -> Optional[single_threaded_binary_trees.Node]:
"""Thread-safe search."""
with self._lock:
return single_threaded_binary_trees.LeftThreadedBinaryTree.search(
self, key=key
)

def insert(self, key: Any, data: Any) -> None:
"""Thread-safe insert."""
with self._lock:
Expand All @@ -117,6 +146,13 @@ def __init__(self) -> None:
single_threaded_binary_trees.RightThreadedBinaryTree.__init__(self)
self._lock = threading.Lock()

def search(self, key: Any) -> Optional[single_threaded_binary_trees.Node]:
"""Thread-safe search."""
with self._lock:
return single_threaded_binary_trees.RightThreadedBinaryTree.search(
self, key=key
)

def insert(self, key: Any, data: Any) -> None:
"""Thread-safe insert."""
with self._lock:
Expand Down
5 changes: 4 additions & 1 deletion forest/binary_trees/avl_tree.py
Expand Up @@ -98,6 +98,9 @@ def search(self, key: Any) -> Optional[Node]:
The node found by the given key.
If the key does not exist, return `None`.
"""
return self._search(key=key)

def _search(self, key: Any) -> Optional[Node]:
current = self.root

while current:
Expand Down Expand Up @@ -165,7 +168,7 @@ def delete(self, key: Any) -> None:
key: `Any`
The key of the node to be deleted.
"""
if self.root and (deleting_node := self.search(key=key)):
if self.root and (deleting_node := self._search(key=key)):

# Case: no child
if (deleting_node.left is None) and (deleting_node.right is None):
Expand Down
5 changes: 4 additions & 1 deletion forest/binary_trees/binary_search_tree.py
Expand Up @@ -96,6 +96,9 @@ def search(self, key: Any) -> Optional[Node]:
The node found by the given key.
If the key does not exist, return `None`.
"""
return self._search(key=key)

def _search(self, key: Any) -> Optional[Node]:
current = self.root

while current:
Expand Down Expand Up @@ -154,7 +157,7 @@ def delete(self, key: Any) -> None:
key: `Any`
The key of the node to be deleted.
"""
if self.root and (deleting_node := self.search(key=key)):
if self.root and (deleting_node := self._search(key=key)):

# Case 1: no child or Case 2a: only one right child
if deleting_node.left is None:
Expand Down
5 changes: 4 additions & 1 deletion forest/binary_trees/double_threaded_binary_tree.py
Expand Up @@ -101,6 +101,9 @@ def search(self, key: Any) -> Optional[Node]:
The node found by the given key.
If the key does not exist, return `None`.
"""
return self._search(key=key)

def _search(self, key: Any) -> Optional[Node]:
current = self.root
while current:
if key == current.key:
Expand Down Expand Up @@ -182,7 +185,7 @@ def delete(self, key: Any) -> None:
key: `Any`
The key of the node to be deleted.
"""
if self.root and (deleting_node := self.search(key=key)):
if self.root and (deleting_node := self._search(key=key)):

# Case 1: no child
if (deleting_node.left_thread or deleting_node.left is None) and (
Expand Down
5 changes: 4 additions & 1 deletion forest/binary_trees/red_black_tree.py
Expand Up @@ -125,6 +125,9 @@ def search(self, key: Any) -> Optional[Node]:
The node found by the given key.
If the key does not exist, return `None`.
"""
return self._search(key=key)

def _search(self, key: Any) -> Optional[Node]:
current = self.root

while isinstance(current, Node):
Expand Down Expand Up @@ -190,7 +193,7 @@ def delete(self, key: Any) -> None:
key: `Any`
The key of the node to be deleted.
"""
if (deleting_node := self.search(key=key)) and (
if (deleting_node := self._search(key=key)) and (
isinstance(deleting_node, Node)
):
original_color = deleting_node.color
Expand Down
11 changes: 9 additions & 2 deletions forest/binary_trees/single_threaded_binary_trees.py
Expand Up @@ -99,6 +99,10 @@ def search(self, key: Any) -> Optional[Node]:
The node found by the given key.
If the key does not exist, return `None`.
"""
return self._search(key=key)

def _search(self, key: Any) -> Optional[Node]:

current = self.root
while current:
if key == current.key:
Expand Down Expand Up @@ -170,7 +174,7 @@ def delete(self, key: Any) -> None:
key: `Any`
The key of the node to be deleted.
"""
if self.root and (deleting_node := self.search(key=key)):
if self.root and (deleting_node := self._search(key=key)):

# Case 1: no child
if deleting_node.left is None and (
Expand Down Expand Up @@ -486,6 +490,9 @@ def search(self, key: Any) -> Optional[Node]:
The node found by the given key.
If the key does not exist, return `None`.
"""
return self._search(key=key)

def _search(self, key: Any) -> Optional[Node]:
current = self.root

while current:
Expand Down Expand Up @@ -559,7 +566,7 @@ def delete(self, key: Any) -> None:
key: `Any`
The key of the node to be deleted.
"""
if self.root and (deleting_node := self.search(key=key)):
if self.root and (deleting_node := self._search(key=key)):

# Case 1: no child
if deleting_node.right is None and (
Expand Down