Yes, multithreading can absolutely help with I/O-bound process performance (as well as with CPU-bound performance of course).
Here is an example for I/O-bound speedup of 100%. Imagine you implement an algorithm to copy a file in a file system (plain old hard drive or SSD, mounted locally). The core loop will be something like this:
- Initialize a buffer of 1 MB
- Read up to 1 MB of data from the source file to the buffer
- Write the buffer to the target file
- Repeat until end-of-file
Assume that the source and destination files are on different physical drives, and that all other components in the computer (CPU, RAM, Caches, etc.) are much faster than the physical storage so can be neglected here.
Written without multithreading, this will take as long as it takes to read all bytes from the source file, plus the time it takes to write them to the destination file.
A multithreaded variant using a ring buffer would be:
Tread 0:
- Initialize a ring buffer of 10 MB
- Initialize global multithreading-safe variables HEAD := 0, TAIL := 0, FINISHED := 0
- Start Thread 1
- Start Thread 2
- Wait until both are finished
- Exit the process
Thread 1:
- Start loop
- Read up to 1 MB from the source file and place them at location HEAD * 1 MB in the ring buffer.
- If (HEAD + 1) MOD 10 == TAIL, wait until they are not.
- Update HEAD := (HEAD + 1) MOD 10
- Repeat until end-of-file.
- Set global variable FINISHED := TRUE.
- End thread.
Thread 2:
- Start loop
- Wait while HEAD == TAIL AND NOT FINISHED.
- End thread if HEAD == TAIL.
- Write the 1-MB block starting at TAIL * 1 MB to the destination file.
- Update TAIL := (TAIL + 1) MOD 10
- Repeat
The overall time for this will be variable, but roughly on the order of the maximum of the individual times for reading the whole file and writing the whole file (so half of the time used in the single-threaded algorithm, assuming those operations take roughly the same time), plus the time for writing a random small number of blocks (since thread 2 must by definition trail a bit after thread 1).
In other words, for huge files, the multithreaded approach will be roughly twice as fast (assuming reading and writing take the same time, and assuming it's separate physical devices for the two files).
The same principle works for all kinds of I/O, not only file system but also network or direct access to chipset I/O pins or or keyboard input or whatever you have.
Also note that there are very different ways to implement parallelism for I/O, not only multi-threading; for example there is the concept of multi-processing which feels somewhat similar to multi-threading but "looks" quite different. There is event-driven I/O (or multiplexing) which is implemented totally differently from the application point of view (and does not even require or use multi-threading), but will lead to similar results if the CPU required for the I/O operations is negligible.
In fact, when looking at low(ish) level Unix-style I/O sockets, it is totally normal to use these kinds of operations (i.e. the select multiplexing scheme); blocking on I/O by default is kind of a convenience programmers use when very little parallelism or scaling is required.