Skip to content

Commit 35aa862

Browse files
authored
Merge pull request #38607 from gottesmm/pr-922edd6308103f396aeedb86f4c86d8133c6ff9d
2 parents d52dca5 + 2edc32b commit 35aa862

File tree

4 files changed

+420
-918
lines changed

4 files changed

+420
-918
lines changed

test/Concurrency/Runtime/exclusivity.swift

Lines changed: 299 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,18 @@ public func debugLog(_ s: String) {
4848
@available(SwiftStdlib 5.5, *)
4949
@main
5050
struct Runner {
51+
@MainActor
52+
@inline(never)
53+
static func withExclusiveAccessAsync<T, U>(to x: inout T, f: (inout T) async -> U) async -> U {
54+
await f(&x)
55+
}
56+
57+
@MainActor
58+
@inline(never)
59+
static func withExclusiveAccess<T, U>(to x: inout T, f: (inout T) -> U) -> U {
60+
f(&x)
61+
}
62+
5163
@inline(never)
5264
@MainActor
5365
static func doSomething() async { }
@@ -293,8 +305,8 @@ struct Runner {
293305
// First access begins here.
294306
await callCallee1()
295307
useGlobal(&global1) // We should not crash here since we cleaned up
296-
// the access in callCallee1 after we returned
297-
// from the await there.
308+
// the access in callCallee1 after we returned
309+
// from the await there.
298310
debugLog("==> Exit Main")
299311
}
300312

@@ -336,11 +348,294 @@ struct Runner {
336348
// First access begins here.
337349
await callCallee1()
338350
useGlobal(&global1) // We should not crash here since we cleaned up
339-
// the access in callCallee1 after we returned
340-
// from the await there.
351+
// the access in callCallee1 after we returned
352+
// from the await there.
341353
debugLog("==> Exit Main")
342354
}
343355

356+
// These are additional tests that used to be FileChecked but FileCheck
357+
// was too hard to use in a concurrent context.
358+
exclusivityTests.test("case1") { @MainActor in
359+
@inline(never)
360+
@Sendable func callee2(_ x: inout Int, _ y: inout Int, _ z: inout Int) -> Void {
361+
debugLog("==> Enter callee2")
362+
debugLog("==> Exit callee2")
363+
}
364+
365+
// We add an inline never here to make sure that we do not eliminate
366+
// the dynamic access after inlining.
367+
@MainActor
368+
@inline(never)
369+
func callee1() async -> () {
370+
debugLog("==> Enter callee1")
371+
let handle = Task { @MainActor in
372+
debugLog("==> Enter callee1 Closure")
373+
374+
// These accesses end before we await in the task.
375+
do {
376+
callee2(&global1, &global2, &global3)
377+
}
378+
let handle2 = Task { @MainActor in
379+
debugLog("==> Enter handle2!")
380+
debugLog("==> Exit handle2!")
381+
}
382+
await handle2.value
383+
debugLog("==> Exit callee1 Closure")
384+
}
385+
await handle.value
386+
debugLog("==> Exit callee1")
387+
}
388+
389+
debugLog("==> Enter 'testCase1'")
390+
await callee1()
391+
debugLog("==> Exit 'testCase1'")
392+
}
393+
394+
// Case 2: (F, F, T). In case 2, our task does not start with a live access
395+
// and nothing from the outside synchronous context, but does pop with a new
396+
// access.
397+
//
398+
// We use a suspend point and a withExclusiveAccessAsync(to:) to test this.
399+
exclusivityTests.test("case2.filecheck.nocrash") { @MainActor in
400+
debugLog("==> Enter 'testCase2'")
401+
402+
let handle = Task { @MainActor in
403+
debugLog("==> Inner Handle")
404+
await withExclusiveAccessAsync(to: &global1) { @MainActor (x: inout Int) async -> Void in
405+
let innerTaskHandle = Task { @MainActor in
406+
// Different task, shouldn't crash.
407+
withExclusiveAccess(to: &global1) { _ in
408+
debugLog("==> No crash!")
409+
}
410+
debugLog("==> End Inner Task Handle")
411+
}
412+
// This will cause us to serialize the access to global1. If
413+
// we had an access here, we would crash.
414+
await innerTaskHandle.value
415+
debugLog("==> After")
416+
}
417+
// Accessis over. We shouldn't crash here.
418+
withExclusiveAccess(to: &global1) { _ in
419+
debugLog("==> No crash!")
420+
}
421+
debugLog("==> Inner Handle: After exclusive access")
422+
}
423+
424+
await handle.value
425+
debugLog("==> After exclusive access")
426+
let handle2 = Task { @MainActor in
427+
debugLog("==> Enter handle2!")
428+
debugLog("==> Exit handle2!")
429+
}
430+
await handle2.value
431+
debugLog("==> Exit 'testCase2'")
432+
}
433+
434+
exclusivityTests.test("case2.filecheck.crash") { @MainActor in
435+
expectCrashLater(withMessage: "Fatal access conflict detected")
436+
debugLog("==> Enter 'testCase2'")
437+
438+
let handle = Task { @MainActor in
439+
debugLog("==> Inner Handle")
440+
await withExclusiveAccessAsync(to: &global1) { @MainActor (x: inout Int) async -> Void in
441+
let innerTaskHandle = Task { @MainActor in
442+
debugLog("==> End Inner Task Handle")
443+
}
444+
await innerTaskHandle.value
445+
// We will crash here if we properly brought back in the
446+
// access to global1 despite running code on a different
447+
// task.
448+
withExclusiveAccess(to: &global1) { _ in
449+
debugLog("==> Got a crash!")
450+
}
451+
debugLog("==> After")
452+
}
453+
debugLog("==> Inner Handle: After exclusive access")
454+
}
455+
456+
await handle.value
457+
debugLog("==> After exclusive access")
458+
let handle2 = Task { @MainActor in
459+
debugLog("==> Enter handle2!")
460+
debugLog("==> Exit handle2!")
461+
}
462+
await handle2.value
463+
debugLog("==> Exit 'testCase2'")
464+
}
465+
466+
// Case 5: (T,F,F). To test case 5, we use with exclusive access to to
467+
// create an exclusivity scope that goes over a suspension point. We are
468+
// interesting in the case where we return after the suspension point. That
469+
// push/pop is going to have our outer task bring in state and end it.
470+
//
471+
// CHECK-LABEL: ==> Enter 'testCase5'
472+
// CHECK: ==> Task: [[TASK:0x[0-9a-f]+]]
473+
// CHECK: Inserting new access: [[LLNODE:0x[a-z0-9]+]]
474+
// CHECK-NEXT: Tracking!
475+
// CHECK-NEXT: Access. Pointer: [[ACCESS:0x[a-z0-9]+]]
476+
// CHECK: Exiting Thread Local Context. Before Swizzle. Task: [[TASK]]
477+
// CHECK-NEXT: SwiftTaskThreadLocalContext: (FirstAccess,LastAccess): (0x0, 0x0)
478+
// CHECK-NEXT: Access. Pointer: [[ACCESS]]. PC:
479+
// CHECK: Exiting Thread Local Context. After Swizzle. Task: [[TASK]]
480+
// CHECK_NEXT: SwiftTaskThreadLocalContext: (FirstAccess,LastAccess): ([[LLNODE]], [[LLNODE]])
481+
// CHECK_NEXT: No Accesses.
482+
//
483+
// CHECK-NOT: Removing access:
484+
// CHECK: ==> End Inner Task Handle
485+
// CHECK: ==> After
486+
// CHECK: Removing access: [[LLNODE]]
487+
// CHECK: ==> After exclusive access
488+
// CHECK: Exiting Thread Local Context. Before Swizzle. Task: [[TASK]]
489+
// CHECK-NEXT: SwiftTaskThreadLocalContext: (FirstAccess,LastAccess): (0x0, 0x0)
490+
// CHECK-NEXT: No Accesses.
491+
// CHECK: Exiting Thread Local Context. After Swizzle. Task: [[TASK]]
492+
// CHECK-NEXT: SwiftTaskThreadLocalContext: (FirstAccess,LastAccess): (0x0, 0x0)
493+
// CHECK-NEXT: No Accesses.
494+
//
495+
// CHECK: ==> Exit 'testCase5'
496+
exclusivityTests.test("case5.filecheck") { @MainActor in
497+
debugLog("==> Enter 'testCase5'")
498+
499+
let outerHandle = Task { @MainActor in
500+
await withExclusiveAccessAsync(to: &global1) { @MainActor (x: inout Int) async -> Void in
501+
let innerTaskHandle = Task { @MainActor in
502+
debugLog("==> End Inner Task Handle")
503+
}
504+
await innerTaskHandle.value
505+
debugLog("==> After")
506+
}
507+
debugLog("==> After exclusive access")
508+
let handle2 = Task { @MainActor in
509+
debugLog("==> Enter handle2!")
510+
debugLog("==> Exit handle2!")
511+
}
512+
await handle2.value
513+
}
514+
await outerHandle.value
515+
debugLog("==> Exit 'testCase5'")
516+
}
517+
518+
exclusivityTests.test("case5.filecheck.crash") { @MainActor in
519+
expectCrashLater(withMessage: "Fatal access conflict detected")
520+
debugLog("==> Enter 'testCase5'")
521+
522+
let outerHandle = Task { @MainActor in
523+
await withExclusiveAccessAsync(to: &global1) { @MainActor (x: inout Int) async -> Void in
524+
let innerTaskHandle = Task { @MainActor in
525+
debugLog("==> End Inner Task Handle")
526+
}
527+
await innerTaskHandle.value
528+
debugLog("==> After")
529+
withExclusiveAccess(to: &global1) { _ in
530+
debugLog("==> Crash here")
531+
}
532+
}
533+
debugLog("==> After exclusive access")
534+
let handle2 = Task { @MainActor in
535+
debugLog("==> Enter handle2!")
536+
debugLog("==> Exit handle2!")
537+
}
538+
await handle2.value
539+
}
540+
await outerHandle.value
541+
debugLog("==> Exit 'testCase5'")
542+
}
543+
544+
// Case 6: (T, F, T). In case 6, our task starts with live accesses and is
545+
// popped with live accesses. There are no sync accesses.
546+
//
547+
// We test this by looking at the behavior of the runtime after we
548+
// finish executing handle2. In this case, we first check that things
549+
// just work normally and as a 2nd case perform a conflicting access to
550+
// make sure we crash.
551+
exclusivityTests.test("case6.filecheck") { @MainActor in
552+
let outerHandle = Task { @MainActor in
553+
let callee2 = { @MainActor (_ x: inout Int) -> Void in
554+
debugLog("==> Enter callee2")
555+
debugLog("==> Exit callee2")
556+
}
557+
558+
// We add an inline never here to make sure that we do not eliminate
559+
// the dynamic access after inlining.
560+
@MainActor
561+
@inline(never)
562+
func callee1(_ x: inout Int) async -> () {
563+
debugLog("==> Enter callee1")
564+
// This task is what prevents this example from crashing.
565+
let handle = Task { @MainActor in
566+
debugLog("==> Enter callee1 Closure")
567+
// Second access. Different Task so it is ok.
568+
await withExclusiveAccessAsync(to: &global1) {
569+
await callee2(&$0)
570+
}
571+
debugLog("==> Exit callee1 Closure")
572+
}
573+
await handle.value
574+
debugLog("==> callee1 after first await")
575+
// Force an await here so we can see that we properly swizzle.
576+
let handle2 = Task { @MainActor in
577+
debugLog("==> Enter handle2!")
578+
debugLog("==> Exit handle2!")
579+
}
580+
await handle2.value
581+
debugLog("==> Exit callee1")
582+
}
583+
584+
// First access begins here.
585+
await callee1(&global1)
586+
}
587+
debugLog("==> Enter 'testCase6'")
588+
await outerHandle.value
589+
debugLog("==> Exit 'testCase6'")
590+
}
591+
592+
exclusivityTests.test("case6.filecheck.crash") { @MainActor in
593+
expectCrashLater(withMessage: "Fatal access conflict detected")
594+
let outerHandle = Task { @MainActor in
595+
let callee2 = { @MainActor (_ x: inout Int) -> Void in
596+
debugLog("==> Enter callee2")
597+
debugLog("==> Exit callee2")
598+
}
599+
600+
// We add an inline never here to make sure that we do not eliminate
601+
// the dynamic access after inlining.
602+
@MainActor
603+
@inline(never)
604+
func callee1(_ x: inout Int) async -> () {
605+
debugLog("==> Enter callee1")
606+
// This task is what prevents this example from crashing.
607+
let handle = Task { @MainActor in
608+
debugLog("==> Enter callee1 Closure")
609+
// Second access. Different Task so it is ok.
610+
await withExclusiveAccessAsync(to: &global1) {
611+
await callee2(&$0)
612+
}
613+
debugLog("==> Exit callee1 Closure")
614+
}
615+
await handle.value
616+
debugLog("==> callee1 after first await")
617+
// Force an await here so we can see that we properly swizzle.
618+
let handle2 = Task { @MainActor in
619+
debugLog("==> Enter handle2!")
620+
debugLog("==> Exit handle2!")
621+
}
622+
await handle2.value
623+
// Make sure we brought back in the access to x so we crash
624+
// here.
625+
withExclusiveAccess(to: &global1) { _ in
626+
debugLog("==> Will crash here!")
627+
}
628+
debugLog("==> Exit callee1")
629+
}
630+
631+
// First access begins here.
632+
await callee1(&global1)
633+
}
634+
debugLog("==> Enter 'testCase6'")
635+
await outerHandle.value
636+
debugLog("==> Exit 'testCase6'")
637+
}
638+
344639
await runAllTestsAsync()
345640
}
346641
}

0 commit comments

Comments
 (0)