When you open a port, the SerialPort class fires up a new thread under the hood which is responsible (via the WaitCommEvent Windows API function) for waiting for serial port activity (e.g. data arrived) and firing the appropriate events to your handlers. That's why events like DataReceived actually occur on a secondary thread.
When you close the port, the Close() call returns immediately but it takes some time for the secondary thread to spin down.
If you attempt to re-open the port too quickly after calling Close, and the thread hasn't yet spun down, then the SerialPort instance is not in a state where it can begin a new connection.
Note the MSDN documentation for SerialPort.Close states:
The best practice for any application is to wait for some amount of
time after calling the Close method before attempting to call the Open
method, as the port may not be closed instantly.
You could keep track of when you closed the port, and before opening it again make sure some arbitrary timeout has elapsed.
No need to sleep before reads / writes, although a few quirks to keep in mind:
Keep in mind the SerialPort class in the .NET BCL still relies on the underlying Win32 API, and I don't think it's gotten a lot of love from Microsoft since the initial implementation.
For more information see: