• Keine Ergebnisse gefunden

Non-blocking Data Structures 2

2.3 Manual Memory Reclamation

2.3.3 Hazard Pointers

The hazard pointer (HP) [Michael 2002b] method provides a protection mechanism for individual objects. Protections signal that an object is still in use and that its deletion should be deferred.

To be precise, HP guarantees thatthe deletion of an object is deferred if it has been continuously protected since before it was retired[Gotsman et al. 2013]. Figure 2.8 gives a simplified version of the HP implementation due to Michael [2004]. The implementation equips every thread with a fixed number of single-writer multiple-reader pointers, the eponymous hazard pointers. We refer to the𝑖-th hazard pointer of thread𝑡byhp𝑡[𝑖]and may drop the thread subscript if clear

3The literature may deem memory reclamation schemes blocking if they allow for an unbounded number of objects awaiting reclamation [Balmau et al. 2016]. The reason for this is that an unbounded backlog of reclamation is assumed to result in the system running out of memory, making subsequent allocations block/wait until memory is reclaimed. The issue is irrelevant for the present thesis: we do not suggest how to construct non-blocking data structures but verify the correctness of existing ones. EBR remains an essential technique as many SMR algorithms are derived from it or are drop-in replacements for it (cf. Chapter 9).

20 Chapter 2 Non-blocking Data Structures

Figure 2.8:A simplified version of the hazard pointer (HP) implementation by Michael [2004]

for a placeholder typeTandKhazard pointers per thread. The implementation supports dynamic thread joining and parting.

177 void protect(T* ptr, int index) { 178 assert(0 <= index < K);

186 void retire(T* ptr, int index) { 187 myHP->retired.push(ptr);

201 for (T* ptr : myHP->retired) { 202 if (defer.contains(ptr)) continue;

203 myHP->retired.remove(ptr);

204 delete ptr;

205 } }

from the context. Protections are issued by a call to functionprotect. It takes as parameters an object and a hazard pointer index, and simply writes the object to the hazard pointer with the given index, Line 179. Protections can be revoked byunprotectwhich takes a hazard pointer index and resets the corresponding hazard pointer by writingNULLto it, Line 183. Protections are respected as follows. Functionretirestores the passed object into a thread-local list of retired objects, Line 187. Moreover, it periodically tries to reclaim the objects from that list, Line 188.

To do so, it scans the hazard pointers of all threads, collecting all the objects that are currently protected, Lines 192 to 199. Then, a retired object is reclaimed if it is not in the list of protected objects, Lines 201 to 205. Similar to the EBR implementation from Section 2.3.2, HP supports dynamic thread joining and parting. Again, parting threads do not reclaim internalHpRecobjects.

Unlike for EBR, thread failures do not stop reclamation; failures may only prevent reclamation of objects protected by crashed thread.

Section 2.3 Manual Memory Reclamation 21

Figure 2.9:An adaption of the simple counter to reuse memory via HP. A single hazard pointer per thread is required. Modifications wrt. Figure 2.3 are marked in bold font.

206 structContainer {

Figure 2.9 presents a version of the simple counter from Figure 2.3 adapted to reclaim memory using HP with a single hazard pointer per thread. While the HP method is conceptually simple, it may be non-trivial to detect whether or not an object has been protected successfully, i.e., if an object has been protected before it was retired. In the counter implementation, we need to protectcurrbecause it is subsequently accessed. To that end, a protection is issued using hazard pointerhp[0]. At this point, we cannot guarantee that a dereference of curris safe. Between reading outcurrin Line 219 and protecting it in Line 220, an interfering thread might have updated the counter and retired the object referenced bycurr. Because the protection was not yet announced,currmight have already been reclaimed. Line 221 checks that this is not the case.

It does so by ensuring thatcurrcoincides with the sharedCounter. It is worth pointing out that this check relies on the invariant thatCounteris never retired. Only after both Lines 220 and 221 have been executed,currcan be accessed safely. As we will see in Section 2.4, this procedure for successfully protecting pointers is common in non-blocking data structures. Unfortunately, we will also see that there are data structures that are fundamentally incompatible with this procedure and the HP method in general [Brown 2015; Michael 2002b].

The alert reader readily realizes that the counter implementation using HP, more precisely the protection check from Line 221, is prone to the ABA problem. Indeed, as noted above the object referenced bycurrcould have been reclaimed. Consequently, it could have been reused and installed as the sharedCounteragain. Those scenarios are not problematic since we just ensure thatcurrcontains the current value of Counterand that is has been protected successfully. Put differently, Lines 219 to 221 appear as if they were executed atomically. Accesses to the content ofcurrhappen only later.

22 Chapter 2 Non-blocking Data Structures

Figure 2.10:Nodetype for singly-linked data structures. Member fieldsmarkandnextare stored consecutively in memory, so both fields can be modified with a single double-wordCAS.

229 struct Node { 230 int data;

231 bool mark;

232 Node* next;

233

234 Node(int value) { data = value; mark = false; next = NULL; } // constructor 235 }

Finally, we revisit the guarantee thatthe deletion of an object is deferred if it has been continuously protected since before it was retired. It is imperative to make precise the notion of continuous protections. A single hazard pointer’s protection is continuous. More involved data structures, however, use multiple hazard pointers to protect a single object [Michael 2002a]. A common pattern first issues a protection perhp[𝑖]and later, in order to reusehp[𝑖], issues a protection perhp[𝑖+1]and resetshp[𝑖]. We say that the protection istransferedfromhp[𝑖]tohp[𝑖+1].

The order is important [Michael 2002a]: a protection can be transferred fromhp[𝑖]tohp[𝑗]only if𝑖 < 𝑗. This is because of the scanning process from methodreclaim, Lines 192 to 199 in Fig-ure 2.8. It reads out hazard pointers in ascending order. Hence, protections can go unrecognized when attempting to transfer fromhp[𝑗]tohp[𝑖]with𝑖 < 𝑗. To see this, consider a thread𝑡 protecting an objectowithhp𝑡[1]. Assume that another thread𝑡executes functionreclaimup to the point where it scanshp𝑡[0]but nothp𝑡[1]. Now, let𝑡protectoperhp𝑡[0]and resethp𝑡[1].

Then,𝑡misses the protection ofo—it is not transferred fromhp𝑡[1]tohp𝑡[0]. Altogether, this means that a protection is continuous only if it is due to a single hazard pointer or due to transfers among multiple hazard pointers.