Quantifiability: Concurrent Correctness from First Principles

05/15/2019
by   Victor Cook, et al.
University of Central Florida
0

Architectural imperatives due to the slowing of Moore's Law, the broad acceptance of relaxed semantics and the O(n!) worst case verification complexity of generating sequential histories motivate a new approach to concurrent correctness. Desiderata for a new correctness condition are that it be independent of sequential histories, composable over objects, flexible as to timing, modular as to semantics and free of inherent locking or waiting. We propose Quantifiability, a novel correctness condition based on intuitive first principles. Quantifiability models a system in vector space to launch a new mathematical analysis of concurrency. The vector space model is suitable for a wide range of concurrent systems and their associated data structures. This paper formally defines quantifiablity with its system model and demonstrates useful properties such as compositionality. Analysis is facilitated with linear algebra, better supported and of much more efficient time complexity than traditional combinatorial methods. We present results showing that quantifiable data structures are highly scalable due to the usage of relaxed semantics, an explicit implementation trade-off that is permitted by quantifiability.

READ FULL TEXT VIEW PDF
POST COMMENT

Comments

There are no comments yet.

Authors

page 1

page 2

page 3

page 4

04/03/2018

Distributionally Linearizable Data Structures

Relaxed concurrent data structures have become increasingly popular, due...
10/12/2021

A Simple Way to Verify Linearizability of Concurrent Stacks

Linearizability is a commonly accepted correctness criterion for concurr...
07/03/2021

Engineering MultiQueues: Fast Relaxed Concurrent Priority Queues

Priority queues with parallel access are an attractive data structure fo...
11/30/2020

Modularising Verification Of Durable Opacity

Non-volatile memory (NVM), also known as persistent memory, is an emergi...
10/23/2018

Correctness of Concurrent Objects under Weak Memory Models

In this paper we develop a theory for correctness of concurrent objects ...
05/21/2020

Repairing and Mechanising the JavaScript Relaxed Memory Model

Modern JavaScript includes the SharedArrayBuffer feature, which provides...
12/13/2017

B-slack trees: Highly Space Efficient B-trees

B-slack trees, a subclass of B-trees that have substantially better wors...
This week in AI

Get the week's most popular data science and artificial intelligence research sent straight to your inbox every Saturday.

I Introduction

As predicted [1], concurrent data structures have arrived at a tipping point where change is inevitable. These drivers converge to motivate new thinking about correctness:

  • Architectural demands to utilize multicore and distributed resources [2]

  • General acceptance of relaxed semantics [3, 4, 5, 6, 7, 8, 9, 10, 11, 12]

  • The intractable complexity of concurrent system models [13] prompting the search for reductions [14, 8, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 7, 29, 30, 31]

There are a number of correctness conditions for concurrent systems presented in literature [32, 33, 34, 35, 36, 12, 37]. A discussion of these concurrent correctness conditions found in Section V-A. Here follows a brief synopsis.

A concurrent history defines the events in a system of concurrent processes and objects. An object subhistory, , of a history is the subsequence of all events in that are invoked on object  [34]. A sequential specification for an object is a set of sequential histories for the object [35]. A sequential history is legal if each subhistory in for object belongs to the sequential specification for .

The difference between the correctness conditions resides in the allowable method call orderings. Serializability [32] places no constraints on the method call order. Sequential Consistency [33] requires that each method call take effect in program order. Linearizability [34] requires that each method call take effect at some instant between its invocation and response. A correctness condition is compositional if and only if, whenever each object in the system satisfies , the system as a whole satisfies  [35]. Linearizability [34] is a desirable correctness condition for systems of many shared objects where the ability to reason about correctness in a compositional manner is essential. Sequential consistency [33] is suitable for an independent system such as a hardware memory interface that requires program order of method calls to be preserved. Other correctness conditions [36, 12, 37] are defined that permit relaxed behaviors of method calls to obtain performance enhancements in concurrent programs.

These correctness conditions require a concurrent history to be equivalent to a sequential history. While this way of defining correctness enables concurrent programs to be reasoned about using verification techniques for sequential programs [38, 39], it imposes several inevitable limitations on a concurrent system. Such limitations include 1) requiring the specification of the behavior of a concurrent system to be described as if it were a sequential system, 2) restricting the method calls in a concurrent history to respect data structure semantics and to be ordered in a way that satisfies the correctness condition, leading to performance bottlenecks, and 3) burdening correctness verification with a worst-case time complexity of to compute the sequential histories for the possible interleavings of concurrent method calls. Some correctness verification tools have provided optimizations [40, 37] integrated into model checkers that accept user annotated linearization points to reduce the search space of possible sequential histories to time in addition to the time to perform the model checking with dynamic partial-order reductions, where is the number of processes, is the maximum size of the search stack, and is the number of transitions explored in the search space [41]. However, this optimization technique for correctness verification is only effective if all methods have fixed linearization points.

This paper proposes Quantifiability, a new definition of concurrent correctness that does not require reference to a sequential history. Freeing analysis from this historical artifact of the era of single threaded computation and establishing a purely concurrent correctness condition is the goal of this paper. Quantifiability eliminates the necessity of demonstrating equivalence to sequential histories by evaluating correctness of a concurrent history based solely on the outcome of the method calls. Like other conditions it does require atomicity of method calls and enables modular analysis with its compositional properties.

Quantifiability supports separation of concerns. Principles of correctness are not mixed with real-time ordering or data structure semantics. These are modifiers or constraints on the system. The disregard of real-time order and conservation of method calls enables concurrent histories to be represented in vector space. Although it is not possible to foresee all uses of the vector space model, this paper will demonstrate the use of linear algebra to efficiently verify concurrent histories as quantifiable.

The following presentation of quantifiability and its proposed verification technique draws unabashedly on the work of Herlihy, who shaped the way a generation thinks about concurrency. This paper repositions concurrent correctness in a way that overcomes the inherent limitations associated with defining correctness through equivalence to sequential histories. Contributions to the field are:

  1. We propose quantifiability as a concurrent correctness condition and illustrate its benefits over other correctness conditions.

  2. We show that quantifiability is compositional and non-blocking.

  3. We introduce linear algebra as a formal tool for reasoning about concurrent systems.

  4. We present a verification algorithm for quantifiability with a significant improvement in time complexity compared to analyzing many sequential histories using traditional correctness conditions.

  5. A quantifiably correct concurrent stack and queue are implemented and shown to scale well.

I-a First Principles

Among principles that describe concurrent system behavior, first principles define what things happen while secondary principles such as timing and order are modifiers on them. The conditions defined in secondary principles do not make sense without the first, but the reverse is not the case [42]. This view accords with intuition: Tardiness to a meeting is secondary to there being a meeting at all. In a horse race, first principles define that the jockeys and horses are the same ones who started; only then does the order of finish makes sense. The intuition that the events themselves are more important than their order, and that conservation is a prerequisite for ordering will be motivated with the following examples.

P0

P1

History H1: Sequentially consistent and “almost”linearizable.

Consider history on object with Last-In-First-Out (LIFO) semantics. The notation follows the convention (), where is the method, is the object, is the process, and is the item to be input or output. is serializable and also sequentially consistent. is not linearizable, but it would be if the interval of were slightly extended to overlap or a similar adjustment were made relative to . Linearizability requires determining “happens before” relationships for among all method calls to project them onto a sequential timeline. Doing this with a shared clock timing the start and end of each method call is not feasible [43]. What is available, given some inter-process communication, is a logical clock [44]. Once established, linearizability is compositional [45]. Linearizability is sometimes “relaxed”, creating loopholes to enable performance gains. Without timing changes, is linearizable using -LIFO semantics where [9].

P0

P1

History H2: Not serializable because calls are notconserved.

Consider history on the same object . is not serializable, not sequentially consistent, not linearizable, and no changes in timing will allow to meet any of these conditions. Also, there is no practical relaxation of semantics that accepts . There is an essential difference in the correctness of and . What happened in history is intuitively acceptable, given some adjustments to when (timing) and how (relaxed semantics) it happened. What happened in history is impossible, as it creates the return value 3 from nothing. As in the equestrian example, item 3 is not one of the starting horses. The method calls on object are not conserved. A correctness condition that captures the difference between and would allow separating the concerns of what happened and when according to the (possibly relaxed) semantics.

P0

P1

History H3: Serializable, not sequentially consistent.

History has two objects and . The projections and are serializable. The combined history is serializable. Projections and are also sequentially consistent. However, their composition into is not sequentially consistent. Sequential consistency is not compositional [44]. Projection is not linearizable, therefore is also not linearizable.

Conditional semantics are semantics that enable a method to return null upon reaching an undefined object state. History is the same as with the exception of introducing conditional semantics for , making explicit a common relaxation of how a stack works. With the conditional becomes sequentially consistent, yielding multiple correct orderings and end states.

P0

P1

History H4: Conditional makes sequentiallyconsistent.

But conditional is not consistent with early formal definitions of the stack abstract data type where on an empty stack threw an error [46] or a signal [47]. The semantics of these exceptions were taken seriously [48]. Invariants prevented exceptions, and there was “no guarantee” of the result if they were violated [49]. The conditional can be traced to the literature on performance [50], where the requirement to handle errors and check invariants is ignored. Conditional semantics remain prevalent in recent work, extending to proofs of correctness allowing two different linearization points with respect to the same method calls [20].

Allowing a stack pop to conditionally return null is not merely a convenience for performance benchmarks. Altering the semantics of to return null is as significant as changing the order in which items are returned. Imagine the task of debugging even a simple sequential process if the result of method calls defined inside the program were conditional upon events occurring outside the program. For example, define x-- as a method , which a process calls respecting the invariant that x>0. Now suppose that the method occasionally returns null and fails to decrement x. This is analogous to the behavior of a concurrent stack with a conditional .

Type theory supports the view that conditional semantics, although permissible, alter the method just as fundamentally as other semantic changes. A well-cited formal definition of an abstract data type confirms this [49], representing both the empty stack invariant and the ordering of return values for as subtypes of an unordered “bag” of items. Each is a separate concern, explicitly implemented within a type system.

P0

P1

P2

)

History H5: keeps trying to .

History illustrates another problem with the conditional . Consider a stack that allocates a scarce resource. issued a request before and repeats it soon after, but gets nothing. might be repeated many times with and exchanging the item. The scheduler allocates twice as many requests per cycle to as either or , so why is there starvation? It is because conditional is inherently unfair. Although is not blocked in the sense of waiting to complete the method [51], conditional causes it to repeatedly lose its place in the ordering of requests. It might be called “progress without progress.” Recall that serializability causes inherent blocking and this was used to show the benefits of linearizability [34]. A new correctness condition should be free of inherent unfairness as well. In a quantifiably correct system the first request would remain active, not blocking unless the thread desires to wait, in the program can be made asynchronous by allocating an address as an inbox so it reads and checking as desired to see if the address is updated.

I-B Temporal Order of Asynchronous Method Calls

Quantifiability places less importance on order than linearizability, and is more like serializability. Several authors promote the idea that linearizability and serializability are of different categories and should be applied to different domains. This recognizes the fact that there are important domains where the temporal order does not matter. Database systems are mentioned as an example, where the semantics of related atomic method calls in a transaction determine correctness, not the temporal order of (mostly) unrelated individual method calls.

The point of relaxed semantics of concurrent system ordering is to allow method calls to be executed out of order to effectively utilize multiple processors. In general relaxed semantics apply to all method calls, even those in the same process. A push followed by another push in the same process on a stack with relaxation of 4 may be reordered so that the first push ends up second, and the second push goes into fourth place. A concurrent system with shared objects does not need to be strictly sequentially consistent, because calls to the shared objects are asynchronous and the result returned depends upon what all the other processes did in the interval between calls in the program order.

An appropriate baseline for the use of relaxed semantics can be stated for a given concurrent system. Consider a system with P processes. At any moment there may be P candidates as the next item put into or removed from the container. The only way to know which item it will be is if you are controlling which process is given the CAS. That process will win. The others will spin with contention. There are up to P possible candidates for the next item. A 64 processor system should use at least 64 as its relaxation level because the placement of the next item depends upon which of 64 processors wins the CAS race. It is not something over which the data structure itself has control, a fact that is ignored in most system models. Therefore it follows that any system with P processes should adopt a relaxation level of

at least P.

Quantifiability recognizes that order can be critical. However the temporal order of unrelated method calls rarely is, and should not be used to determine program correctness. Instead of starting from the most restrictive ordering semantics and relaxing them, quantifiability starts by assigning a value to the method calls and then applying constraints on temporal, semantic, or transactional ordering that are appropriate to the application.

Multicore programming is considered an art [35] and is generally regarded as difficult. The projection of a concurrent history onto a sequential timeline provided an abstraction to understand systems with a few processes and led to definitions of their correctness. Linearizability, being compositional, ensured that reasoning about system correctness is linear with respect to the number of objects. But verifying linearizability for an individual object is not linear in the number of method calls. Systems today may have thousands of processes and millions of method calls, far beyond the capacity of current verification tools. The move from art to an engineering discipline requires a new correctness condition with the following desirable properties:

  • Conservation What happens is what the methods did. Return values cannot be pulled from thin air (history ). Method calls cannot disappear into thin air (history ).

  • Measurable Method calls have a certain and measurable impact on system state, not sometimes null (history ).

  • Compositional Demonstrably correct objects and their methods may be combined into demonstrably correct systems (history ).

  • Unconstrained by Timing Correctness based on timing has high complexity and can be handled in the type system rather than in the correctness condition (history and ).

  • Lock-free, Wait-free, Starvation-Free Design of the correctness condition should not limit or prevent system progress.

Ii Definition

Quantifiability is concerned with the impact of method calls on the system as opposed to the projection of a method call onto a sequential timeline. The configuration of an arbitrary element comprises the values stored in that element. The system state is the configuration of all the objects that represents the outcome of the method calls by the processes.

Ii-a Principles of Quantifiability

A familiar way to introduce a correctness condition is to state principles that must be followed for it to be true [35]. Quantifiability embodies two principles.

Principle 1.

Method conservation: Method calls are first class objects in the system that must succeed, remain pending, or be explicitly cancelled.

Principle 1 requires that every instance of a process calling a method, including any arguments and return values specified, is part of the system state. Method calls are not ephemeral requests, but “first class” [52, 53] members of the system. All remain pending until they succeed or are explicitly cancelled. Method calls may not be cancelled implicitly as in the conditional . Actions expected from the method by the calling process must be completed. This includes returning values (if any) and making the expected change to the state of the concurrent object on which the method is defined.

Principle 2.

Method quantifiability: Method calls have a measurable impact on the system state.

Principle 2 requires that every method call owns a scalar value, or metric, that reflects its impact on system state. There is some total function that computes this metric for each instance of a method call.

Building on Principle 1 that conserves the method calls themselves, Principle 2 requires that a value can be assigned to the method call. All method calls “count.” The conservation of method calls along with the measurement of their impact on system state is what gives quantifiability its name. Values are assigned as part of the correctness analysis. As with concepts such as linearization points, these values are not necessarily part of the data structure, but are artifacts for proving correctness.

The assignment of values to the method calls may be straightforward. For the stack abstract data type, is set to +1 and is set to -1. Sometimes value assignments are subtle: Principle 1 requires that pending reads are first class members of the system state, so completing them is a state change. Reads will have a small but measurable impact, unlike reads in other system models that are considered to have no effect on system state. Probes such as a method on a set data type would also have a value.

It may appear as though quantifiability is equivalent to serializability. However, quantifiability does not permit method calls to return null upon reaching an undefined object state while serializability does permit this behavior. Quantifiability measures the outcome of every method call by virtue of its completion. This subtle difference directly impacts the complexity of analysis, and can lead to throughput increases for quantifiability when designing a data structure. Quantifiable implementations learn from relaxed semantics to “save” a method call in an accumulator rather than discarding it due to a data structure configuration where the method call could not be immediately fulfilled. Quantifiable data structure design is discussed in further details in Section VI.

Ii-B System Model

A concurrent system is defined here as a finite set of methods, processes, objects and items. Methods define what happens in the system. Methods are defined on a class of objects but affect only the instances on which they are called. Processes are the actors who call the methods, either in a predetermined sequence or asynchronously driven by events. Objects are encapsulated containers of concurrent system state. Objects are where things happen. Items are data passed as arguments to and returned as a result from completed method calls on the concurrent objects. Items are passive and may not have methods defined on them. Method invariants and semantics place constraints such as order, defining how things happen. Quantifiable concurrent histories are serializable so every method call takes effect during the interval spanning the history, meaning that when method calls occur may be reordered to achieve correctness. A summary of the components of the system model are presented in Table I.

Component Role
Processes Processes call methods
Methods Methods act on instances of objects, only on whose abstract data type that method is defined
Objects Objects are instances of an abstract data type
Items Items are data passed as input to methods or returned as output from methods
TABLE I: System model components and roles

A method call is a pair consisting of an invocation and next matching response [34]. Each method call is specified by a tuple (Method, Process, Object, Item). Method calls with input or output that comprises multiple items can be represented as a single structured item. An execution of a concurrent system is modeled by a concurrent history (or simply history), which is a multiset of method calls [34]. An invocation is pending in history if no matching response follows the invocation [34]. Although the domain of possible methods, processes, objects, and items is infinite, actual concurrent histories are a small subset of these. Method calls in the history are done by a process on an object and further specified with items passed or returned.

It is not unusual when discussing concurrent histories to speak of, “the projection of a history onto objects.” However the focus from there has always been on building sequential histories, so the literature does not extend this language to bring the analysis of concurrent histories formally into the realm of linear algebra. Quantifiability facilitates this extension, with fruitful consequences.

Ii-C Vector Space

A vector is an ordered -tuple of numbers, where is an arbitrary positive integer. A row vector is a vector with a row-by-column dimension of 1 by . A column vector is a vector with a row-by-column dimension of by 1. In this section our system model is mapped to a vector space over the field of real numbers . The system model is isomorphic to vector spaces over described in matrix linear algebra textbooks [54]. From this foundation, analysis of concurrent histories can proceed using the tools of linear algebra.

The components of the system model are represented as dimensions in the vector space, written in the order Methods, Processes, Objects and Items. The basis vector of the history is the Cartesian product of the dimensions . Each unique configuration of the four components defines a basis for a vector space over the real numbers. The spaces thus defined are of finite dimension. In this model, orthogonal means that dimensions are independent. For example, the projection of history onto object () is separate from the projection of history onto object (). An orthogonal basis is a basis whose vectors are orthogonal. It is necessary to define an orthogonal basis because each non-interacting method call with distinct objects, processes, and items is an independent occurrence from every other combination.

A history is represented by a vector with elements corresponding to the basis vector uniquely defined by the concurrent system. Principle 2 states that each method call has a value. These are the values represented in the elements of the history vector. On a LIFO stack, and pop methods are inverses of each other. An important difference is that is completed without dependencies in an unbounded stack, whereas returns the next available item, which may not arrive for some time. A history that shows a completed must account for the source of the item being returned, either in the initial state or in the history itself. The discussion of history in Section I-A showed this is common to the analysis of serializability, quantifiability and linearizability.

Concurrent histories can be written as column vectors whose elements quantify the occurrences of each unique method call, that is, a vector of coordinates over acting on a basis constructed of the Methods, Processes, Objects and Items involved. History in Section I-A can be written:

(1)

The basis shown above is orthogonal due to the independence of the dimensions. The process and method dimensions are clearly independent from each other, one is the caller, the other is being called. Their roles do not permit overlap. Likewise the sets of objects and the items they manipulate should be distinct in order to apply this model. Every possible history using the given methods, processes, objects and items can be written in terms of the basis.

Two histories with the same basis can be added. Let H6 be the history , , , . can be added to history to get a new history:

(2)

Scalar multiplication is simply repeating a history:

(3)

The history vector has the potential to be large considering that the basis vector is defined according to the Cartesian product of the dimensions . However, the history vector will likely be sparse unless all possible combinations in which the items passed to the methods invoked by the processes on the objects occur in the history. If the history vector is sparse, then the algorithms analyzing the history vector can be optimized to take advantage of the sparse structure. With a dense history vector where the majority of the elements in the history vector represent a method that actually occurs in the history, the complexity remains contained as standard linear algebra can be applied in the analysis of the history vector. An example analysis is demonstrated in III-C using the Python Numpy library.

Ii-D Formal Definition

The formal definition of quantifiability is described using terminology from mathematics, set theory, and linear algebra in addition to formalisms presented by Herlihy et al. [34]

to describe concurrent systems. Methods are classified according to the following convention. A

producer is a method that generates an item to be placed in a data structure. A consumer is a method that removes an item from the data structure. A reader is a method that reads an item from the data structure. A writer is a method that writes to an existing item in the data structure.

A method call set is an unordered set of method calls in a history. A producer set is a subset of the method call set that contains all its producer method calls. A consumer set is a subset of the method call set that contains all its consumer method calls. A writer set is a subset of the method call set that contains all its writer method calls. A reader set is a subset of the method call set that contains all its reader method calls. Since a method call is a pair consisting of an invocation and next matching response, no method in the method call set will be pending. Quantifiability does not discard the pending method calls from the system state nor does it place any constraints on their behavior while they remain pending.

The history vector described in Section II-C is transformed such that the method calls in a history are represented as a set of vectors. To maintain consistency in defining quantifiability as a property over a set of vectors, all method calls are represented as column vectors. Each position of the vector represents a unique combination of input/output parameters and objects that are encountered by the system, where this representation is uniform among the set of vectors. Since quantifiability places no constraints on the methods based on the process calling the method, the process dimension described in Section II-C is omitted. Given a system that encounters unique combinations of input/output parameters and objects, each method call is represented by an -dimensional column vector.

The value assignment scheme is chosen such that the changes to the system state by the method calls are “quantified.” For all cases, let be a column vector that represents method call in a concurrent history. Each element of is initialized to 0.
Case (): Let be the position in representing the combination of input parameters passed to and the object that operates on. Then .
Case (): Let be the position in representing the combination of output parameters returned by and the object that operates on. Then .
Case (): Let be the position in representing the combination of input parameters passed to and the object that operates on. Let be the position in representing the combination of input parameters that correspond to the previous value held by the object that is overwritten by . If , then and , else .
Case (): Let be the position in representing the combination of output parameters returned by and the object that operates on. Let be a column vector representing a read index for each combination of output parameters returned by a reader method, where each element of is initialized to 0. Then , .

In the case for , setting to 1, where denotes the position representing the combination of input parameters passed to and the object that operates on, captures the entrance of the new item into the system. In the case for , setting to -1, where denotes the position representing the combination of output parameters returned by and the object that operates on, captures the removal of the item from the system.

In the case for , an item that exists in the system is overwritten with the input parameters passed to . A writer method accomplishes two different things in one atomic step: 1) it consumes the previous value held by the item and 2) it produces a new value for the item. This state change is represented by setting the position in representing the corresponding object and combination of the input parameters to be written to an item to 1 and by setting the position in representing the corresponding object and combination of input parameters corresponding to the previous value held by the item to -1. If the input parameters corresponding to the previous value held by the item are identical to the input parameters to be written to an item, then the position in representing the combination of input parameters to be written to the item is set to zero since no change has been made to the system state.

To separate the two actions performed by the writer method into separate vectors, linear algebra can be applied to in the following way. Let be the vector representing the producer effect of the writer method. Then . Let be the vector representing the consumer effect of the writer method. Then .

The addition of to when computing will cause all elements with a -1 value to become 0, and the multiplication of the scalar will revert all elements with a value of 2 back to 1. The floor function is applied to revert elements with a value of back to 0. A similar reasoning can be applied to the computation of .

In the case for , returns the state of an item that exists in the system as output parameters. Multiple reads are permitted for an item with the constraint that the output parameters returned by a reader reflect a state of the item that was initialized by a producer method or updated by a writer method. This behavior is accounted for by setting , where denotes the position representing the corresponding object and the combination of output parameters returned by and represents the read count for the combination of output parameters returned by . The series is a geometric series, where . Since a concurrent history will always contain a finite number of methods, the elements of the resulting vector obtained by taking the sum of the reader method vectors will have a value in the range of . If this vector is further added with the sum of the producer method vectors and the writer method vectors , the elements of the resulting vector will always be greater than zero given that the output of all reader methods corresponds with a value that was either initialized by a producer method or updated by a writer method.

Definition 1.

Let be the vector obtained by applying vector addition to the set of vectors for the producer set of history . Let be the vector obtained by applying vector addition to the set of vectors for the writer set of history . Let be the vector obtained by applying vector addition to the set of vectors for the writer set of history . Let be the vector obtained by applying vector addition to the set of vectors for the reader set of history . Let be the vector obtained by applying vector addition to the set of vectors for the consumer set of history . Let be a vector with each element initialized to 0.
For each element ,
if then = + +
else = + + + + .
History is quantifiable if for each element , .

Informally, if all vectors representing the methods in the method call set of history are added together, the value of each element should be greater than or equal to zero. This property indicates that the net effect of all methods invoked upon the system is compliant with the requirement that no non-existent items have been removed, updated, or read from the system. In other words, all method calls are “conserved.” The values of the vectors for the reader method are assigned such that each element in the sum of the reader method vectors is always greater than -1. As long as the output of the reader method is equivalent to the input of a producer method or writer method, then the reader method has observed a state of the system that corresponds to the occurrence of a producer method or writer method. The ceiling function is applied to if which yields a value that is also . Once the reader method vectors have been added appropriately, the remaining method call vectors can be directly added to compute for history .

If any element of is less than zero, then a consume action has been applied to either an item that does not exist in the system state (the item was previously consumed by another consumer method) or an item that never existed in the system state (the item was never produced by a producer method or written by a writer method), which is not quantifiable due to a violation of Principle 1.

A notable difference between defining correctness as properties over a set of vectors and defining correctness as properties of legal sequential histories is the growth rate of a set of vectors versus legal sequential histories when the number of methods called in a history is increased. The size of a set of vectors grows at the rate of with respect to methods called in a history. The number of legal sequential histories grows at the rate of with respect to methods called in a history. This leads to significant time cost savings when verifying a correctness condition defined as properties over a set of vectors since analysis of -dimensional vectors using linear algebra can be performed in time ( time to assign values and compute separate vectors that each represent a sum of the producer, consumer, writer, and reader method call vectors, and time to add the vectors representing the sum of the producer, consumer, writer, and reader method call vectors).

1:enum OpType
2:   Producer
3:   Consumer
4:   Reader
5:   Writer
6:struct Method
7:   OpType
8:   void*
9:   void*
10:   void*
11:   void*
12:function ParamsToIndex()
Algorithm 1 Type Definitions

Iii Verification Algorithm

Algorithm 1 presents the type definitions for a method call in a history. Each Method type has a field for its OpType (producer, consumer, reader, or writer) (line 1.7), a field for the object that the method is invoked upon (line 1.8), a field for its input parameters (line 1.9), a field for its output parameters (line 1.10), and a field for a previous value if the method is a write (line 1.11). The function ParamsToIndex on line 1.12 is a hashing function that retrieves in time the unique index associated with the object and input/output parameters. Each method type has a field for its classification (producer, consumer, reader, or writer), a field for the object that the method is invoked upon, a field for its input parameters, a field for its output parameters, and a field for a previous value if the method is a write. The function ParamsToIndex is a hashing function that retrieves in time the unique index associated with the object and input/output parameters.

Algorithm 2 presents the verification algorithm for quantifiability derived from the corresponding formal definition. Line 2.1 is defined as a constant that is the total number of unique configurations comprising the input/output and objects that are encountered by the system. The array tracks the running sum of the producer method vectors. The array tracks the running sum of the new values written by the writer method vectors. The array tracks the running sum of the previous values overwritten by the writer method vectors. The array tracks the running sum of the reader method vectors. The array tracks the running sum of the consumer method vectors. The array tracks the running sum of the read index for the reader method vectors. The array tracks the final sum of all method call vectors. The VerifyHistory function accepts a set of method calls as an argument on line 2.3. The for-loop on line 2.5 iterates through the methods in the method call set and adds a value to the appropriate array according to the value assignment scheme discussed in Section II-D.

The for-loop on line 2.21 iterates through each of the configurations and sums the method call vectors according to Definition 1 to obtain the final vector . If any element of is less than zero or greater than one (line 2.26), then the history is not quantifiable. Otherwise, if all elements of are greater than or equal to zero, then the history is quantifiable.

1: Total number of input/output/object configurations
2:, , , , , ,
3:function VerifyHistory()
4:    
5:    for  do
6:        if  then
7:            
8:            
9:        else if  then
10:            
11:            
12:            
13:            
14:        else if  then
15:            
16:            
17:            
18:        else if  then
19:            
20:                         
21:    for  do
22:        if   then
23:             = + +
24:        else
25:             = + + + +         
26:        if  then
27:            return false            
28:    return true
Algorithm 2 Quantifiability Verification

Iii-a Time Complexity of Verification Algorithm

Let be the total number of methods in a history and let be the total number of configurations determined according to the input/output of each method and the object to be invoked on by the method. The for-loop on line 2.5 takes time to iterate through all methods in the method call set. The for-loop on line 2.5 takes time to iterate through all possible configurations. Let be the total number of input/output combinations and let be the total number of objects. The total number of configurations is . Therefore, the total time complexity of VerifyHistory is .

Iii-B Correctness of Verification Algorithm

The correctness arguments for VerifyHistory are provided to convince the reader that representing a history as a set of method call vectors and applying linear algebra to the vectors to compute guarantees that if each element of is greater than or equal to zero, then the methods of appear to occur atomically in some arbitrary order.

Theorem 1.

Let be a history. Let be an -dimensional vector, where is the total number of configurations comprising the object and input or output parameters of the method calls of . Let vector represent the final state of the system resulting from history . Let be an arbitrary item configuration that is a unique combination of the object and input/output method call parameters. VerifyHistory produces the vector such that if and only if history corresponds to an arbitrary permutation of the methods in the method call set such that the methods appear to occur atomically.

Proof.

Since no restrictions are placed on the producer methods (duplicate items are permitted), it is sufficient to show that the methods in a history appear to occur atomically as long as no non-existent items have been removed, updated, or read from the system. Each element of is greater than or equal to one if an item with the configuration associated with position exists in the system, or zero if if an item with the configuration associated with position does not exist in the system. First, it must be shown that if then history corresponds to an arbitrary permutation of the methods in the method call set such that the methods appear to occur atomically. By contrapositive proof, suppose that history corresponds to an arbitrary permutation of the methods in the method call set such that the methods do not appear to occur atomically (i.e., a non-existent item has either been removed from the system, updated in the system, or read from the system). Proceed using a proof by cases evaluation.
Case 1: A non-existent item has been removed from the system. If a non-existent item is removed from the system, then ( + ) + . Since and are always less than or equal to zero, , so is false.
Case 2: A non-existent item has been updated in the system. If a non-existent item is updated in the system, then ( + ) + . Since and are always less than or equal to zero, , so is false.
Case 3: A non-existent item has been read in the system. If a non-existent item is read from the system, then ( + ) and the ceiling function cannot be applied. Since , and and are always less than or equal to zero, , so is false.

Second, it must be shown that if history corresponds to an arbitrary permutation of the methods in the method call set such that the methods appear to occur atomically then . By contrapositive proof, suppose that . The only elements that could cause to be less than zero are , , and . Proceed using a proof by cases evaluation.
Case 1: , , and . If , then ( + + ) . Isolating to one side of the inequality yields ( + ), which implies that a non-existent item has been removed from the system, so the statement “the methods appear to occur atomically” is false.
Case 2: , , and . If , then ( + + ) . Isolating to one side of the inequality yields ( + ), which implies that a non-existent item has been updated in the system, so the statement “the methods appear to occur atomically” is false.
Case 3: , , and . If , then ( + + ) . Since is guaranteed to fall in the range of , the inequality ( + + ) will only be true if ( + ) . This implies that a non-existent item has be read from the system, so the statement “the methods appear to occur atomically” is false. ∎

Iii-C Correctness with Linear Algebra

The mathematics above is readily translated into programming languages optimized for linear algebra, such as Python. To facilitate certain computations a history vector may be reshaped into tensors of rank 2, 3, or 4. Reshaping makes it convenient to project the history over one or more components and calculate properties of the history.

To determine quantifiability using Algorithm 2, the history vector is folded into a tensor of rank 2 and summed over the methods and the processes, thereby reduced to the axes of objects and items. the ceiling and floor functions are applied and quantifiable correctness of the history verified using a few lines of Numpy code.

For example consider the sample history vector below of two methods, two processes, two objects and four items.

import numpy as np
from numpy import array
methods = array([’push’,’pop’])
processes = array([’P0’,’P1’])
objects = array([’x’,’y’])
items = array([’7’,’8’,’9’,’10’])
dM = methods.size
dP = processes.size
dO = objects.size
dI = items.size
dim = dM * dP * dO * dI
base = array([[M, P, O, I] \
for M in methods for P in processes \
for O in objects for I in items ])
H = array([1,1,4,3, 1,1,2,11, \
6,4,1,1, 1,12,7,5, -2,-1,-1,-1, \
-1,-2,-3,-10, -1,-1,-1,-1, -1,-1,-1,-2])
H_oi_sum = H.reshape(dM*dP, dO*dI).sum(0)
print("Quantifiable") \
if (all(i >= 0 for i in H_oi_sum)) \
else print("NotQuantifiable")
base_oi = base[0:dO*dI, 2:4]
print("QtyofObject-Itempairs:", ] \
list(zip(H_oi_sum, base_oi.tolist())))

Iv Properties of Quantifiability

The system model presented in Section II-B is mapped to a vector space. We do not claim that the axioms of a vector space hold for all possible concurrent systems. We do propose a mapping from many types of concurrent systems to the mathematical ideal of a vector space. Concurrent systems fitting the model define a vector space and their histories are the vectors in that space. For concurrent systems fitting the model properties of a vector space become axiomatic and have a variety of uses.

Iv-a Vector Space Properties

This section interprets vector space properties as applied to concurrent histories. Quantifiable histories, closed under addition and scalar multiplication, are a subspace of these histories.

[style=unboxed,leftmargin=0cm,itemsep=1pt,parsep=2pt]

Closure under addition

- Adding two quantifiable histories (i.e. whose sum over Object-Item pairs is non-negative) produces another history with the same property.

Addition is associative and commutative

- The quantifiable system model allows any order and grouping of multiple histories as they are added together.

Zero history

- The system model admits a history consisting of all zeroes, in which nothing happens.

Inverse histories

- An inverse history is one that cancels all the operations of a previous history, following quantifiability Principle 1.

Closure under scalar multiplication

- A quantifiable history may be repeated any number of times and the resulting history remains quantifiable.

Scalar multiplication is distributive

- Scalar multiplication is repeating a history. Repeating multiple histories together or individually the same number of times sums to the same values representing the final state of the system, following quantifiability Principle 1 and Principle 2.

Identity scalar

- The identity scalar is one. A history multiplied by one remains the same vector.

Iv-B Vector Norms

Vector norms are scalar quantities that can be assigned meaning to concurrent systems that define a vector space.

Iv-B1 Taxicab Norm

The taxicab norm, also called the norm, represents the operation count. The norm is interpreted based on the value assignment scheme. In general it measures the amount of work done on the objects in the system during the history. The norm of a history resembles the potential function used in amortized complexity analysis [55].

(4)

If the history vector is transformed into a set of vectors {,…,…}, where is the total number of objects in the system state and where only includes the producer methods for object , the taxicab norm of yields the maximum size of object . This information is necessary to verify that a method that observes the size of an object is quantifiable. Since quantifiability is compositional, these methods can be reasoned about separately from the other methods in a history. Let size() be a method that returns the size of object . Let the value assigned to size() be the value returned by size(). The method size() is quantifiable if size() . The reasoning for determining the quantifiability of size() in this manner is that size() should never return a value greater than the total number of items produced for object .

Let is_empty() be a method that returns true if the size of object  is zero and false otherwise. Let the value assigned to is_empty() be 1 if is_empty() returns true and zero otherwise. The method is_empty() is quantifiable if (is_empty() == 1) or (is_empty() == 0 and 0). The reasoning for determining the quantifiability of is_empty() in this manner is that is_empty() could return true for any circumstance and be quantifiable under the assumption that object  is initially empty. However, if is_empty() returns false, then at least one item must have been produced for object , which is verified by checking that 0.

Iv-B2 Euclidean Distance

The Euclidean distance, i.e. length or norm of a history vector, is the distance from the initial to the final state in the vector space defined by the concurrent system.

(5)

Iv-B3 Floor of Sum

Although not a vector norm (because it may have a negative value), the floor of the sum of the history vector reveals net versus for the history. This is height of the stack in the final state, possibly a negative stack as in Figure 1.

(6)

Iv-C Compositionality

To show compositionality, it must be shown that the composition of two quantifiable histories is quantifiable, and that the decomposition of histories, i.e. the projection of the history on any of its obejcts, is also a quantifiable history. This is formally stated in the following theorem.

Theorem 2.

History is quantifiable if and only if, for each object , is quantifiable.

Proof.

It first must be shown that if each history for object is quantifiable, then history is quantifiable. Since the addition of quantifiable histories is closed under addition, it follows that the composition of quantifiable object subhistories is also quantifiable. Therefore, is quantifiable.

It now must be shown that if history is quantifiable, then each history for object is quantifiable. Since is quantifiable, then each element of the vector . Each position in corresponds to a unique configuration representing the input/output of a method and the object that the method is invoked upon. Since each element of the vector , then for each element associated with object , . Each history for object is therefore quantifiable. ∎

Iv-D Non-Blocking and Non-Waiting Properties

A correctness condition may inherently cause blocking, as is the case with serializability applied to transactions [34]. Quantifiability shares with linearizability the non-blocking property, and for the same reason: it never forces a process with a pending invocation to block.

The requirement that all methods must succeed or be explicitly cancelled raises the question of how this is non-blocking. Indeed a thread might choose to block if there is no way it can proceed without the return value or the state change resulting from the method. It is a matter for the application to decide, not an inherent property of Quantifiability Principle 1. For example, consider thread 1 calling a method on a concurrent stack, . This can be written as <Type> v = s.pop(); which is blocking in C. Or it may be invoked as a call by reference in the formal parameters s.pop(<Type> &v); which is non-blocking. The second invocation also permits the thread to block if desired by spinning on the address to check if a result is available. If address &v is not pointing to a value of <Type>, the method has not yet succeeded. Alternatively, instead of spin-waiting, a thread can do a “context switch” and proceed with other operations while waiting for the pending operation to succeed. And if the thread decides the method is no longer needed, it can be cancelled.

Duplicate method calls can be handled in several ways conforming to Principle 1. A duplicate call might be considered a syntactic shorthand for “cancel the first operation and resubmit”, or it could throw a run time error to have identical calls on the same address. Alternatively an index could be added to the method call to uniquely identify it such that it can be distinguished from other identical calls. Quantifiabile semantics support these options in blocking and non-blocking versions well suited to the type systems of programming languages. Useful extensions of this include permitting threads to pre-request some number of method calls, and the ability to assign priority to the method call independent of the order represented in the data structure. For example the priority queue demonstrated in this paper implements the priority of items in the queue and the priority of incoming requests. The proof of the non-blocking property for quantifiability is presented below.

Iv-E Proof of Non-Blocking Property

The following proof shows that quantifiability is a non-blocking property; that is, it is never required to wait for another pending operation to complete.

Theorem 3.

Let be an invocation of a method . If is a pending invocation in a quantifiable history with a corresponding vector , then either there exists a response such that either is quantifiable or is quantifiable.

Proof.

If method is a producer method that produces item with configuration , then there exists a response such that is quantifiable because is greater than zero since by the definition of quantifiability. If method is a consumer method that consumes an item with configuration , then a response exists if . If method is a writer method that updates item with configuration to a new configuration , then a response exists if and . If method is a reader method that reads an item with configuration , then a response exists if . If a response for method does not exist, then method can be cancelled. Upon cancellation, is removed from history . Since quantifiability places no restrictions on the behavior of pending method calls, is quantifiable. ∎

V Related Work

Quantifiability is motivated by recent advances in concurrency research. Frequently cited works [56, 57] are already moving in the direction of the two principles stated in Section II-A. This section places quantifiability in context of several threads of research: the basis of concurrent correctness conditions, complexity of proving correctness and performance of data structures.

V-a Relationship to Other Correctness Conditions

A sequential specification for an object is a set of sequential histories for the object [35]. A sequential history is legal if each subhistory in for object belongs to the sequential specification for . Many correctness conditions for concurrent data structures are proposed in literature [32, 33, 34, 35, 36, 12, 37], all of which reason about concurrent data structure correctness by demonstrating that a concurrent history is equivalent to a legal sequential history.

Reasoning about concurrent data structures in terms of a legal sequential history is appealing because it builds upon previous techniques for verifying correctness of sequential objects [38, 39]. However, generating all possible legal sequential histories of a concurrent history has a worst case time complexity of , leading to inefficient verification techniques for determining correctness of concurrent data structures.

Serializability  [32] is a correctness condition such that a history is serializable if and only if there is a serial history such that is equivalent to . A history is strictly serializable if there is a serial history such that is equivalent to , and an atomic write ordered before an atomic read in implies that the same order be retained by . Papadimitriou draws conclusions implying that there is no efficient algorithm that distinguishes between serializable and non-serializable histories [32]. A decision problem is a problem that is a yes-no question of the input values. A decision problem is in the Nondeterministic Polynomial time (NP) complexity class if “yes” answers to the decision problem can be verified in Polynomial time (P). A decision problem is in NP-complete if is in NP, and every problem in NP is reducible to in polynomial time. Papadimitriou proved that testing whether a history is serializable is NP-complete. An implication of this result is that, unless , there is no efficient algorithm that distinguishes between serializable and non-serializable histories [32].

Sequential consistency [33] is a correctness condition for multiprocessor programs such that the result of any execution is the same as if the operations of all processors were executed sequentially, and the operations of each individual processor appear in this sequence in program order. Lamport indicates that the sequential consistency of an individual process does not guarantee that the program as a whole is sequentially consistent. Lamport proposes that compositionality for sequentially consistent objects can be achieved by requiring that the memory requests from all processors issued to an individual memory module are serviced from a single FIFO queue which guarantees a total ordering for the program execution. Since sequential consistency is not compositional, Lamport proposes that compositionality for sequentially consistent objects can be achieved by requiring that the memory requests from all processors are serviced from a single FIFO queue. However, a single FIFO queue is a sequential bottleneck that limits the potential concurrency for the entire system.

Linearizability [34] is a correctness condition such that a history is linearizable if is equivalent to a legal sequential history, and each method call appears to take effect instantaneously at some moment between its invocation and response. Herlihy et al. [35] suggest that linearizability can be informally reasoned about by identifying a linearization point in which the method call appears to take effect at some moment between the method’s invocation and response. Identifying linearization points avoids reference to a legal sequential history when reasoning about correctness, but such reasoning is difficult to perform automatically. Herlihy et al. [34] compared linearizability with strict serializability by noting that linearizability can be viewed as a special case of strict serializability where transactions are restricted to consist of a single operation applied to a single object. Comparisons of correctness conditions for concurrent objects to serializability are made in a similar manner by considering a special case of serializability where transactions are restricted to consist of a single method applied to a single object. Herlihy compared linearizability with serializability by grouping multiple method calls into a transaction making the histories strictly serializable. Here we consider the serializability of method calls, not transactions.

Quiescent consistency [36] is a correctness condition for counting networks that establishes a safety property for a network of two-input two-output computing elements such that the inputs will be forwarded to the correct output wires at any quiescent state. A step property is defined to describe a property over the outputs that is always true at the quiescent state. Quasi-linearizability [12] builds upon the formal definition of linearizability to include a sequential specification of an object that is extended to a larger set that includes sequential histories that are not legal, but are within a bounded distance from a legal sequential history.

Ou et al. [37] present non-deterministic linearizability, a correctness model for concurrent data structures that utilize the relaxed semantics of the C/C++ memory model. A specification for a concurrent object comprises a non-deterministic, sequentialized version of the concurrent object and an admissibility function that permits two methods to be unordered in the concurrent execution. A valid sequential history is a sequential history that is legal in the C/C++ memory model. The notion of justified behaviors is introduced to account for method calls exhibiting non-deterministic behavior in a concurrent history. A concurrent object is non-deterministic linearizable on a valid sequential history for a specification if and only if each method call in is justified and returns a value as specified by its non-deterministic specification. A concurrent object is non-deterministic linearizable on an execution for a specification if and only if is non-deterministic linearizable for all valid sequential histories of for .

Unlike the correctness conditions proposed in literature, quantifiability does not define correctness of a concurrent history by referencing an equivalent legal sequential history. Quantifiability requires that the method calls be conserved, enabling correctness to be proven by quantifying the method calls and applying linear algebra to the method call vectors. This fundamental difference enables quantifiability to be verified more efficiently than the existing correctness conditions because applying linear algebra can be performed in + time ( is the number of methods and is the number of input/output and object configurations), while deriving the legal sequential histories has a worst case time complexity of . A diagram illustrating the relationship between quantifiability and the correctness conditions

Herlihy et al. [34] compared linearizability with strict serializability by noting that linearizability can be viewed as a special case of strict serializability where transactions are restricted to consist of a single operation applied to a single object. Comparisons of correctness conditions for concurrent objects to serializability are made in a similar manner by considering a special case of serializability where transactions are restricted to consist of a single method applied to a single object. Figure 1 shows the relationship of linearizability, sequential consistency, quiescent consistency, and quantifiability, all of which are serializable due to the requirement that all method calls should appear to occur atomically. All linearizable histories are sequentially consistent because the preservation of real-time order provided by linearizability also preserves program order. All linearizable histories are also quiescently consistent because method calls always take effect according to real-time order regardless of periods of quiescence. Quiescent consistency and sequential consistency are incomparable [35], so there exist sequentially consistent histories that are not quiescently consistent. Similarly, there exist quiescently consistent histories that are not sequentially consistent. There exist linearizable, sequentially consistent, quiescently consistent, and serializable histories that are not quantifiable because these correctness conditions all allow method calls to return null when reaching an undefined object state. There exist quantifiable histories that are not linearizable, sequentially consistent, or quiescently consistent because quantifiability places no constraints on real-time order or program order of method calls.

Serializable

Neither
Not Seq. Consistent
Not Quantifiable

Quantifiable

Not Sequentially Consistent
Serializable histories where methods
in a thread are out of order.

Sequentially Consistent

Not Quantifiable
Serializable histories where methods are not conserved

Quiescently Consistent

Linearizable
Fig. 1: The hierarchy of serializable histories, showing Quantifiability and other correctness conditions

-2

-1

top @time2

 

0

start

 

1

2

3

top @time1

 

 

 

 

Fig. 2: Negative stack formed @time2 after 3 followed by 5 calls.

V-B Proving Correctness

Verification tools are proposed [40, 58, 59, 37] to enable a concurrent data structure to be checked for correctness according to various correctness conditions. Vechev et al. [40] present an approach for automatically checking linearizability of concurrent data structures. The approach incorporates two methods for checking linearizability: 1) automatic linearization and 2) linearization points. The automatic linearization technique searches for a permitted linearization by exploring all permutations of a concurrent execution. The linearization point technique accepts user-provided annotations indicating the linearization points of an algorithm so that an order between overlapping method calls can be determined, eliminating the need to explore all permutations of a concurrent execution. Burckhardt et al. [58] present Line-Up, a tool that checks deterministic linearizability automatically. Line-up derives a sequential specification for a concurrent data structure by recording all sequential histories for a finite test and checking that each concurrent execution is equivalent to one of the recorded sequential histories. Zhang et al. [59] present Round-up, a runtime verification tool for checking quasi-linearizability violations of concurrent data structures. Round-up systematically executes a test program with a standard sequential data structure to compute all possible legal sequential histories. Each concurrent history is first checked for linearizability by determining if it is equivalent to a legal sequential history. If the concurrent history is linearizable, it is also quasi-linearizable by definition. Otherwise, the order of the method calls in each legal sequential history are rearranged up to a specified distance to generate a quasi-linearization in which the concurrent history is compared with to determine if it is quasi-linearizable. Ou et al. [37] develop a tool that checks non-deterministic linearizability for concurrent data structures designed using the relaxed semantics of the C/C++ memory model. Ordering point annotations are utilized to identify a specific instruction in which the effects of a method become visible to the system. During model checking, the method calls are organized in a directed acyclic graph where a method has a directed edge to if ’s ordering point occurs before ’s ordering point in the generated concurrent history. A topological sort of the directed acyclic graph yields a legal sequential history for the concurrent history. To reduce the large time complexity required to derive all possible legal sequential histories, an option is provided to the user that randomly generates and checks a customized number of legal sequential histories.

These verification tools are all faced with the computationally expensive burden of generating all possible legal sequential histories of a concurrent history since this is the basis of correctness for the correctness conditions in literature. The correctness verification tools presented by Vechev et al. [40] and Ou et al. [37] accept user annotated linearization points to eliminate the need for deriving a legal sequential history for a reordering of overlapping methods. Although this optimization is effective for fixed linearization points, it could potentially miss valid legal sequential histories for method calls with non-fixed linearization points in which the linearization point may change based on overlapping method calls.

V-C Performance of Related Data Structures

The elimination backoff stack (EBS) [57] uses an elimination array where and method calls are matched to each other at random within a short time delay if the main stack is suffering from contention. This concept of waiting to match up mutually satisfying calls provided inspiration for quantifiability. In the algorithm, the delay is set to a fraction of a second, which is a sufficient amount of time to find a match during busy times. If no match arrives, the method call retries its operation on the central stack object. When operating on the central stack object, the method is at risk of failing if the stack is empty. However, if the elimination array delay time is set to infinite, the elimination backoff stack implements Quantifiability Principle 1, and all the method calls wait until they succeed.

Dual data structures [60] are concurrent object implementations that hold reservations in addition to data to handle conditional semantics. Partial methods are divided into a request method and a follow-up method. If the dual data structure is empty, a request is inserted into the data structure to be fulfilled by another thread. The follow-up method is used to determine if the request has been fulfilled. Dual data structures are linearizable and can be implemented to provide non-blocking progress guarantees including lock-free, wait-free, or obstruction-free. The main difference between dual data structures and quantifiable data structures is the allowable order in which the requests may be fulfilled. The relaxed semantics provided by quantifiability provides an opportunity for performance gains over the dual data structures.

The TS-Queue [56] is one of the fastest queue implementations, claiming twice the speed of the elimination backoff version. The TS Queue also relies on matching up method calls, enabling methods that would otherwise fail when reaching an undefined state of the queue to instead be fulfilled at a later time. In the TS Queue, rather than a global delay, there is a tunable parameter called padding

added to different method calls. By setting an infinite time padding on all method calls, the TS Queue follows Quantifiability Principle 1.

These works share in common that they significantly improve performance by using a window of time in which pending method calls are conserved until they can succeed. Quantifiability Principle 1 extends this window of time for conservation of method calls indefinitely, while allowing threads to cancel them as needed for specific applications.

The -FIFO queue [61] maintains segments each consisting of slots implemented as either an array for a bounded queue or a list for an unbounded queue. This design enables up to and operations to be performed in parallel and allows elements to be dequeued out-of-order up to a distance . Quantifiability takes the relaxed semantics of the -FIFO queue a step further by allowing method calls to occur out-of-order up to any arbitrary distance, leading to significant performance gains as demonstrated in Section VI-D.

Fig. 3: QStack, EBS and Treiber stack.
Fig. 4: QQueue, FAA queue, LCRQ and MS queue.

Vi Implementation

Quantifiable containers must conserve method calls. Figure 2 shows a simple LIFO stack implementing quantifiability. As first class members of the system, pending method calls can be included in a data structure. Items in a stack are the result of previous calls to . Quantifiability requires that calls to on an empty stack have equal standing to be included in the structure. Intuitively, the unsatisfied calls to form a stack of negative length, where the methods will be satisfied as resources permit.

The stack is initialized with a pointer to an empty sentinel node. Three calls to load the items 5,9,7 to the stack @time1, followed by five calls resulting in a negative stack @time2. The unfulfilled calls to are first class objects saved to the stack. The next will send an item to and promote to the top of the stack. The calls to are pending and non-blocking. Processes that create the pending calls may periodically check the address. They may also send a request to cancel if it is no longer of interest. The order in which calls are fulfilled is implementation dependent. The stack may include relaxed semantics [12]. It may have arbitrary prioritization, such as execute calls from processes paying higher rates [62].

Quantifiability is applicable to other abstract data types that deliver additional functionality beyond the standard producer/consumer methods provided by queues and stacks. Consider a reader method such as a operation for a vector or a operation for a set. If the item to be read does not exist in the data structure, a pending item is created and placed in the data structure at the same location where the item to be read would be placed if it existed. If a pending item already exists for the item to be read, the reader method references this pending item. Once a producer method produces the item for which the pending item was created, the pending item is updated to a regular (non-pending) item. Since the reader methods hold a reference to this item, they may check the address when desired to determine if the item of interest is available to be read. A similar strategy can be utilized for writer methods.

The concept of retrieving an item to be fulfilled at a later time is implemented in C++11, C#, and Java as promises and futures [63]. The wait_for function in C++ is provided by the future object that enables a thread to wait for the item in the promise object to be set to a value for a specified time duration. Once the wait_for function returns a ready status, the item value can be retrieved by the future object through the get function. The disadvantage of the get function is that it blocks until the item value is set by the promise object. Once the get function returns the item, the future object is no longer valid, leading to undefined behavior if other threads invoke get on this future object. Due to the semantics of the get function, we advise implementing retrieval of an item to be fulfilled at a later time with a shared object used to announce information, referred to as a descriptor object [64, 65]. A discussion on progress guarantees is provided below.

Vi-a Progress

The length of time a method call may remain pending is a progress condition. For example wait free progress requires that the method call would succeed in a finite amount of time. The idea that method calls can happen at the same time is not so hard to accept because they may fulfill each other, in the manner of the elimination backoff stack [57]. Accepting that they may happen in any order is to accept the reality that since the days of the shared bus are gone, there is nothing like an “external observer” to determine if a history is acceptable. Latency is a fact of parallel and distributed systems. Quantifiable systems may include synchronization schemes, important in many domains, but the correctness condition should be that methods succeed, whatever may be done to give an illusion of real-time ordering.

Lock-freedom is the property that some thread is guaranteed to make progress. Wait-freedom is the property that all threads are guaranteed to make progress. Quantifiable data structures can be implemented in a way that satisfies either lock-freedom or wait-freedom. The strategies for guaranteeing lock-freedom or wait-freedom in quantifiable data structures is no different than non-blocking data structures presented in literature [57, 66, 67, 68]. Quantifiability is compatible with the existing synchronization methods for lock-freedom and wait-freedom because it is a non-blocking correctness property.

As discussed in Section IV-D, a thread can proceed with other operations while waiting on a pending operation to succeed. This can be achieved by using a descriptor object to create a pending item to be fulfilled by another thread. Consider a consumer method such as a operation for a hashmap. If the item to be read does not exist in the hashmap, a pending item is created and placed in the hashmap at the same location where the item to be read would be placed if it existed. If a pending item already exists for the item to be read, the reader method references this pending item. Once a producer method produces the item for which the pending item was created, the pending item is updated to a regular (non-pending) item. Since the reader methods hold a reference to this item, they may check the address when desired to determine if the item of interest is available to be read. Consider a reader method such as a operation for a vector. If the item to be read does not exist in the data structure, a pending item is created and placed in the data structure at the same location where the item to be read would be placed if it existed. If a pending item already exists for the item to be read, the reader method references this pending item. Once a producer method produces the item for which the pending item was created, the pending item is updated to a regular (non-pending) item. Since the reader methods hold a reference to this item, they may check the address when desired to determine if the item of interest is available to be read. This enables quantifiability to be achieved in a lock-free manner. A similar strategy can be utilized for consumer and writer methods. Wait-freedom can also be achieved using helping schemes in conjunction with descriptor objects to announce an operation to be completed in a table such that all threads are required to check the announcement table and help a pending operation prior to starting their own operation [69].

Lock-freedom is achieved through the atomic instruction Compare-And-Swap (CAS). CAS accepts as input a memory location, an expected value, and an update value. If the data referenced by the memory location is equivalent to the expected value, then the data referenced by the memory location is changed to the update value and true is returned. Otherwise, no change is made and false is returned. Since CAS will only fail if another thread successfully updates the data referenced by the memory location, a failed CAS implies that some other thread made progress. Thus, lock-freedom can be achieved through a non-cyclical CAS-based loop.

Transactions require multiple atomic steps to complete [70, 71]. For these circumstances, a descriptor object can be used to post the required atomic steps for an operation and a helping scheme can be employed to enable other threads to help complete the pending operation in a lock-free manner. Wait-freedom is also achieved using helping schemes in conjunction with descriptor objects to announce an operation to be completed in a table such that all threads are required to check the announcement table and help a pending operation prior to starting their own operation [69].

Vi-B QStack

The quantifiable stack (QStack) is designed to avoid contention wherever possible. Consider the state of a stack receiving two concurrent operations. Assume a stack contains only the node . Two threads concurrently push nodes and . The state of the stack after both operation have completed is shown in figure 5. The order is one of two possibilities: , or . Under quantifiability, either or are valid candidates for a operation.

1

2(a)

2(b)

Fig. 5: Concurrent push representation of nodes "2(a)" and "2(b)"

The QStack is structured as a doubly-linked tree of nodes. Two concurrent method calls are both allowed to append their nodes to the data structure, forming a fork in the tree (Fig. 5). and are allowed to insert or remove nodes at any “leaf" node. To facilitate this design we add a descriptor pointer to each node in the stack. At the start of each operation, a thread creates a descriptor object with all the details necessary for an arbitrary thread to carry out the intended operation.

1:Struct Node
2:T v;
3:Op op;
4:Node * nexts[];
5:Node * prev;
6:
7:
8:Struct Desc
9:T v;
10:Op op;
11:bool active;
12:
Algorithm 3 Stack: Definitions

Algorithm 3 contains type definitions for the QStack. contains the elements , , and . represents the abstract type being stored in the data structure. identifies the node as either a pushed value or and unsatisfied pop operation. is an array holding references to the children of the node, while contains a reference to its parent. contains the same and variables, as well as . designated whether the associated operation for the descriptor object is currently pending, or if the thread performing that operation has completed it. The stack data structure has a global array , which contains all leaf nodes in the tree.

In order to conserve unsatisfied pops, we generalize the behaviour of and operations with and . If a is made on an empty stack, we instead begin a stack of waiting operations by calling and designating the inserted node as an unfulfilled operation. Similarly, if we call on a stack that contains unsatisfied pops, we instead use to eliminate an unsatisfied operation, which then finally returns the value provided by the incoming .

1:function Insert(