I've implemented a library of immutable data structures in C with C++ wrappers on top. I can't share it as it's proprietary and it did require some elbow grease to build, but just took me a couple of weeks before I was writing a bunch of multithreaded test code that was able to just return transformed versions of these structures without a worry in the world about overlapping reads/writes and race conditions. Of course I've expanded it a lot since then but it doesn't take long to get started and see exciting results even if you're doing this from scratch.
Moreover I found it really simplified implementing undo systems when the undo system can just deep copy the entire data structure and know that it's not actually duplicating that much data beyond the parts that are different/unique (there's some balance of redundancy when designing persistent data structures efficiently where you want to avoid shallow copying the most granular data while simultaneously avoiding deep copies of overly coarse data -- for images/textures I broke them up into tiles to be shallow copied or deep copied based on what image tile becomes different through a transformation as a basic example).
Actually it trivialized the undo system arguably just as much if not more than the multithreading because I used to implement undo systems by storing granular deltas of each and every little thing that the user touched. When you can just store an entire immutable copy of an entire "scene" or application state without worrying about it being too expensive before the user begins some high-level operation, boy does it simplify things on the undo side. It does shift some additional complexity to the client code creating modified versions of the data, but I found the overall exchange results in less complexity than it adds and, beyond complexity, it makes it really, really easy to reason about the system's correctness without wading through endless code, however simple, that can cause side effects and therefore be a potential source of problems.
This still doesn't allow pure functional programming style so effectively since the language still can't recurse deeply without stack overflows, e.g., and it's still unwieldy even with lambdas in C++11 and beyond to write a lot of code in a functional programming style, but you can still get a great deal of the benefits of avoiding the human errors associated with complex state management, especially in a multithreaded context, without fighting the language so much if you build yourself some nice immutable data structures or grab some from a library.
There are libraries like these implemented in C++ which followed the same vein.
https://sinusoid.es/immer/
Mine's not quite as sophisticated since I didn't put as much effort into trying to allow basic scalar operations. My design revolves around the client code expressing what changes to make to the data structures in bulky transactions to "commit" to get the new immutable copy and revolving more around the "transients" idea discussed in that "immer" library where a thread modifies a local mutable structure lock-free and then "commits" it to get a new persistent immutable. Example pseudocode:
Image transform_some_image(const Image& img)
{
// Grab a transient raster object to mutate (basically
// an array of pixels).
TransientRaster raster = img.transform(some_rectangle);
// Modify pixels in the raster.
...
// Commit the raster, getting a new transformed image. This
// does not modify the original, only gives back a new image
// which shallow copies most of the data we didn't touch above.
return img.commit(raster);
}
It also trivializes exception safety since so few parts of the application cause side effects that need to be rolled back in the event of an exception being thrown since the majority of code is just constructing immutable structures which can be tossed away (implicitly through destructors in C++) if the operation fails. That's similar to the undo system since writing exception-safe code is somewhat analogous to undo systems normally where you have to revert every little side effect unless you have immutable structures handy which reduce the number of places causing side effects to a minimum. If the above code threw an exception, the raster transient can be tossed aside and we have no side effects whatsoever since it doesn't touch the pixels of the original image.
The above function can also be parallelized to the smithereens without putting any thought into it since, again, it's not touching any state outside of states local to the function (no side effects, at least as far as the outside world is concerned). The only part that locks is the commit
call to create a new immutable image which only deep copies the tiles modified by the transient raster.
It does result in the client code having to work with these transient objects a lot and some extra code all over the place to express what it wants to do as a "transaction", but actually not that much and possibly even less if you take into account all the things that have to be done for a particular piece of code to be both thread-safe and exception-safe, and it also trivializes the amount of thought you have to put in before you write a function that can achieve both of these.
Moreover, it doesn't solve design-level multithreading problems which is why I see the more practical benefit being in exception safety and undo systems and modifier stacks and node graphs which input something and output something new and things of this sort. For example, if two threads create two modified immutable versions of the same image, which one do we use to show to the user? Unless we have a way to consolidate the transformations meaningfully made by both threads or can just meaningfully ignore the results of one of them, then immutable structures do squat as far as solving the whiteboard issues of the design. However, it does make it so you can rest assured that you won't have some exotic race condition which only occurs when there's a full moon outside while a virgin is being deflowered and only for blonde users who happen to be working on something very important at the time. You can at least feel safe against those extremely obscure, mind-numbing edge cases that seem almost impossible to reproduce until that ultra rare moment you decide to try the application in a release build without a debugger handy and weren't even focused on trying to reproduce this problem.