File: ConcurrencyExclusivity.inc

package info (click to toggle)
swiftlang 6.0.3-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 2,519,992 kB
  • sloc: cpp: 9,107,863; ansic: 2,040,022; asm: 1,135,751; python: 296,500; objc: 82,456; f90: 60,502; lisp: 34,951; pascal: 19,946; sh: 18,133; perl: 7,482; ml: 4,937; javascript: 4,117; makefile: 3,840; awk: 3,535; xml: 914; fortran: 619; cs: 573; ruby: 573
file content (450 lines) | stat: -rw-r--r-- 19,059 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
//===--- ConcurrencyExclusivity.cpp - Exclusivity tracking for concurrency-===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2021 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//
//
// This implements the runtime support for dynamically tracking exclusivity
// in tasks.
//
//===----------------------------------------------------------------------===//

namespace {

/// High Level Algorithm Description
/// --------------------------------
///
/// With the introduction of Concurrency, we add additional requirements to our
/// exclusivity model:
///
/// * We want tasks to have a consistent exclusivity access set across
///   suspensions/resumptions. This ensures that any exclusive accesses began
///   before a Task suspended are properly flagged after the Task is resumed
///   even if the Task is resumed on a different thread.
///
/// * If a synchronous code calls a subroutine that creates a set of tasks to
///   perform work and then blocks, we want the runtime to ensure that the tasks
///   respect exclusivity accesses from the outside synchronous code.
///
/// * We on purpose define exclusive access to the memory from multiple tasks as
///   undefined behavior since that would be an additional feature that needs to
///   be specifically designed in the future.
///
/// * We assume that an access in synchronous code will never be ended in
///   asynchronous code.
///
/// * We additional require that our design leaves the exclusivity runtime
///   unaware of any work we are doing here. All it should be aware of is the
///   current thread local access set and adding/removing from that access set.
///
/// We implement these requirements by reserving two pointers in each Task. The
/// first pointer points at the head access of the linked list of accesses of
/// the Task and the second pointer points at the end of the linked list of
/// accesses of the task. We will for the discussion ahead call the first
/// pointer TaskFirstAccess and the second TaskLastAccess. This allows us to
/// modify the current TLV single linked list to include/remove the tasks’s
/// access by updating a few nodes in the linked list when the task is running
/// and serialize the task’s current access set and restoring to be head the
/// original synchronous access set head when the task is running. This
/// naturally fits a push/pop access set sort of schema where every time a task
/// starts, we push its access set onto the local TLV and then pop it off when
/// the task is suspended. This ensures that the task gets the current
/// synchronous set of accesses and other Tasks do not see the accesses of the
/// specific task providing task isolation.
///
/// The cases can be described via the following table:
///
/// +------+--------------------+--------------------+--------------------+
/// | Case | Live Task Accesses | Live Sync Accesses | Live Task Accesses |
/// |      | When Push          | When Push          | When Pop           |
/// |------+--------------------+--------------------+--------------------|
/// |    1 | F                  | F                  | F                  |
/// |    2 | F                  | F                  | T                  |
/// |    3 | F                  | T                  | F                  |
/// |    4 | F                  | T                  | T                  |
/// |    5 | T                  | F                  | F                  |
/// |    6 | T                  | F                  | T                  |
/// |    7 | T                  | T                  | F                  |
/// |    8 | T                  | T                  | T                  |
/// +------+--------------------+--------------------+--------------------+
///
/// We mark the end of each title below introducing a case with 3 T/F to enable
/// easy visual matching with the chart
///
/// Case 1: Task/Sync do not have initial accesses and no Task accesses are
/// created while running (F,F,F)
///
/// In this case, TBegin and TEnd are both initially nullptr.
///
/// When we push, we see that the current exclusivity TLV has a null head and
/// leave it so. We set TBegin and TEnd as nullptr while running.
///
/// When we pop, see that the exclusivity TLV is still nullptr, so we just leave
/// TBegin and TEnd alone still as nullptr.
///
/// This means that code that does not have any exclusive accesses do not have
/// any runtime impact.
///
/// Case 2: Task/Sync do not have initial access, but Task accesses are created
/// while running (F, F, T)
///
/// In this case, TBegin and TEnd are both initially nullptr.
///
/// When we push, we see that the current exclusivity TLV has a null head. So,
/// we leave TBegin and TEnd as nullptr while the task is running.
///
/// When we pop, we see that the exclusivity TLV has a non-null head. In that
/// case, we walk the list to find the last node and update TBegin to point at
/// the current head, TEnd to point at that last node, and then set the TLV head
/// to be nullptr.
///
/// Case 3: Task does not have initial accesses, but Sync does, and new Task
/// accesses are not created while running (F, T, F)
///
/// In this case, TBegin and TEnd are both initially nullptr.
///
/// When we push, we look at the TLV and see our initial synchronous thread was
/// tracking accesses. In this case, we leave the TLV pointing at the
/// SyncAccessHead and set TBegin to SyncAccessHead and leave TEnd as nullptr.
///
/// When we pop, we see that TBegin (which we know has the old synchronous head
/// in it) is equal to the TLV so we know that we did not create any new Task
/// accesses. Then we set TBegin to nullptr and return. NOTE:  TEnd is nullptr
/// the entire time in this scenario.
///
/// Case 4: Task does not have initial accesses, but Sync does, and new Task
/// accesses are created while running (F, T, T)
///
/// In this case, TBegin and TEnd are both initially nullptr. When we push, we
/// look at the TLV and we see our initial synchronous thread was tracking
/// accesses. In this case, we leave the TLV pointing at the SyncAccessHead and
/// set TBegin to SyncAccessHead and leave TEnd as nullptr.
///
/// When we pop, we see that the TLV and TBegin differ now. We know that this
/// means that our task introduced new accesses. So, we search down from the
/// head of the AccessSet TLV until we find TBegin . The node before TBegin is
/// our new TEnd pointer. We set TBegin to then have the value of head, TEnd to
/// be the new TEnd pointer, set TEnd’s next to be nullptr and make head the old
/// value of TBegin.
///
/// Case 5: Task has an initial access set, but Sync does not have initial
/// accesses and no Task accesses exist after running (T,F,F)
///
/// In this case, TBegin and TEnd are both initially set to non-null values.
/// When we push, we look at the current TLV head and see that the TLV head is
/// nullptr. We then set TLV head to be TBegin and set TBegin to be nullptr to
/// signal the original synchronous TLV head was nullptr.
///
/// When we pop, we see that TBegin is currently nullptr, so we know the
/// synchronous access set was empty. We also know that despite us starting with
/// a task access set, those accesses must have completed while the task was
/// running since the access set is empty when we pop.
///
/// Case 6: Task has initial accesses, sync does not have initial access, and
/// Task access set is modified while running (T, F, T)
///
/// In this case, TBegin and TEnd are both initially set to non-null
/// values. When we push, we look at the current TLV head and see that the TLV
/// head is nullptr. We then set TLV head to be TBegin and set TBegin to be
/// nullptr to signal the original synchronous TLV head was nullptr. We have no
/// requirement on TEnd now in this case but set it to nullptr, to track flags
/// if we want to in the future in a different runtime.
///
/// When we pop, we see that TBegin is currently nullptr, so we know the
/// synchronous access set was empty. We do not have a way to know how/if we
/// modified the Task AccessSet, so we walked the list to find the last node. We
/// then make TBegin head, TEnd the last node, and set the TLV to be nullptr
/// again.
///
/// Case 7: Task has initial accesses, Sync has initial accesses, and new Task
/// accesses are not created while running (T, T, F)
///
/// In this case, TBegin and TEnd are both initially set to non-null values.
/// When we push, we look at the current TLV head and see that the TLV head is a
/// valid pointer. We then set TLV head to be the current value of TBegin, make
/// TEnd->next the old head value and stash the old head value into TBegin. We
/// have no requirement on TEnd now in this case.
///
/// When we pop, we see that TBegin is not nullptr, so we know the synchronous
/// access set had live accesses. We do not have a way to know how/if we
/// modified the Task AccessSet, so we walked the list to find TBegin (which is
/// old sync head).  Noting that the predecessor node of old sync head’s node
/// will be the end of the task’s current access set, we set TLV to point at the
/// node we found in TBegin, set TBegin to the current TLV head, set TEnd to
/// that predecessor node of the current TLV head and set TEnd->next to be
/// nullptr.
///
/// Case 8: Task has initial accesses, Sync does, and Task accesses is modified
/// while running (T, T, T)
///
/// In this case, TBegin and TEnd are both initially set to non-null values.
///
/// When we push, we look at the current TLV head and see that the TLV head is
/// a valid pointer. We then set TLV head to be the current value of TBegin,
/// make TEnd->next the old head value and stash the old head value into
/// TBegin. We have no requirement on TEnd now in this case.
///
/// When we pop, we see that TBegin is not nullptr, so we know the synchronous
/// access set had live accesses. We do not have a way to know how/if we
/// modified the Task AccessSet, so we walked the list to find TBegin (which is
/// old sync head).  Noting that the predecessor node of old sync head’s node
/// will be the end of the task’s current access set, we set TLV to point at
/// the node we found in TBegin, set TBegin to the current TLV head, set TEnd
/// to that predecessor node of the current TLV head and set TEnd->next to be
/// nullptr.
struct SwiftTaskThreadLocalContext {
  uintptr_t state[2];

#ifndef NDEBUG
  void dump() {
    fprintf(stderr,
            "        SwiftTaskThreadLocalContext: (FirstAccess,LastAccess): "
            "(%p, %p)\n",
            (void *)state[0], (void *)state[1]);
  }
#endif

  bool hasInitialAccessSet() const {
    // If state[0] is nullptr, we have an initial access set.
    return bool(state[0]);
  }

  Access *getTaskAccessSetHead() const {
    return reinterpret_cast<Access *>(state[0]);
  }

  Access *getTaskAccessSetTail() const {
    return reinterpret_cast<Access *>(state[1]);
  }

  void setTaskAccessSetHead(Access *newHead) { state[0] = uintptr_t(newHead); }

  void setTaskAccessSetTail(Access *newTail) { state[1] = uintptr_t(newTail); }

#ifndef NDEBUG
  const char *getTaskAddress() const {
    // Constant only used when we have an asserts compiler so that we can output
    // exactly the header location of the task for FileCheck purposes.
    //
    // WARNING: This test will fail if the Task ABI changes. When that happens,
    // update the offset!
    //
    // TODO: This probably will need 32 bit help.
#if __POINTER_WIDTH__ == 64
    unsigned taskHeadOffsetFromTaskAccessSet = 128;
#else
    unsigned taskHeadOffsetFromTaskAccessSet = 68;
#endif
    auto *self = reinterpret_cast<const char *>(this);
    return self - taskHeadOffsetFromTaskAccessSet;
  }
#endif
};

} // end anonymous namespace

// See algorithm description on SwiftTaskThreadLocalContext.
void swift::swift_task_enterThreadLocalContext(char *state) {
  auto &taskCtx = *reinterpret_cast<SwiftTaskThreadLocalContext *>(state);
  auto &tlsCtxAccessSet = SwiftTLSContext::get().accessSet;

#ifndef NDEBUG
  if (isExclusivityLoggingEnabled()) {
    withLoggingLock([&]() {
      fprintf(stderr,
              "Entering Thread Local Context. Before Swizzle. Task: %p\n",
              taskCtx.getTaskAddress());
      taskCtx.dump();
      swift_dumpTrackedAccesses();
    });
  }

  auto logEndState = [&] {
    if (isExclusivityLoggingEnabled()) {
      withLoggingLock([&]() {
        fprintf(stderr,
                "Entering Thread Local Context. After Swizzle. Task: %p\n",
                taskCtx.getTaskAddress());
        taskCtx.dump();
        swift_dumpTrackedAccesses();
      });
    }
  };
#else
  // Just a no-op that should inline away.
  auto logEndState = [] {};
#endif

  // First handle all of the cases where our task does not start without an
  // initial access set.
  //
  // Handles push cases 1-4.
  if (!taskCtx.hasInitialAccessSet()) {
    // In this case, the current synchronous context is not tracking any
    // accesses. So the tlsCtx and our initial access set are all nullptr, so we
    // can just return early.
    //
    // Handles push cases 1-2.
    if (!tlsCtxAccessSet) {
      logEndState();
      return;
    }

    // Ok, our task isn't tracking any task specific accesses, but our tlsCtx
    // was tracking accesses. Leave the tlsCtx alone at this point and set our
    // state's begin access to be tlsCtx head. We leave our access set tail as
    // nullptr.
    //
    // Handles push cases 3-4.
    taskCtx.setTaskAccessSetHead(tlsCtxAccessSet.getHead());
    logEndState();
    return;
  }

  // At this point, we know that we did have an initial access set. Both access
  // set pointers are valid.
  //
  // Handles push cases 5-8.

  // Now check if our synchronous code had any accesses. If not, we set TBegin,
  // TEnd to be nullptr and set the tlsCtx to point to TBegin.
  //
  // Handles push cases 5-6.
  if (!bool(tlsCtxAccessSet)) {
    tlsCtxAccessSet = taskCtx.getTaskAccessSetHead();
    taskCtx.setTaskAccessSetHead(nullptr);
    taskCtx.setTaskAccessSetTail(nullptr);
    logEndState();
    return;
  }

  // In this final case, we found that our task had its own access set and our
  // tlsCtx did as well. So we then set the Task's head to be the new TLV head,
  // set tail->next to point at old head and stash oldhead into the task ctx.
  //
  // Handles push cases 7-8.
  auto *oldHead = tlsCtxAccessSet.getHead();
  auto *tail = taskCtx.getTaskAccessSetTail();

  tlsCtxAccessSet.setHead(taskCtx.getTaskAccessSetHead());
  tail->setNext(oldHead);
  taskCtx.setTaskAccessSetHead(oldHead);
  taskCtx.setTaskAccessSetTail(nullptr);
  logEndState();
}

// See algorithm description on SwiftTaskThreadLocalContext.
void swift::swift_task_exitThreadLocalContext(char *state) {
  auto &taskCtx = *reinterpret_cast<SwiftTaskThreadLocalContext *>(state);
  auto &tlsCtxAccessSet = SwiftTLSContext::get().accessSet;

#ifndef NDEBUG
  if (isExclusivityLoggingEnabled()) {
    withLoggingLock([&]() {
      fprintf(stderr,
              "Exiting Thread Local Context. Before Swizzle. Task: %p\n",
              taskCtx.getTaskAddress());
      taskCtx.dump();
      swift_dumpTrackedAccesses();
    });
  }

  auto logEndState = [&] {
    if (isExclusivityLoggingEnabled()) {
      withLoggingLock([&]() {
        fprintf(stderr,
                "Exiting Thread Local Context. After Swizzle. Task: %p\n",
                taskCtx.getTaskAddress());
        taskCtx.dump();
        swift_dumpTrackedAccesses();
      });
    }
  };
#else
  // If we are not compiling with asserts, just use a simple identity function
  // that should be inlined away.
  //
  // TODO: Can we use defer in the runtime?
  auto logEndState = [] {};
#endif

  // First check our ctx to see if we were tracking a previous synchronous
  // head. If we don't then we know that our synchronous thread was not
  // initially tracking any accesses.
  //
  // Handles pop cases 1,2,5,6
  Access *oldHead = taskCtx.getTaskAccessSetHead();
  if (!oldHead) {
    // Then check if we are currently tracking an access set in the TLS. If we
    // aren't, then we know that either we did not start with a task specific
    // access set /or/ we did start but all of those accesses ended while the
    // task was running. In either case, when we pushed initially, we set
    // TBegin, TEnd to be nullptr already and since oldHead is already nullptr,
    // we can just exit.
    //
    // Handles pop cases 1,5
    if (!tlsCtxAccessSet) {
      assert(taskCtx.getTaskAccessSetTail() == nullptr &&
             "Make sure we set this to nullptr when we pushed");
      logEndState();
      return;
    }

    // In this case, we did find that we had live accesses. Since we know we
    // did not start with any synchronous accesses, these accesses must all be
    // from the given task. So, we first find the tail of the current TLS linked
    // list, then set the Task access set head to accessSet, the Task accessSet
    // tail to the TLS linked list tail and set tlsCtx.accessSet to nullptr.
    //
    // Handles pop cases 2,6
    auto *newHead = tlsCtxAccessSet.getHead();
    auto *newTail = tlsCtxAccessSet.getTail();
    assert(newTail && "Failed to find tail?!");
    tlsCtxAccessSet = nullptr;
    taskCtx.setTaskAccessSetHead(newHead);
    taskCtx.setTaskAccessSetTail(newTail);
    logEndState();
    return;
  }

  // Otherwise, we know that we /were/ tracking accesses from a previous
  // synchronous context. So we need to unmerge our task specific state from the
  // exclusivity access set.
  //
  // Handles pop cases 3,4,7,8.

  // First check if the current head tlsAccess is the same as our oldHead. In
  // such a case, we do not have new task accesses to update. So just set task
  // access head/tail to nullptr. The end access should be nullptr.
  //
  // Handles pop cases 3.
  if (tlsCtxAccessSet.getHead() == oldHead) {
    taskCtx.setTaskAccessSetHead(nullptr);
    taskCtx.setTaskAccessSetTail(nullptr);
    logEndState();
    return;
  }

  // Otherwise, we have task specific accesses that we need to serialize into
  // the task's state. We currently can not tell if the Task actually modified
  // the task list beyond if the task list is empty. So we have to handle case 7
  // here (unfortunately).
  //
  // NOTE: If we could tell if the Task modified its access set while running,
  // we could perhaps avoid the search for newEnd.
  //
  // Handles pop cases 4,7,8.
  auto *newHead = tlsCtxAccessSet.getHead();
  auto *newEnd = tlsCtxAccessSet.findParentAccess(oldHead);
  tlsCtxAccessSet.setHead(oldHead);
  newEnd->setNext(nullptr);
  taskCtx.setTaskAccessSetHead(newHead);
  taskCtx.setTaskAccessSetTail(newEnd);
  logEndState();
}