Yes, I advise not to use tokio::spawn this way. It is not needed for mutli-threaded execution, and does not give you threads. It is not like std::thread::spawn.
You can use regular inline .await, and still have your tasks executed on multiple threads automatically when it's applicable, and the tasks will be run in parallel anyway (this is what async is all about).
In tokio you don't control threads. You will get them if tokio thinks you need them, and you won't get them if tokio thinks you don't need them.
Remember that .await doesn't block executor's thread. Other tasks can run at the same time, and can run on the same thread whenever you yield with .await.
No no no no, don't mix async and std::thread::spawn. Only use .await in side async functions or blocks, and don't spawn anything anywhere.
Regular .await gets threaded scheduler too. You don't need spawn.
However, if you're getting deadlocks, you should look closely at the logic of your program to check if you're not having some circular dependency between the locks (e.g. could writer be waiting for the reader to finish reading?). Spawning or sleeping doesn't fix those circular dependencies, only makes them happen more randomly by reshuffling order of async tasks in tokio's queues.
Also you can add explicit drop(lock) as soon as you're done using it to ensure it's immediately released instead of at the end of a scope.
Also consider not using locks at all. Instead of shared mutable data, refactor the code to have objects with exclusive access to readers/writers, maybe use channels to communicate. It's hard to recommend specifics, because it depends on the task.
And there's tokio console that lets you inspect a running program. So if you get a deadlock, you can connect to your program and see what tasks are waiting.