Chaos Project

Game Development => Sea of Code => Topic started by: Blizzard on May 11, 2017, 05:13:20 am

Title: Another reason why WinRT is shit
Post by: Blizzard on May 11, 2017, 05:13:20 am
So today I had to work with WinRT again. It was with sockets and WinRT has their own socket implementation. So no c-style sockets are allowed.

The main first issue is the problem that WinRT can't handle reading an "endless" stream from a socket. The problem point is the IInputStream::ReadAsync() method (because everything in WinRT MUST be async). Once you run the method, you can a special AsyncOperation object. You can assign a "Completed" delegate to that object and once WinRT reads at least one byte from the socket, that delegate will be called. Now, the issue here is that if there are no bytes available for reading, WinRT will wait an undefined period of time. This can be a timeout of apparently 180 seconds (somebody mentioned that information somewhere on Stack Overflow, I don't know if it's correct) or basically indefinitely. The catch is that there is no way to ask WinRT whether there are any bytes available for reading. So if you want to keep reading for a while or are wrapping sockets into a higher level system, you are fucked.

Now hold on. Yes, this is nasty, but this is only the introduction into the probably biggest async API design flaw I've seen in my entire life. Prepare yourself for a next-level fuck-up of multi-threaded programming.

So I kept thinking about this issue and an effective way to circumvent it. And I came up with something. I decided to use a buffer which I would then check for data in my main thread method call where I want to receive the data while I just keep calling ReadAsync() and let it do its thing.
Since we're working with async calls here and shared data between what appears to be separate threads (waaaaaait for it...), obviously this data needs to be protected with a mutex (this is a locking mechanism that prevents threads accessing the same data at the same time since threads are undeterministic). So I did that. But suddenly I would keep getting deadlocks sometimes (this is when an already locked mutex is locked in the same thread again and basically means freezing of the thread). I was surprised how this was possible. And then it hit me. I did some testing to confirm my theory and I was right.

You see, usually that "Completed" delegate callback should be called from a different thread. So using a mutex to protect data is absolutely necessary. There is no other way. And most of the time that's exactly what WinRT does. Except when it doesn't. It's possible that ReadAsync() actually finishes before you assign the "Completed" delegate. And you know happens when you finally do assign it? The callback gets called immediately IN THE SAME THREAD WHERE "Completed" WAS ASSIGNED! So if you locked a mutex before assigning "Completed" and then you have to lock that mutex withing the delegate that was assigned to "Completed", you will get a fucking deadlock! Fuck you, Microsoft! Fuck you!

The good news is that I was able to resolve the deadlock by unlocking the mutex before assigning "Completed". But fuck Microsoft and their asynchronous-but-sometimes-it-isn't API. >:(

Here's the final code so you have an easier understanding what I've been struggling with. I added a few comments to make it easier to understand


// this workaround is required due to the fact that IAsyncOperationWithProgress::Completed could be fire upon assignment and then a mutex deadlock would occur
hmutex::ScopeLock _lockAsync(&this->_mutexReceiveAsyncOperation); // ScopeLock makes sure the mutex is unlocked when this method finishes (very useful when throwing exceptions, etc.)
bool asyncOperationRunning = (this->_receiveAsyncOperation != nullptr);
_lockAsync.release(); // has to unlock that mutex again...
if (!asyncOperationRunning)
{
try
{
this->_receiveBuffer = ref new Buffer(this->bufferSize);
this->_receiveAsyncOperation = inputStream->ReadAsync(this->_receiveBuffer, this->bufferSize, InputStreamOptions::Partial);
this->_receiveAsyncOperation->Completed = ref new AsyncOperationWithProgressCompletedHandler<IBuffer^, unsigned int>(
[this](IAsyncOperationWithProgress<IBuffer^, unsigned int>^ operation, AsyncStatus status)
{
if (status == AsyncStatus::Completed)
{
IBuffer^ _buffer = operation->GetResults();
Platform::Array<unsigned char>^ _data = ref new Platform::Array<unsigned char>(_buffer->Length);
DataReader^ reader = DataReader::FromBuffer(_buffer);
try
{
reader->ReadBytes(_data);
hmutex::ScopeLock _lock(&this->_mutexReceiveStream);
this->_receiveStream.writeRaw(_data->Data, _data->Length);
hlog::errorf("OK", "async data: %d", _data->Length);
}
catch (Platform::OutOfBoundsException^ e)
{
}
reader->DetachBuffer();
hmutex::ScopeLock _lock(&this->_mutexReceiveAsyncOperation); // ... because this lock here might as well happen in the same thread as the one at the beginning of this code and cause a deadlock
this->_receiveAsyncOperation = nullptr;
this->_receiveBuffer = nullptr;
}
});
}
catch (Platform::Exception^ e)
{
PlatformSocket::_printLastError(_HL_PSTR_TO_HSTR(e->Message));
return false;
}
}
... // down here I lock this->_mutexReceiveStream and read from this->_receiveStream, but it's not relevant to this issue