Most of the answers above talk about performance and simultaneous operation. I approach this from a different angle.
Take the case of, say, a simplified terminal emulation program. You should do the following:
- monitor incoming characters from the remote system and display them
- monitor things coming from the keyboard and send them to the remote system.
(Real terminal emulators do more, including potentially echoing from what you type on the display, but we will get it right now.)
Now the loop for reading from the remote is simple according to the following pseudo-code:
while get-character-from-remote: print-to-screen character
The loop for controlling the keyboard and sending is also simple:
while get-character-from-keyboard: send-to-remote character
The problem is that you have to do this at the same time. The code should now look bigger if you don't have streaming:
loop: check-for-remote-character if remote-character-is-ready: print-to-screen character check-for-keyboard-entry if keyboard-is-ready: send-to-remote character
The logic, even in this intentionally simplified example that does not take into account the complexity of communications in the real world, is rather confusing. However, when threading even on one core, two pseudo-code cycles can exist independently of each other, without alternating their logic. Since both threads will be mainly connected with I / O binding, they do not load the CPU, even if they are, strictly speaking, more wasteful for CPU resources than the integral cycle.
Now, of course, use in the real world is more complicated than stated above. But the complexity of the integrated loop is increasing exponentially as you add more problems to the application. Logic is becoming more and more fragmented, and you need to start using methods such as state machines, coroutines, etc., to achieve convenience. Managed, but not readable. Threading keeps the code more readable.
So why don't you use streams?
Well, if your tasks are CPU bound instead of I / O binding, threads actually slow down your system. Performance will suffer. In many cases a lot. ("Thrashing" is a common problem, if you throw too many threads associated with the processor, you spend more time changing active threads than on the contents of the threads themselves). In addition, one of the reasons is so simple that I very consciously chose a simplified (and unrealistic) example. If you want to repeat what was printed on the screen, then you have a new world when you enter a lock on shared resources. With just one shared resource, this is not so much a problem, but it is starting to become an increasingly serious problem as you have more resources to share.
So, after all, multithreading is a lot of things. For example, this is due to the fact that the processes associated with I / O binding are more responsive (even if they are less efficient in general), as some have already said. It also facilitates the logic of the logic (but only if you minimize the overall state). It is about a lot, and you need to decide whether its advantages exceed its disadvantages on an individual basis.