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

Add MVar #217

Merged
merged 17 commits into from May 15, 2018
Merged

Add MVar #217

merged 17 commits into from May 15, 2018

Conversation

alexandru
Copy link
Member

@alexandru alexandru commented May 14, 2018

Adds MVar to Cats-Effect.

PR also contains website documentation.

Implementation and documentation are imported from Monix, with some changes for working with F[_] and for cancelability.

@SystemFw
Copy link
Contributor

Only had a very quick glance, but one thing: you need read to be a primitive operation.
Having read be take then put leads to all sort of weird behaviour (mostly deadlock) due to concurrent interleaving (e.g. someone puts in between your take and your put).

The original MVar in Haskell had this issue, and read was changed to be primitive because of that.

The other question is adding derived operations, specifically which ones should be added (e.g. modify, modifyF or none) and related questions/docs about their nature (again, mostly due to them not being atomic).

@codecov-io
Copy link

codecov-io commented May 14, 2018

Codecov Report

Merging #217 into master will increase coverage by 0.01%.
The diff coverage is 87.68%.

@@            Coverage Diff             @@
##           master     #217      +/-   ##
==========================================
+ Coverage   88.64%   88.66%   +0.01%     
==========================================
  Files          54       57       +3     
  Lines        1251     1385     +134     
  Branches      100      135      +35     
==========================================
+ Hits         1109     1228     +119     
- Misses        142      157      +15

@alexandru
Copy link
Member Author

@SystemFw

Only had a very quick glance, but one thing: you need read to be a primitive operation.
Having read be take then put leads to all sort of weird behaviour (mostly deadlock) due to concurrent interleaving (e.g. someone puts in between your take and your put).

This is why I left read out of it. If you're designing MVar like a concurrent queue, which it is, read is difficult to introduce as an atomic operation.

Are there any MVar implementations that provide an atomic read? And is it worth the complexity cost?

The other question is adding derived operations, specifically which ones should be added (e.g. modify, modifyF or none) and related questions/docs about their nature (again, mostly due to them not being atomic).

I would lean towards not including any derived operations.

@SystemFw
Copy link
Contributor

Are there any MVar implementations that provide an atomic read? And is it worth the complexity cost?

Well, the MVar implementation (the one in GHC) does, and it was deemed to be worth the cost due to the many problems implementing things that require reading.

If you're designing MVar like a concurrent queue, which it is, read is difficult to introduce as an atomic operation.

I think this is an implementation detail that shouldn't influence the interface one way or another.

I would lean towards not including any derived operations.

No strong opinions here, I can see pros and drawbacks of both

@alexandru
Copy link
Member Author

@SystemFw OK, will try to add it

@alexandru
Copy link
Member Author

@SystemFw so I did not remember read from MVar being atomic, but apparently this changed in "base 4.7": https://hackage.haskell.org/package/base-4.9.1.0/docs/Control-Concurrent-MVar.html#v:readMVar

@SystemFw
Copy link
Contributor

so I did not remember read from MVar being atomic, but apparently this changed in "base 4.7"

Yeah that's what I meant, it proved too inconvenient/error prone in practice, and it was changed 👍

@alexandru
Copy link
Member Author

@SystemFw pushed a commit with an implementation that supports the atomic read.

I have to add more tests and maybe clean it up a bit.

* - [[take]] which empties the var if full, returning the contained
* value, or blocks (asynchronously) otherwise until there is
* a value to pull
*
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

read needs to be added here

@alexandru alexandru changed the title WIP: MVar Add MVar May 14, 2018
@alexandru
Copy link
Member Author

I've added tests and documentation. It is now ready for review.

@@ -41,10 +41,10 @@ private[effect] object Callback {
}

/** Reusable `Right(())` reference. */
final val rightUnit = Right(())

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why are these not final now?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AFAIK final val in companion objects will make the compiler treat that as a constant and inline it. Which would defeat the point of declaring it in the first place, as we're doing such shenanigans in order to not allocate memory every time we need a Right(()).

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@non I thought told me that was only if there is no type annotation, so we could add that but keep the final. But in any case probably not a big deal.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, final val x = ... will inline but final val x: SomeType = ... will not. It's a weird rule.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any difference between final val x: SomeType = ... and val x: SomeType?

From the JVM's point of view both will be final, so I'd rather go with the later.

emptyRef.asInstanceOf[LinkedMap[K, V]]

private val emptyRef =
new LinkedMap[Any, Any](Map.empty, LongMap.empty, 0)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

strictly speaking I think Nothing would be a better type for the value.

private def unsafePut(a: A)(onPut: Listener[Unit]): Unit = {
if (a == null) {
onPut(Left(new NullPointerException("null not supported in MVarAsync/MVar")))
return

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rather than using return could we just wrap the rest in an else?

Reviewers have to always think "is this a return that is actually ok or not?" if we don't

case current @ WaitForTake(value, puts) =>
val update = WaitForTake(value, puts.enqueue(a -> onPut))
if (!stateRef.compareAndSet(current, update)) {
// $COVERAGE-OFF$

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need to do this? can we not try to trigger races that will actually hit this branch?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Triggering race conditions that hit those branches is very difficult and you're not gaining much.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It makes me nervous if we have concurrency primatives we aren’t testing in race conditions.

Seems like it shouldn’t be too hard to hit he branch: just have N threads and maybe two MVars that they take from one and put to the other or something like that.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's easier said than done. In Monix I've got plenty of tests like that and I hate writing them with all my heart 🙂 Note that the current tests are concurrent, but hitting all of those branches is hard an non-deterministic.

That said, I just removed the coverage annotations. So if others want to improve code coverage, they are free to do it.

F.delay(MVarAsync[F, A](a)(F))

/** Builds an empty `MVar`. */
def empty[A](implicit F: Async[F]): F[MVar[F, A]] =
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you have a second Async here? You already have one as a class parameter.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. It's from my experiments 🙂

*
* @see [[async]]
*/
def apply[F[_]](implicit F: Concurrent[F]): ConcurrentBuilder[F] =
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TBH I would prefer to have a simple def apply[F[_]: Concurrent): MVar[F, A]. I understand it brings some type inference help to scalac but on the other hand is less clearer to read and it is not consistent with the other datatypes recently introduced (Ref, Deferred, Semaphore and Resource).

until there is a value available, at which point the operation resorts
to a `take` followed by a `put`

An additional but non-atomic operation is `read`, which tries reading the
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

read is now an atomic operation? :)

}
```

## Use-case: Producer/Consumer Channel
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great example~ 👍

@alexandru
Copy link
Member Author

alexandru commented May 15, 2018

@johnynek so regarding your review, I've added another concurrency test with take, put and read operations racing each other. I also removed the coverage annotations, as mentioned before.

@gvolpe

TBH I would prefer to have a simple def apply[F[_]: Concurrent): MVar[F, A]. I understand it brings some type inference help to scalac but on the other hand is less clearer to read and it is not consistent with the other datatypes recently introduced (Ref, Deferred, Semaphore and Resource).

In my experience thus far with Cats's monad transformers and with Monix's Iterant, without partially applying types in apply, these data types parameterized for F[_] become really annoying. As for the others:

  • this isn't new and has been done in Cats, EitherT as mentioned for example
  • Ref is addressed in Fix #210: curry type params of Ref constructors #211
  • Deferred doesn't need it because you aren't giving it an initial value, so there's no type inference to be had for it
  • Semaphore doesn't need it, because it doesn't have an A type parameter
  • Resource doesn't need it because apply takes an F[_] as parameter and so the Scala compiler is perfectly fine inferring the types for that

Besides Ref, none of the other data types mentioned are relevant.

I did make changes. So in addition to apply's magic, I added those builders as plain functions as well. So as mentioned in the ScalaDoc:

MVar[IO].empty[Int] <-> MVar.empty[IO, Int]

MVar[IO].init("hello") <-> MVar.init[IO, String]("hello")

MVar[IO].initF(IO("hello")) <-> MVar.initF(IO("Hello"))

Also there's no more async, so users will have to use the normal emptyAsync, initAsync or initAsyncF.

*
* @see [[init]], [[initF]] and [[empty]]
*/
def apply[F[_]](implicit F: Concurrent[F]): ApplyBuilders[F] =
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this pattern - can we use it in Ref too? Specifically the notion of having both the regular method on companion and then ApplyBuilders for the type curried type params.

@mpilquist mpilquist self-requested a review May 15, 2018 14:01
Copy link
Member

@rossabaker rossabaker left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@SystemFw SystemFw merged commit 70d58a0 into typelevel:master May 15, 2018
oleg-py added a commit to oleg-py/cats-effect that referenced this pull request May 15, 2018
- Ref.ApplyBuilders is introduced and made public, similar to typelevel#217
- Refs can be constructed as Ref[F, A].of(a) or Ref[F].of(a)
- Unsafe builder is only available as a version with all type parameters
@oleg-py oleg-py mentioned this pull request May 15, 2018
@alexandru alexandru added this to the 1.0.0-RC2 milestone May 23, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

10 participants