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

[ base ] Add atomically function #3380

Merged
merged 14 commits into from
Sep 11, 2024

Conversation

Matthew-Mosior
Copy link
Contributor

@Matthew-Mosior Matthew-Mosior commented Sep 6, 2024

This PR adds a new function, atomically to Data.IORef (only for the chez backend).

This function atomically runs its argument according to the provided mutex.

It can for instance be used to modify the contents of an IORef ref with a function f in a safe way in a multithreaded program by using atomically lock (modifyIORef ref f) provided that other threads also rely on the same lock to modify ref.

Credit to @stefan-hoeck for providing code to test this addition in chez003.

Thanks to @gallais for the generalized atomically implementation.

@Matthew-Mosior Matthew-Mosior marked this pull request as ready for review September 7, 2024 00:43
Comment on lines 56 to 70

||| This function atomically modifies the contents of an IORef.
||| This function is useful for using IORef in a safe way in a multithreaded program.
||| If you only have one IORef, then using atomicModifyIORef to access and modify it will prevent race conditions.
||| Any backend other than the default (chez) will perform modifyIORef (non-atomic).
export
atomicModifyIORef : HasIO io => IORef a -> (a -> a) -> io ()
atomicModifyIORef ref f
= case codegen of
"chez" => do mutex <- makeMutex
mutexAcquire mutex
val <- readIORef ref
writeIORef ref (f val)
mutexRelease mutex
_ => modifyIORef ref f
Copy link
Contributor

Choose a reason for hiding this comment

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

This cannot work. Two threads that compete for an atomic reference must be using the same mutex. Here, you create a new mutex for every new call to atomicModifyIORef which is quite pointless. So, the mutex must either be an argument to atomicModifyIORef (see my example on discord) or wrapped up in IORef itself, but that would probably slow down everything else.

Comment on lines 20 to 23
t1 <- fork $ do
atomicModifyIORef z (* 2)
t2 <- fork $
atomicModifyIORef z (* 2)
Copy link
Contributor

Choose a reason for hiding this comment

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

In order to proof atomicity, the two competing threads must be long running (hundreds of milliseconds) and both access the mutable reference a lot (thousands or better millions of accesses). Preferably, one is considerably slower than the other. Only then will you create enough contention to truly show atomicity. Again, see my example on discord.

In this example, creating and acquiring the mutex (which you currently do on every call to atomicModifyIORef) is so slow (mutexes are slow) compared to the simple update action, that you will almost always get the illusion of atomicity. Try running your version of atomicModifyIORef with the example code I gave on discord and you will see that it is not atomic at all.

Copy link
Contributor Author

@Matthew-Mosior Matthew-Mosior Sep 7, 2024

Choose a reason for hiding this comment

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

Ah I see, the code I added in chez003 never actually created any true contention thus never tested the atomicity of atomicModifyIORef. I added some of the code from discord you provided for chez003, thank you for providing this, and thanks for helping me see/understand this!

@stefan-hoeck
Copy link
Contributor

One more thought on this: I think in your implementation of atomicModifyIORef you are trying to make sure that the mutex version is only used on Scheme backends. I'm afraid, this won't work, because the compiler has to generate code for every possible branch in your pattern match (but maybe I'm mistaken, and this is indeed how such tests should be performed). In order to verify this, you should add a test that tries compiling and running some code with atomicModifyIORef on Node.js.

Again, there are several possibilities to approach this if the current version does not work:

  • Let the decision if atomicModifyIORef should be used or not be made in client code.
  • Write dummy implementations of prim__makeMutex and friends for the JavaScript backends. You could, for instance, just return 0 or undefined for prim__makeMutex on JavaScript backends. mutexAcquire and mutexRelease would then just be no-ops on those backends, since they are single-threaded anyway.

…client decide this. Also update test to ensure enough contention to test for true atomicity (thanks to @stefan-hoeck for help with both of these).
@Matthew-Mosior
Copy link
Contributor Author

Matthew-Mosior commented Sep 7, 2024

One more thought on this: I think in your implementation of atomicModifyIORef you are trying to make sure that the mutex version is only used on Scheme backends. I'm afraid, this won't work, because the compiler has to generate code for every possible branch in your pattern match (but maybe I'm mistaken, and this is indeed how such tests should be performed). In order to verify this, you should add a test that tries compiling and running some code with atomicModifyIORef on Node.js.

Again, there are several possibilities to approach this if the current version does not work:

* Let the decision if `atomicModifyIORef` should be used or not be made in client code.

* Write dummy implementations of `prim__makeMutex` and friends for the JavaScript backends. You could, for instance, just return 0 or `undefined` for `prim__makeMutex` on JavaScript backends. `mutexAcquire` and `mutexRelease` would then just be no-ops on those backends, since they are single-threaded anyway.

Thank you for the feedback and the possible solutions.

I have decided to just let the client make the decision of when to utilize atomicModifyIORef, which seems like the simplest solution currently (this should be addressed via 0fb8ed7).

Might be cool to add some dummy primitives for the javascript backend in the future though.

libs/base/Data/IORef.idr Outdated Show resolved Hide resolved
Matthew-Mosior and others added 3 commits September 7, 2024 16:38
Co-authored-by: G. Allais <guillaume.allais@ens-lyon.org>
…dating tests/chez/chez003/IORef.idr to reflect new function.
@Matthew-Mosior Matthew-Mosior changed the title [ base ] Add atomicModifyIORef function [ base ] Add atomically function Sep 7, 2024
@Matthew-Mosior
Copy link
Contributor Author

@stefan-hoeck @gallais

Not sure what to do about the failing checks (pretty much the same issue for all of them):

Downloading single artifact
Error: Unable to download artifact(s): Artifact not found for name: ubuntu-installed-idris2-0.7.0-chez
        Please ensure that your artifact is not expired and the artifact was uploaded using a compatible version of toolkit/upload-artifact.
        For more information, visit the GitHub Artifacts FAQ: https://github.com/actions/toolkit/blob/main/packages/artifact/docs/faq.md

@mattpolzin
Copy link
Collaborator

mattpolzin commented Sep 11, 2024

RE failing checks, it may just be that GitHub broke home-directory resolution in their paths for artifacts. I am testing this theory out now.

[EDIT] I was wrong. But I did spot a suspicious looking "include hidden files: false" setting that might be causing this problem.

@Matthew-Mosior
Copy link
Contributor Author

RE failing checks, it may just be that GitHub broke home-directory resolution in their paths for artifacts. I am testing this theory out now.

[EDIT] I was wrong. But I did spot a suspicious looking "include hidden files: false" setting that might be causing this problem.

Thank you for looking into this!

@mattpolzin
Copy link
Collaborator

mattpolzin commented Sep 11, 2024

Testing the hidden file theory now. Very promising given the recency of actions/upload-artifact#602.

[EDIT] Looks good. Fix is up for PR.

@mattpolzin
Copy link
Collaborator

You're good to merge main into this branch when you get the chance and CI should work again.

@Matthew-Mosior
Copy link
Contributor Author

You're good to merge main into this branch when you get the chance and CI should work again.

Sounds good, just merged main in!

Comment on lines 46 to 47
writeIORef1 : HasLinearIO io => IORef a -> (1 val : a) -> io ()
writeIORef1 (MkRef m) val = primIO1 (prim__writeIORef m val)
Copy link
Member

Choose a reason for hiding this comment

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

Unrelated but this seems unsafe: by writing a linear value into a reference, we gain the ability to freely duplicate it!

@gallais gallais merged commit 53f448c into idris-lang:main Sep 11, 2024
22 checks passed
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.

4 participants