CSC/ECE 506 Spring 2012/ch5a ja: Difference between revisions
No edit summary |
|||
Line 81: | Line 81: | ||
== Trees == | == Trees == | ||
=== Serial Code Example === | === Serial Code Example === | ||
Below is a code for tree traversal algorithms | |||
its behavior is as figure below shows: | |||
[[File: tree.PNG]] | |||
procedure Tree_Traversal is | |||
type Node; | |||
type Node_Access is access Node; | |||
type Node is record | |||
Left : Node_Access := null; | |||
Right : Node_Access := null; | |||
Data : Integer; | |||
end record; | |||
procedure Destroy_Tree(N : in out Node_Access) is | |||
procedure free is new Ada.Unchecked_Deallocation(Node, Node_Access); | |||
begin | |||
if N.Left /= null then | |||
Destroy_Tree(N.Left); | |||
end if; | |||
if N.Right /= null then | |||
Destroy_Tree(N.Right); | |||
end if; | |||
Free(N); | |||
end Destroy_Tree; | |||
function Tree(Value : Integer; Left : Node_Access; Right : Node_Access) return Node_Access is | |||
Temp : Node_Access := new Node; | |||
begin | |||
Temp.Data := Value; | |||
Temp.Left := Left; | |||
Temp.Right := Right; | |||
return Temp; | |||
end Tree; | |||
procedure Preorder(N : Node_Access) is | |||
begin | |||
Put(Integer'Image(N.Data)); | |||
if N.Left /= null then | |||
Preorder(N.Left); | |||
end if; | |||
if N.Right /= null then | |||
Preorder(N.Right); | |||
end if; | |||
end Preorder; | |||
procedure Inorder(N : Node_Access) is | |||
begin | |||
if N.Left /= null then | |||
Inorder(N.Left); | |||
end if; | |||
Put(Integer'Image(N.Data)); | |||
if N.Right /= null then | |||
Inorder(N.Right); | |||
end if; | |||
end Inorder; | |||
procedure Postorder(N : Node_Access) is | |||
begin | |||
if N.Left /= null then | |||
Postorder(N.Left); | |||
end if; | |||
if N.Right /= null then | |||
Postorder(N.Right); | |||
end if; | |||
Put(Integer'Image(N.Data)); | |||
end Postorder; | |||
procedure Levelorder(N : Node_Access) is | |||
package Queues is new Ada.Containers.Doubly_Linked_Lists(Node_Access); | |||
use Queues; | |||
Node_Queue : List; | |||
Next : Node_Access; | |||
begin | |||
Node_Queue.Append(N); | |||
while not Is_Empty(Node_Queue) loop | |||
Next := First_Element(Node_Queue); | |||
Delete_First(Node_Queue); | |||
Put(Integer'Image(Next.Data)); | |||
if Next.Left /= null then | |||
Node_Queue.Append(Next.Left); | |||
end if; | |||
if Next.Right /= null then | |||
Node_Queue.Append(Next.Right); | |||
end if; | |||
end loop; | |||
end Levelorder; | |||
N : Node_Access; | |||
begin | |||
N := Tree(1, | |||
Tree(2, | |||
Tree(4, | |||
Tree(7, null, null), | |||
null), | |||
Tree(5, null, null)), | |||
Tree(3, | |||
Tree(6, | |||
Tree(8, null, null), | |||
Tree(9, null, null)), | |||
null)); | |||
Put("preorder: "); | |||
Preorder(N); | |||
New_Line; | |||
Put("inorder: "); | |||
Inorder(N); | |||
New_Line; | |||
Put("postorder: "); | |||
Postorder(N); | |||
New_Line; | |||
Put("level order: "); | |||
Levelorder(N); | |||
New_Line; | |||
Destroy_Tree(N); | |||
end Tree_traversal; | |||
=== Parallel Code Solution === | === Parallel Code Solution === | ||
== Hash Tables == | == Hash Tables == |
Revision as of 21:09, 27 February 2012
Chapter 5a CSC/ECE 506 Spring 2012 / ch5a
An exploration and summary of concurrency issues as it relates to linked-list based data structures such as hash tables, trees, and graphs. This topic examines concurrency problems related to each type and possible solutions to allow for parallelization.
Introduction to Linked-List Parallel Programming
In determining opportunities for parallel programming during the software design process, many programmers will focus exclusively on loop or array structures, rather than also considering that linked-list pointer-based structures could also provide opportunities to run code in parallel across different processors.
For example, one common parallel technique used in an array processing algorithm is the copy-scan technique. This technique involves copying rows of data from one processor to another in a log(n) fashion until all processors have their own copy of that row. From there, you could perform a reduction technique to generate a sum of all the data, all while working in a parallel fashion. Take the following grid:
The basic process for copy-scan would be to:
Step 1) Copy the row 1 array to row 2. Step 2) Copy the row 1 array to row 3, row 2 to row 4, etc on the next run. Step 3) Continue in this manner until all rows have been copied in a log(n) fashion. Step 4) Perform the parallel operations to generate the desired result (reduction for sum, etc.).
But how does this same process work in the linked list world?
With linked lists, there is a concept called pointer doubling, which works in a very similar manner to copy-scan.
Step 1) Each processor will make a copy of the pointer it holds to it's neighbor. Step 2) Next, each processor will make a pointer to the processor 2 steps away. Step 3) This continues in logarithmic fashion until each processor has a pointer to the end of the chain. Step 4) Perform the parallel operations to generate the desired result.
However, with linked list programming, similar to array-based programming, it becomes imperative to have some sort of locking mechanism on critical sections to avoid race conditions. To make sure the results are correct, it is important that operations can be serialized appropriately and that data remains current and synchronized.
In this chapter, we will explore 3 linked-list based data structures and the concurrency issues they present: hash tables, trees, and graphs.
Linked Data Structure algorithms extension
Lock free algorithms
In order to overcome the ABA problems (shown in figure blow), we add auxiliary nodes to the data we structure. An auxiliary node is a cell that contains only a next field. We require that every normal cell in the list have an auxiliary node as its predecessor and as its successor. We permit “chains” of auxiliary nodes in the list (i.e., we do not require that every auxiliary node have a normal cell as its predecessor and successor), although such chains are undesirable for performance reasons.
The list also contains two dummy cells as the first and last normal cells in the list. These two cells are pointed at by the root pointers First and Last. These dummy cells need not, respectively, be preceded and followed by auxiliary nodes. Thus, m empty list data structure consists of these two dummy cells separated by an auxiliary node.
A cursor is implemented as three pointers into the data structure: target is a pointer to the cell at the position the cursor is visiting. If the cursor is visiting the end-of-list position, then target will be equal to Last. The pointer pre-aux points to an auxiliary node in the data structure. For a cursor c, if c“.pre-aux = c“.target, then the cursor is valid; otherwise it is invalid. The pointer pre.cell points to a regular cell in the data structure. This pointer is used only by the TRYDELETE operation described below. An invalid cursor indicates that the structure of the list in t he vicinity of the cursor has changed (due to a concurrent insert ion or deletion by another process) since the pointers in the cursor were last read. The UPDATE algorithm, given in Figure 5, examines the state of the list and updates the pointers in the cursor so that it becomes valid. Since the list structure contains auxiliary nodes (perhaps more than one in a row), the UPDATE algorithm must skip over them. If two adjacent auxiliary nodes axe found in the list, the UPDATE algorithm will remove one of them.
Traversal of the list data structure is accomplished by using the FIRST and NEXT operations, which use the UPDATE operation. Algorithms are given in Figures 6 and 7. The NEXT operation returns FALSE if the cursor is already at the end of the list and cannot be advanced. Adding new cells into the list requires the insertion of both the cell and a new auxiliary node. This insertion is restricted to occur in the following way: The new auxiliary node will follow the new cell in the list, and insertion can only occur between an auxiliary node and a normal cell, as shown in Figure 8.
Figure 9 gives an algorithm, which takes as arguments a cursor and pointers to a new cell and auxiliary node. The algorithm will try to insert the new cell and auxiliary node at the position specified by the cursor, returning the value TRUE if successful.
If the cursor becomes invalid, then the operation returns without inserting the new cell and returns the value FALSE. This allows a higher-level operation to detect that a change to the structure of the list occurred and to take it into account before attempting to insert the new cell again. For example, in the next section we show how the items in the list can be kept sorted using this technique.
Given a valid cursor, the cell that it is visiting can also be deleted from the list. As with the insertion of new cells, if the list structure changes (i.e., the cursor becomes invalid) then the operation fails and must be tried again. Figure 10 gives the TRYDELETE algorithm. The deletion of the cell from the list leaves an “extra” auxiliary node; concurrent processes deleting adjacent cells can result in longer chains. Most of the TRYDELETE algorithm is concerned with removing the extra auxiliary nodes from the list. Normally, removing the extra auxiliary node that results from the deletion of a cell from the list is accomplished by simply swinging the pointer in the cell pointed at by the pre-cell pointer in the cursor.
However, this does not always work; in particular, this cell may have itself been deleted from the list, in which case swinging its next pointer will not remove the extra auxiliary node: In order to overcome this problem, we add a back~ink field to the normal cells in the list. When a cell is deleted from the list, the pre.cell field of the cursor is copied into the cell’s backdink field. The TRYDELETE algorithm can then use these pointers to reverse back to a cell that has not been deleted from the list.
With just two processes, it is possible to create a chain of auxiliary nodes (with no intervening normal cells) of any length. However, any such chain can exist in the list only as long as some process is executing the TRYDELETE algorithm.
If all deletions have been completed, then the list will contain no extra auxiliary nodes. To see this, assume that there is a chain of two or more-+ auxiliary nodes in the list. Let z be the normal cell that was deleted from between the first two auxiliary nodes in the chain. Note that this implies that the normal cell that immediately preceded x in the list has not been deleted. By assumption, the operation that deleted z has completed. Consider the loop at lines 17–21 of the TRYDELETE algorithm. The only way for the process to exit this 100P, and hence to complete the operation, is for another deletion operation to have extended the chain of auxiliary nodes by deleting the normal cell g immediately following the chain, since the cell z is at the front of the chain. Furthermore, the deletion of y must have occurred after the operation deleting x had set its back_link pointer at line 6; otherwise the auxiliary node following y would have been included in the chain found in lines 13–16. Thus, the chain of back_link pointers followed by the process that deleted y will lead to the same normal cell that preceded x.
Now, the only way for the operation that deleted y to have completed is for the same reason as above; i.e., another TRYDELETE operation must extend the chain of auxiliary nodes by deleting a cell z. Since the length of the list must be finite, there must be a last such deletion which, but by the argument above, cannot have completed. Thus this operation must still be in progress, contradicting the assumption that there were no TRYDELETE operations in progress.
Linked Data Structures
Given this background knowledge, we can begin to explore solutions to some specific data structures that also utilized linked lists in some fashion.
Trees
Serial Code Example
Below is a code for tree traversal algorithms its behavior is as figure below shows:
type Node; type Node_Access is access Node; type Node is record Left : Node_Access := null; Right : Node_Access := null; Data : Integer; end record; procedure Destroy_Tree(N : in out Node_Access) is procedure free is new Ada.Unchecked_Deallocation(Node, Node_Access); begin if N.Left /= null then Destroy_Tree(N.Left); end if; if N.Right /= null then Destroy_Tree(N.Right); end if; Free(N); end Destroy_Tree; function Tree(Value : Integer; Left : Node_Access; Right : Node_Access) return Node_Access is Temp : Node_Access := new Node; begin Temp.Data := Value; Temp.Left := Left; Temp.Right := Right; return Temp; end Tree; procedure Preorder(N : Node_Access) is begin Put(Integer'Image(N.Data)); if N.Left /= null then Preorder(N.Left); end if; if N.Right /= null then Preorder(N.Right); end if; end Preorder; procedure Inorder(N : Node_Access) is begin if N.Left /= null then Inorder(N.Left); end if; Put(Integer'Image(N.Data)); if N.Right /= null then Inorder(N.Right); end if; end Inorder; procedure Postorder(N : Node_Access) is begin if N.Left /= null then Postorder(N.Left); end if; if N.Right /= null then Postorder(N.Right); end if; Put(Integer'Image(N.Data)); end Postorder; procedure Levelorder(N : Node_Access) is package Queues is new Ada.Containers.Doubly_Linked_Lists(Node_Access); use Queues; Node_Queue : List; Next : Node_Access; begin Node_Queue.Append(N); while not Is_Empty(Node_Queue) loop Next := First_Element(Node_Queue); Delete_First(Node_Queue); Put(Integer'Image(Next.Data)); if Next.Left /= null then Node_Queue.Append(Next.Left); end if; if Next.Right /= null then Node_Queue.Append(Next.Right); end if; end loop; end Levelorder; N : Node_Access;
begin
N := Tree(1, Tree(2, Tree(4, Tree(7, null, null), null), Tree(5, null, null)), Tree(3, Tree(6, Tree(8, null, null), Tree(9, null, null)), null)); Put("preorder: "); Preorder(N); New_Line; Put("inorder: "); Inorder(N); New_Line; Put("postorder: "); Postorder(N); New_Line; Put("level order: "); Levelorder(N); New_Line; Destroy_Tree(N);
end Tree_traversal;
Parallel Code Solution
Hash Tables
Hash Table Intro
Hash tables are very efficient data structures often used in searching algorithms for fast lookup operations. Hash tables contain a series of "buckets" that function like indexes into an array, each of which can be accessed directly using their key value. The bucket for which a piece of data will be placed is determine by a special hashing function.
The major advantage of a hash table is that lookup times are essentially a constant value, much like an array with a known index. With a proper hashing function in place, it should be fairly rare that any 2 keys would generate the same value.
In the case that 2 keys do map to the same position, there is a conflict that must be dealt with in some fashion to obtain the correct value. One way that is relevant to linked list structures is to have a chained hash table in which a linked list is created with all values that have been placed in that particular bucket. The developer would have to not only take into account the proper bucket for the data being searched for, but also must considered the chained linked list.
Serial Code Example
Insert Example: Create new node if hash bucket doesnt exist, else append to the chain.
/** * Put a key/value pair into the table by hashing the key to get the array * index of the chain that should hold the node containing the pair. * If an existing node with the same key is present then overwrite its value, * otherwise add a new node. * *@param key key object reference *@param val data object reference */ public void put(Object key, Object val) { int index = getIndex(key); Node node = find(key, data[index]); // Use helper method to determine if node with same key is already in // chain. If not, add a new node to head of chain. if (node == null) { data[index] = new Node(data[index], key, val); } else { // Otherwise update data value of existing node. node.val = val; } }
/** * Given a key object, return a data object from the table or * null if it is not found. * *@param key key object reference *@return data object reference or null if no object found */ public Object get(Object key) { Node temp = find(key, data[getIndex(key)]); if (temp != null) { return temp.val; } else { return null; } }
Delete Example: Delete a node from the chain.
/** * Remove a key/value pair from table if present, otherwise make no change. * *@param key key object reference */ public void remove(Object key) { int index = getIndex(key); Node ref = data[index]; Node previous = null; while (ref != null) { if ((ref.key).equals(key)) { if (previous == null) { data[index] = ref.next; } else { previous.next = ref.next; } return; } previous = ref; ref = ref.next; } }
/** * Given a key, use a hash function to obtain the array index of the chain * corresponding to the key. * The hashCode method inherited (and possibly overridden) * from class Object is called to do the hashing, with the returned * value constrained to the hash table array bounds. * The distribution of objects in the hash table will depend * on the quality of the hash function implemented by hashCode. * *@param key reference to object to be hashed *@return array index of chain corresponding to key */ private int getIndex(Object key) { return Math.abs(key.hashCode() % data.length); }
/** * Find node given a key and a chain to search. * *@param key key object reference *@param ref node object reference where search will start *@return node object holding key or null if not found */ private Node find(Object key, Node ref) { while (ref != null) { if ((ref.key).equals(key)) { return ref; } ref = ref.next; } return null; }
Parallel Code Solution
The key issue in a hash table in a parallel environment is to make sure any update/insert/delete sequences have been completed properly prior to attempting subsequent operations to make sure the data has been synched appropriately. However, since access speed is such a critical component of the design of a hash table, it is essential to try and avoid using too many locks for performing synchronization. Fortunately, a number of lock-free hash designs have been implemented to avoid this bottleneck.
Graphs
Graph Intro
A graph data structure is another type of linked-list structure that focuses on data that requires linking according to various types of relationships. For example, in a networking application, one network node may have connections to a variety of other network nodes. These nodes then also link to a variety of other nodes in the network. Using this connection of nodes, it would be possible to then find a path from one specific node to another in the chain. This could be accomplished by having each node contain a linked list of pointers to all other reachable nodes.
Serial Code Example
/** * Add a new edge to the graph. */ public void addEdge( String sourceName, String destName, double cost ) { Vertex v = getVertex( sourceName ); Vertex w = getVertex( destName ); v.adj.add( new Edge( w, cost ) ); }
/** * Single-source unweighted shortest-path algorithm. */ public void unweighted( String startName ) { clearAll( );
Vertex start = (Vertex) vertexMap.get( startName ); if( start == null ) throw new NoSuchElementException( "Start vertex not found" );
LinkedList q = new LinkedList( ); q.addLast( start ); start.dist = 0;
while( !q.isEmpty( ) ) { Vertex v = (Vertex) q.removeFirst( );
for( Iterator itr = v.adj.iterator( ); itr.hasNext( ); ) { Edge e = (Edge) itr.next( ); Vertex w = e.dest; if( w.dist == INFINITY ) { w.dist = v.dist + 1; w.prev = v; q.addLast( w ); } } } }
Parallel Code Solution
Quiz
References
http://oreilly.com/catalog/masteralgoc/chapter/ch08.pdf
http://www.devjavasoft.org/code/classhashtable.html
http://osr600doc.sco.com/en/SDK_c++/_Intro_graph.html
web.eecs.utk.edu/~berry/cs302s02/src/code/Chap14/Graph.java
http://people.engr.ncsu.edu/efg/506/s01/lectures/notes/lec8.html
http://en.wikipedia.org/wiki/File:Hash_table_3_1_1_0_1_0_0_SP.svg