This is a common complaint (not just for Crystal), but I would argue that most people do not actually need or want shared memory concurrency. They need either parallelism and asynchronicity, for which threads are just one possible implementation.

I'm assuming here that this is not just about multiple threads of control (which can also be achieved through having multiple processes), but having threads interact through shared memory rather than through message passing.

The problem is that with shared memory, there's a constant tension between performance and safety. Pretty much all safe approaches introduce either contention (through mutexes or other synchronization constructs) or considerable overhead (such as the O(log n) factor typical for purely functional data structures or creating duplicates to avoid contention). In order to write actual high performance code accessing shared memory, you will typically have to deal with inherently unsafe constructs.

In most cases, it's far easier to optimize message passing, perhaps throwing in some specialized shared memory constructs (such as Mnesia for Erlang) in order to make some common use cases less cumbersome. Note that with current memory hierarchies, the overhead of message passing compared to shared memory may be less than you think; any data that's actually used by more than one core has to pass through the L3 cache at a minimum.

I think that the reason why more people don't use message passing approaches is (1) that few languages actually support it well and (2) that message passing as a programming paradigm can be far more intimidating than shared memory, because causality is more difficult to reason about (though the ease of use of shared memory models can be deceptive and it's easy to run into difficult to diagnose correctness or performance problems). But technologically, message-passing is much easier to support. I've written highly parallel code in Python, Ruby, and OCaml, all of which have a GIL and cannot support shared memory parallelism. But they all have (near) universal serialization, so implementing basic message passing is trivial.

As an example, one of the main attractions of Go is, I think, that it provides a very accessible programming model for message passing (building on top of CSP). And we have PGAS approaches, which essentially simulate shared memory on top of a distributed architecture.

This is not to say that message passing is a panacea; there are plenty of use cases where it's a poor choice. For example, I'm currently working on a problem that is essentially about performing a reduction operation in parallel on multiple threads, where the operation is typically defined through a multi-GB data structure. Copying that data structure to every single thread is prohibitive, and keeping it in one thread would kill all the possible parallelism. But message passing is plenty good enough for 90% or so of all use cases.

Generally, I would phrase it so that the problems that shared memory and message passing have are duals of each other: shared memory is concerned with contention as a source of cost and complexity, message passing is concerned with communication as a source of cost and complexity. For the majority of problems, you can engineer solutions that fit either model (though they may look substantively different), so not using shared memory is not going to be a problem for those.

Asynchronicity is a different concern and does not actually require threads.