Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SR-6604] Are Array writes race free in case of copying an array? #49154

Closed
aschwaighofer opened this issue Dec 13, 2017 · 9 comments
Closed
Labels
bug A deviation from expected or documented behavior. Also: expected but undesirable behavior.

Comments

@aschwaighofer
Copy link
Member

Previous ID SR-6604
Radar None
Original Reporter @aschwaighofer
Type Bug
Status Closed
Resolution Done
Additional Detail from JIRA
Votes 0
Component/s
Labels Bug
Assignee None
Priority Medium

md5: a0c8b7713ccdc6ab175a31af44e161a6

relates to:

  • SR-6543 Data race when passing copies of values across threads.

Issue Description:

I have heard people say that they can rely on copy-on-write behavior of Array to guarantee race-freedom when working with arrays in certain situations (the array is copied). I do not believe this to be true in today's Array implementation. Here is my reasoning.

I am going to use c++'s memory model and assume that:

  • strong_retain has acquire semantics on the reference

  • is_unique has acquire semantics on the reference

  • strong_release has release semantics on the reference

(Swift's reference counting implementation actually uses more the more relaxed consume semantics but for the discussion this modeling is good enough)

Here is my example that shows a race even though we have created a copy of the array.

func thread1() {
   var g = [0]
   g[0] = 1
   var copy = g
   queue.async {
     copy[0] = 2
   } 
}

Here is the code that will get executed on thread 1:

Thread 1:
  is_unique(g.arr_ref)             // acquire
  write(g.arr_ref)
  copy = strong_retain(g.arr_ref)  // acquire

Here is the code that will get executed on dispatch thread:

Thread 2:
 is_unique(copy.arr_ref)           // acquire, returns false because refcount=2 so we create the copied array
 read(copy.arr_ref)

Note, that there is no release-acquire handshake that establishes a happens-before relationship between thread1's write to the array and thread 2's is_unique check. So the following racing interleaving is allowed:

Thread 2:
is_unique(g.arr_ref) // acquire
copy = strong_retain(g.arr_ref) //acquire
is_unique(copy.arr_ref) // acquire
write(g.arr_ref) || read(copy.arr_ref) // race!

I don't believe this race to be a problem because there is a race at the language level on the captured copy. If I am wrong on this point we have a problem.

var copy = ...
{ capture copy}

races with

{
   copy[0] =
}

But I don't believe that we can say the programmer can rely on race freedom because arrays are copy-on-write. If we wanted this property we would have to insert a release memory fence after the array mutation.

To clarify I am not talking about the situation where the copy was created under synchronization and then we have two writes to distinct values. This case is of course okay.

var globalA = []
var globalB = []

func thread1() {
   var globalA = [1]
   var globalB = globalA
   fence()
   queue.dispatch {
       globalA[0] = 1
   }
   globalB[0] = 2
}
@belkadan
Copy link
Contributor

It is part of the language model that this is safe, so clearly we have to get it right. See also SR-6543.

@aschwaighofer
Copy link
Member Author

Then we have to fix the implementation of array and insert a release fence (or a release memory operation on the reference count).

@aschwaighofer
Copy link
Member Author

Note that if queue.dispatch has the fence semantics then we are fine. But I am not assuming this because then we would not be relying on array to give us the race freedom. What I am asking is that assume that dispatch does not have fence semantics: is there a language level race or not? If there is no language level race then we have a problem.

@atrick
Copy link
Member

atrick commented Dec 13, 2017

The programmer can rely on race freedom on the array buffer because it is semantically part of the array's value. An array has the same semantics as any struct w.r.t. races.

queue.async would need to have a memory fence, otherwise you wouldn't be able to pass any (by-value) data into the closure.

@belkadan
Copy link
Contributor

Arnold and I discussed this some in person, and I think we boiled it down to these three examples:

func test() {
  var copy = 0
  spawnThreadWithoutBarrier {
    waitForConditionVariableWithoutBarrier()
    print(copy) // clearly a race
  }
  copy = 5
  signalConditionVariableWithoutBarrier()
}

func test() {
  var g = [0]
  g[0] = 1
  let copy = g
  spawnThreadWithoutBarrier {
    waitForConditionVariableWithoutBarrier()
    print(copy[0]) // race on [0], but not just that (see below)
  }
  signalConditionVariableWithoutBarrier()
}

func test() {
  var g = 42
  var copy = g // This allocates a box and then writes into it
  spawnThreadWithoutBarrier {
    waitForConditionVariableWithoutBarrier()
    print(copy) // STILL A RACE on the storage for 'copy' - may observe it uninitialized!
  }
  signalConditionVariableWithoutBarrier()
}

That is, this isn't about Array specifically; it's about closure captures. The situation does get worse with CoW types (i.e. the second example has two races), but that's not the fundamental scaryness here. However, the point about DispatchQueue.async providing a memory barrier anyway is a good one—most concurrency utilities that operate in terms of closures probably won't run into this in practice.

(cc @devincoughlin as well, for previous discussions of both exclusivity and "value types are safe")

@belkadan
Copy link
Contributor

cc phabouzit (JIRA User) for Dispatch insight

@swift-ci
Copy link
Collaborator

Comment by Pierre Habouzit (JIRA)

This:
spawnThreadWithoutBarrier
Or this:
waitForConditionVariableWithoutBarrier
Is just a bug. don't do that.

On any sane platform that I know of, spawning a thread has barriers, condition variables are supposed to be used with a mutex that have the right barriers etc..

Dispatch e.g. for dispatch_async:

  • uses a store-release when enqueing a continuation

  • relies on the underlying consume ordering and dependencies from the continuation address on dequeue

I don't think Swift should protect against utterly broken synchronization primitives that are used for data synchronization but do not provide the most basic barriers. these are broken.

@belkadan
Copy link
Contributor

:-) That's fair. In that case, we should be okay. We won't ever have a race on the initial value that gets captured by a closure because that closure somehow has to make it onto another thread (barrier), and then any other races are at least explainable to people. And catchable by TSan.

If this becomes relevant again in some "system-level Swift" effort, well, they can just take extra care in whatever bizarre situation they have using closures across threads without barriers.

@swift-ci
Copy link
Collaborator

Comment by Pierre Habouzit (JIRA)

Even at the "system" level, people have to be mindful of data synchronization themselves, else you have to pessimize for concurrent use and your runtime would have to store-release about everything which is a broken model.

  1. Swift should not provide unsafe sync primitive that can be used as locks and are missing barriers

  2. People writing their own sync primitive, need to know what they're doing, and if it synches data, it implies having proper visibility.

  3. Profit?

@swift-ci swift-ci transferred this issue from apple/swift-issues Apr 25, 2022
This issue was closed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug A deviation from expected or documented behavior. Also: expected but undesirable behavior.
Projects
None yet
Development

No branches or pull requests

4 participants