• Keine Ergebnisse gefunden

Ownership and Reclamation 6

6.1 Reclamation breaks Ownership

Thread-modular analyses [Berdine et al. 2008; Jones 1983; Owicki and Gries 1976] verify each thread individually. On the one hand, this yields an efficient analysis for programs with a fixed number of threads as it avoids an explicit cross-product of all threads. On the other hand, it makes verification for an arbitrary number of threads possible. The downside of thread-modularity is its imprecision in computing thread interferences. Since threads are verified individually, the relation among thread-local information gets lost. We discuss this problem and why its common solution does not apply for manual memory management.

As we have seen in Chapter 4, thread-modular analyses abstract program configurations into sets of views which capture a thread’s perception of the configuration. To compute the effect that an interfering thread has on a victim thread, the views for the two threads are combined, the interfering thread takes a step in the resulting view, which is then projected to the victim thread. Combining two views is more problematic than one might think. As already noted, views

Section 6.1 Reclamation breaks Ownership 55

abstract away the relation among the interfering and victim threads. For an analysis to be sound, it has to consider all possible relations among those two threads. This introduces imprecision and may ultimately lead to false alarms. We illustrate the problem on an example.

Example 6.1.Consider the views from Figure 6.2 which arise during a thread-modular analysis of Michael&Scott’s queue. The threads captured by views𝜐1and𝜐2from Figure 6.2a are𝑡1and𝑡2, respectively. Thread𝑡1is executingenqueue. It has already allocated a new node𝑏, referenced by its local pointer variable𝑡1node, and is about to execute theCASfrom Line 315 in order to insert the new node afterTail. Thread𝑡2is executingdequeue. It has removed node𝑐, referenced by𝑡2head. Its next step is to retire𝑐.1

Let us consider the interference𝑡1is exposed to due to the actions of𝑡2. The goal is to compute a new view for𝑡1which captures the effect of𝑡2performing the insertion. To that end, we combine the two views from Figure 6.2a. The result is given in Figure 6.2b. View𝜐3 is the expected one:𝑡1nodepoints to𝑏and𝑡2headpoints to𝑐with𝑏≠𝑐—the threads hold pointers to distinct nodes. In𝜐4, however, both threads alias the exact same node. Although peculiar, we have to consider view𝜐4as well to guarantee soundness of the overall analysis, that is, guarantee that all possible views are explored. Indeed, just from inspecting𝜐1and𝜐2we cannot conclude that the memory layout of𝜐4 is spurious in the sense that it does not occur in any execution of Michael&Scott’s queue. Unfortunately, the spurious view will lead to a false alarm.

To see why view𝜐4is problematic, we continue to compute the inference. To that end, we let𝑡2 execute its next command in𝜐4. The result is view𝜐4 from Figure 6.2c. In𝜐4, node𝑏has been retired. Here, we assume that the retirement is followed immediately by afree(𝑏). Next, we project away thread𝑡2 from𝜐4 and let𝑡1execute its next command. In the resulting view,𝜐′′4 from Figure 6.2c, the deleted node𝑏has become the newTail, breaking the shape invariant of the queue. Subsequentenqueueoperations can now reallocate𝑏and update it. The updates lead to unintended updates of the overall queue, changing the queue’s content or losing elements if the next field of𝑏is modified. This constitutes a linearizability violation, verification fails.

A well-known and common technique to avoid such spurious views during thread-modular analyses isownershipreasoning [Castegren and Wrigstad 2017; Dietl and Müller 2013; Gotsman et al. 2007; O’Hearn 2004; Vafeiadis and Parkinson 2007]. The allocation of a new node grants the allocating thread ownership over the new node. Ownership is removed as soon as the new node is published, that is, made accessible to other threads. Typically, this happens when a pointer to a node is written to a shared pointer variable or to a pointer field of another node that is reachable from the shared variables. (We refrain from a formal definition of ownership at this point.) Then, we exploit ownership to avoid spurious views and increase the precision

1The attentive reader of Chapter 2 might observe that, unlike presented in Figure 6.2 here, the next field𝑐 .next

of the removed but not yet retired node𝑐is neverNULLin Michael&Scott’s queue. To obtain such a view where𝑐 .nextisNULL, we require a preceding interference step which suffers from the same imprecision as the interference step presented here. For simplicity, we stick with𝑐 .nextbeingNULLin this example.

56 Chapter 6 Ownership and Reclamation

Figure 6.2:Views encountered during a thread-modular analysis of Michael&Scott’s queue.

Imprecision in interference steps leads to spurious verification failure.

(a)Two views the interference among which is computed. View𝜐1captures thread𝑡1which has allocated a new node𝑏and is about to append it to theTailof the queue via theCASfrom Line 315.

View𝜐2captures thread𝑡2which has removed node𝑐from the queue and is about to retire it, Line 339.

⋯ 𝑎

(b)Possible combinations of views𝜐1and𝜐2. Judging from the view abstraction alone, a vanilla thread-modular analysis cannot know whether nodes𝑏and𝑐coincide in the actual program configuration the views abstract from. Interference has to consider both𝜐3and𝜐4, although𝜐4spurious.

⋯ 𝑎

(c)Continuing the interference computation for𝜐4, we let thread𝑡2take a step and retire𝑏, Line 339.

The result is𝜐4where the retirement of𝑏has immediately freed it (marked with †). Next, we project away𝑡2and let𝑡1execute Line 315. The result is𝜐′′4where the freed node𝑏has been inserted into the queue. Subsequent reallocations of𝑏may thus change the queue’s content unknowingly, leading to verification failure. Note that the verification failure is spurious since it is a result of the spurious𝜐4.

⋯ 𝑎

of thread-modular analyses. To that end, we extend views to track ownership information and prevent combinations of views where an owned node is referenced by another non-owning thread. That is, we ensure that the access exclusivity granted by ownership is respected. For the above example, this means that thread𝑡2cannot have a pointer to𝑏. Hence, we can rule out view𝜐4as a combination of𝜐1and𝜐2because𝑏=𝑐is guarantee to be no longer possible.

While ownership reasoning is elegantly simple and yet effective, we cannot use it in our setting.

The above approach is sound only under garbage collection, when nodes are neither reused nor reclaimed, but unsound otherwise. We demonstrate this with an example. Thereafter, in Section 6.2, we introduce a new variant of ownership that applies to our setting.

Example 6.3.To see why traditional ownership reasoning breaks when memory is reclaimed and reused, consider the configurations of Micheal&Scott’s queue with hazard pointers depicted in Figure 6.4a. Configurationcfgcorresponds to the scenario where node𝑏used to be theTailof the queue, however, it has subsequently been removed from the queue, reclaimed, and reallocated.

The reallocating thread and owner of𝑏is𝑡1. Thread𝑡2started its operation while𝑏was still theTailand acquired a pointer to it,𝑡2tail. Node𝑏was removed and reallocated before𝑡2 could protect it. Hence,𝑡2tailis a dangling pointer to the now𝑡1-owned𝑏. The view abstraction ofcfgis the expected one: views𝜐1and𝜐2from Figure 6.4b. To make explicit that we lose any relation among threads in the abstraction, we renamed node𝑏to𝑐in𝜐2(in practice, memory abstractions are unlikely to maintain the addresses explicitly [Chang et al. 2020]).

If we let thread𝑡1continue its execution incfg, it appends𝑏to the end of the queue and then swingsTailto the newly added node. The result iscfgfrom Figure 6.4a. For an analysis to be sound, interference has to produce from𝜐1and𝜐2a new view for𝑡2that captures the effect of𝑡1’s actions. The first step of interference is to combine𝜐1 and𝜐2. Intuitively, we expect the combined view to correspond tocfg. The fact that𝑡1owns𝑏, however, makes traditional ownership reasoning ignore the relevant case𝑏 = 𝑐. That is,𝜐1 and𝜐2 do not producecfg although they were obtained from it. Consequently, it is not guaranteed that a view for𝑡2 is explored which reflectscfg. This compromises soundness.

It is worth pointing out that under GC the same problem does not arise. Node𝑏would have not

been reclaimed due to𝑡2tailpointing to it.

To overcome the problem of an unsound analysis, we introduce a variant of ownership that allows for both soundness and precision in the presence of memory reclamation and reuse.

58 Chapter 6 Ownership and Reclamation

Figure 6.4:Reallocation scenario from Michael&Scott’s queue with hazard pointers where traditional ownership reasoning, as done under garbage collection, is unsound.

(a)Program configurations without view abstraction. Incfg, thread𝑡1owns node𝑏while𝑡2holds a dangling pointer to it. The scenario arises if𝑏is reclaimed and subsequently reallocated in-between𝑡2 acquiring and protecting pointer𝑡2tail. Next,𝑡1inserts𝑏into the queue. The result iscfg.

owned(𝑡1)

⋯ 𝑎

Tail

𝑏 𝑡1node

𝑡2tail

Configuration cfg 𝑡1∶pc=Line 315 𝑡2∶pc=Line 308

⋯ 𝑎

Tail

𝑏 𝑡1node

𝑡2tail

Configuration cfg 𝑡1∶pc=Line 317 𝑡2∶pc=Line 308

(b)View abstraction forcfggives views𝜐1and𝜐2. Applying traditional ownership reasoning prevents a combined view where nodes𝑏and𝑐coincide, because𝑏is owned. Hence, configurationcfgis not guaranteed to be covered by any view. The analysis is unsound.

owned(𝑡1)

⋯ 𝑎

Tail

𝑏 𝑡1node

View𝜐1 𝑡1∶pc=Line 315

⋯ 𝑎

Tail

𝑐 𝑡2tail

View𝜐2 𝑡2∶pc=Line 308

Section 6.1 Reclamation breaks Ownership 59