This gist demonstrates the difference between threading and asyncio. To be clear, they're both limited by the Global Interpreter Lock and are both single process, multi-threaded. They are both forms of concurrency but not parallelism.
Threading (via Thread, concurrent.futures) employs time-slicing of CPU. All threads are given a slot of CPU time to do work. If the thread is blocking (sleeping or blocked on sockets), then off it goes to the next thread. With many threads that are blocked for long periods, this begins to degrade into polling (polling vs. interrupt)
asyncio uses an event loop and is more akin to a pub-sub, push notification model. Threads will announce they're blocked by using asyncio methods. The next available thread at the top of the queue is then processed on, until it completes or is blocked again. This has reduced concurrency and can allow one thread to starve out the others. If the access pattern is threads that are blocked for long time, this model will ensure that you don't bother checking a thread, you wait for it to announce it's available.
@DuncanNguyez one is multi-threading and the other is event-driven paradigm. These are abstract computer science concepts, not able to be explained within this gist.
If forced to give a blanket recommendation, you'd be safest with asyncio. Because the GIL in python (to be removed soon™) reduces multi-threading to single process: it effectively behaves like asyncio but with complete lack of control over control flow. It's unpredictable when context switching will happen, so you expose yourself to all the gotchas with concurrency issues (e. g.race conditions, clobbering memory).