Repository: encode/httpx

Issue:
Python 3.7: RuntimeError: read() called while another coroutine is already waiting for incoming data 
As we already discussed with @florimondmanca in https://github.com/encode/httpx/issues/382, I am reporting this issue here. 

I get the following error: `RuntimeError: read() called while another coroutine is already waiting for incoming data` when I am sending images to the server. As you can see from the code it is implemented that only max 100 requests are sent to the server at the same time. I know there could be lazy loading used but the limitation is made custom because before we send images to the server we check whether they are in the local cache or not. The limit of 100 is set that not all images are loaded in the memory at the same time (there can be also 10000 images).

Here is the trace:
```
Traceback (most recent call last):
  File "/Users/primoz/PycharmProjects/orange3-imageanalytics/example.py", line 63, in <module>
    asyncio.get_event_loop().run_until_complete(cl.embedd_batch())
  File "/Users/primoz/miniconda3/lib/python3.7/asyncio/base_events.py", line 584, in run_until_complete
    return future.result()
  File "/Users/primoz/PycharmProjects/orange3-imageanalytics/example.py", line 18, in embedd_batch
    embeddings = await asyncio.gather(*requests)
  File "/Users/primoz/PycharmProjects/orange3-imageanalytics/example.py", line 41, in _send_to_server
    emb = await self._send_request(client, im, url)
  File "/Users/primoz/PycharmProjects/orange3-imageanalytics/example.py", line 56, in _send_request
    data=image
  File "/Users/primoz/venv/orange/lib/python3.7/site-packages/httpx/client.py", line 484, in post
    trust_env=trust_env,
  File "/Users/primoz/venv/orange/lib/python3.7/site-packages/httpx/client.py", line 626, in request
    trust_env=trust_env,
  File "/Users/primoz/venv/orange/lib/python3.7/site-packages/httpx/client.py", line 650, in send
    trust_env=trust_env,
  File "/Users/primoz/venv/orange/lib/python3.7/site-packages/httpx/client.py", line 265, in _get_response
    return await get_response(request)
  File "/Users/primoz/venv/orange/lib/python3.7/site-packages/httpx/middleware/redirect.py", line 31, in __call__
    response = await get_response(request)
  File "/Users/primoz/venv/orange/lib/python3.7/site-packages/httpx/client.py", line 226, in get_response
    request, verify=verify, cert=cert, timeout=timeout
  File "/Users/primoz/venv/orange/lib/python3.7/site-packages/httpx/dispatch/connection_pool.py", line 126, in send
    raise exc
  File "/Users/primoz/venv/orange/lib/python3.7/site-packages/httpx/dispatch/connection_pool.py", line 121, in send
    request, verify=verify, cert=cert, timeout=timeout
  File "/Users/primoz/venv/orange/lib/python3.7/site-packages/httpx/dispatch/connection.py", line 62, in send
    response = await self.h2_connection.send(request, timeout=timeout)
  File "/Users/primoz/venv/orange/lib/python3.7/site-packages/httpx/dispatch/http2.py", line 52, in send
    status_code, headers = await self.receive_response(stream_id, timeout)
  File "/Users/primoz/venv/orange/lib/python3.7/site-packages/httpx/dispatch/http2.py", line 171, in receive_response
    event = await self.receive_event(stream_id, timeout)
  File "/Users/primoz/venv/orange/lib/python3.7/site-packages/httpx/dispatch/http2.py", line 206, in receive_event
    data = await self.stream.read(self.READ_NUM_BYTES, timeout, flag=flag)
  File "/Users/primoz/venv/orange/lib/python3.7/site-packages/httpx/concurrency/asyncio.py", line 114, in read
    data = await asyncio.wait_for(self.stream_reader.read(n), read_timeout)
  File "/Users/primoz/miniconda3/lib/python3.7/asyncio/tasks.py", line 416, in wait_for
    return fut.result()
  File "/Users/primoz/miniconda3/lib/python3.7/asyncio/streams.py", line 640, in read
    await self._wait_for_data('read')
  File "/Users/primoz/miniconda3/lib/python3.7/asyncio/streams.py", line 460, in _wait_for_data
    f'{func_name}() called while another coroutine is '
RuntimeError: read() called while another coroutine is already waiting for incoming data
```

The minimum example that reproduces the error is here:

```
import asyncio
import pickle

import httpx


class SendRequests:
    num_parallel_requests = 0
    MAX_PARALLEL = 100

    async def embedd_batch(self):
        requests = []
        async with httpx.AsyncClient(
                timeout=httpx.TimeoutConfig(timeout=60),
                base_url="https://api.garaza.io/") as client:
            for i in range(1000):
                requests.append(self._send_to_server(client))

            embeddings = await asyncio.gather(*requests)

        return embeddings

    async def __wait_until_released(self):
        while self.num_parallel_requests >= self.MAX_PARALLEL:
            await asyncio.sleep(0.1)

    async def _send_to_server(self, client):
        await self.__wait_until_released()

        self.num_parallel_requests += 1
        # simplified image loading 
        with open("image.pkl", "rb") as f:
            im = pickle.load(f)
        url = "/image/inception-v3?machine=1&session=1&retry=0"
        emb = await self._send_request(client, im, url)
        self.num_parallel_requests -= 1
        return emb

    async def _send_request(self, client, image, url):
        headers = {'Content-Type': 'image/jpeg',
                   'Content-Length': str(len(image))}
        response = await client.post(url, headers=headers, data=image)
        print(response.content)
        return response


if __name__ == "__main__":
    cl = SendRequests()
    asyncio.get_event_loop().run_until_complete(cl.embedd_batch())
```

All advice about this error is welcome. I tried to debug this error by myself but was not successful. 

The pickle file required in the code snippet is here: 
[image.pkl.zip](https://github.com/encode/httpx/files/3847317/image.pkl.zip)


Discussion:
@iluxonchik posted another example in #382, which I'm reposting here in a runnable form:

```python
import asyncio
import httpx

async def print_content_from_url(url, client):
    res = await client.get(url)
    print(res, url)

async def main():
    urls = ['https://example.com', 'https://google.com']*100
    async with httpx.AsyncClient() as client:
        await asyncio.gather(*(print_content_from_url(url, client) for url in urls))

asyncio.run(main())
```

This program runs fine on my side, on 3.7.3 and 3.8.0. @iluxonchik Which Python version were you using?
@PrimozGodec I was able to run your code and reproduce the error you listed (on 3.8.0).

From the traceback I noticed that the requests are made using HTTP/2. I forced usage of HTTP/1.1 by passing `http_versions=["HTTP/1.1"]` and the requests went through just fine. Can you confirm this dirty-fixes the issue for you as well?

This is probably somewhat related to #514.
Interestingly, if I remove the `HTTP/1.1` hot fix, but then set `MAX_PARALLEL = 2` and only make 4 requests, the issue occurs too. It doesn't occur if using anything else than (2, 4), e.g. (1, 2) goes through just fine, and so does (2, 3).

So I turned on TRACE logs and here's what we get:

```console
❯ HTTPX_LOG_LEVEL=trace python debug/read_error_prizmo.py
TRACE [2019-11-17 12:11:58] httpx.dispatch.connection_pool - acquire_connection origin=Origin(scheme='https' host='api.garaza.io' port=443)
TRACE [2019-11-17 12:11:58] httpx.dispatch.connection_pool - acquire_connection origin=Origin(scheme='https' host='api.garaza.io' port=443)
TRACE [2019-11-17 12:11:58] httpx.dispatch.connection_pool - new_connection connection=HTTPConnection(origin=Origin(scheme='https' host='api.garaza.io' port=443))
TRACE [2019-11-17 12:11:58] httpx.config - load_ssl_context verify=True cert=None trust_env=True http_versions=HTTPVersionConfig(['HTTP/1.1', 'HTTP/2'])
TRACE [2019-11-17 12:11:58] httpx.dispatch.connection_pool - new_connection connection=HTTPConnection(origin=Origin(scheme='https' host='api.garaza.io' port=443))
TRACE [2019-11-17 12:11:58] httpx.config - load_ssl_context verify=True cert=None trust_env=True http_versions=HTTPVersionConfig(['HTTP/1.1', 'HTTP/2'])
TRACE [2019-11-17 12:11:58] httpx.config - load_verify_locations cafile=/Users/florimond/.pyenv/versions/3.8.0/lib/python3.8/site-packages/certifi/cacert.pem
TRACE [2019-11-17 12:11:58] httpx.config - load_verify_locations cafile=/Users/florimond/.pyenv/versions/3.8.0/lib/python3.8/site-packages/certifi/cacert.pem
TRACE [2019-11-17 12:11:58] httpx.dispatch.connection - start_connect host='api.garaza.io' port=443 timeout=TimeoutConfig(timeout=60)
TRACE [2019-11-17 12:11:58] httpx.dispatch.connection - start_connect host='api.garaza.io' port=443 timeout=TimeoutConfig(timeout=60)
TRACE [2019-11-17 12:11:58] httpx.dispatch.connection - connected http_version='HTTP/2'
TRACE [2019-11-17 12:11:58] httpx.dispatch.http2 - send_headers stream_id=1 method='POST' target='/image/inception-v3?machine=1&session=1&retry=0' headers=[(b':method', b'POST'), (b':authority', b'api.garaza.io'), (b':scheme', b'https'), (b':path', b'/image/inception-v3?machine=1&session=1&retry=0'), (b'user-agent', b'python-httpx/0.7.7'), (b'accept', b'*/*'), (b'accept-encoding', b'gzip, deflate'), (b'connection', b'keep-alive'), (b'content-type', b'image/jpeg'), (b'content-length', b'30959')]
TRACE [2019-11-17 12:11:58] httpx.dispatch.http2 - receive_event stream_id=0 event=<RemoteSettingsChanged changed_settings:{ChangedSetting(setting=SettingCodes.MAX_CONCURRENT_STREAMS, original_value=None, new_value=128), ChangedSetting(setting=SettingCodes.INITIAL_WINDOW_SIZE, original_value=65535, new_value=65536), ChangedSetting(setting=SettingCodes.MAX_FRAME_SIZE, original_value=16384, new_value=16777215)}>
TRACE [2019-11-17 12:11:58] httpx.dispatch.http2 - receive_event stream_id=0 event=<WindowUpdated stream_id:0, delta:2147418112>
TRACE [2019-11-17 12:11:58] httpx.dispatch.http2 - end_stream stream_id=1
TRACE [2019-11-17 12:11:58] httpx.dispatch.connection - connected http_version='HTTP/2'
TRACE [2019-11-17 12:11:58] httpx.dispatch.http2 - send_headers stream_id=1 method='POST' target='/image/inception-v3?machine=1&session=1&retry=0' headers=[(b':method', b'POST'), (b':authority', b'api.garaza.io'), (b':scheme', b'https'), (b':path', b'/image/inception-v3?machine=1&session=1&retry=0'), (b'user-agent', b'python-httpx/0.7.7'), (b'accept', b'*/*'), (b'accept-encoding', b'gzip, deflate'), (b'connection', b'keep-alive'), (b'content-type', b'image/jpeg'), (b'content-length', b'30959')]
TRACE [2019-11-17 12:11:58] httpx.dispatch.http2 - receive_event stream_id=0 event=<RemoteSettingsChanged changed_settings:{ChangedSetting(setting=SettingCodes.MAX_CONCURRENT_STREAMS, original_value=None, new_value=128), ChangedSetting(setting=SettingCodes.INITIAL_WINDOW_SIZE, original_value=65535, new_value=65536), ChangedSetting(setting=SettingCodes.MAX_FRAME_SIZE, original_value=16384, new_value=16777215)}>
TRACE [2019-11-17 12:11:58] httpx.dispatch.http2 - receive_event stream_id=0 event=<WindowUpdated stream_id:0, delta:2147418112>
TRACE [2019-11-17 12:11:58] httpx.dispatch.http2 - end_stream stream_id=1
TRACE [2019-11-17 12:11:58] httpx.dispatch.http2 - receive_event stream_id=0 event=<SettingsAcknowledged changed_settings:{}>
TRACE [2019-11-17 12:11:58] httpx.dispatch.http2 - receive_event stream_id=1 event=<WindowUpdated stream_id:1, delta:2147418111>
TRACE [2019-11-17 12:11:58] httpx.dispatch.http2 - receive_event stream_id=0 event=<SettingsAcknowledged changed_settings:{}>
TRACE [2019-11-17 12:11:58] httpx.dispatch.http2 - receive_event stream_id=1 event=<WindowUpdated stream_id:1, delta:2147418111>
TRACE [2019-11-17 12:11:58] httpx.dispatch.http2 - receive_event stream_id=1 event=<ResponseReceived stream_id:1, headers:[(b':status', b'200'), (b'server', b'nginx/1.13.5'), (b'date', b'Sun, 17 Nov 2019 11:11:58 GMT'), (b'content-type', b'application/json'), (b'strict-transport-security', b'max-age=15724800; includeSubDomains;'), (b'content-encoding', b'gzip')]>
TRACE [2019-11-17 12:11:58] httpx.dispatch.http2 - receive_event stream_id=1 event=<DataReceived stream_id:1, flow_controlled_length:2006, data:1f8b08000000000000034c974d8e64450c84efc2>
TRACE [2019-11-17 12:11:58] httpx.dispatch.http2 - receive_event stream_id=1 event=<DataReceived stream_id:1, flow_controlled_length:1940, data:54996d92eb2610453734492101022d28fbdf42ce>
TRACE [2019-11-17 12:11:58] httpx.dispatch.http2 - receive_event stream_id=1 event=<DataReceived stream_id:1, flow_controlled_length:1894, data:54da4996a4381004d00bf502100271ff8bb57d97>
TRACE [2019-11-17 12:11:58] httpx.dispatch.http2 - receive_event stream_id=1 event=<DataReceived stream_id:1, flow_controlled_length:1849, data:64db4992e3380c05d00bf5c2a2e6fb5facf13e28>
TRACE [2019-11-17 12:11:58] httpx.dispatch.http2 - receive_event stream_id=1 event=<DataReceived stream_id:1, flow_controlled_length:1861, data:5cdc59d69c2a1405e00965dda52828f39fd8e5db>
TRACE [2019-11-17 12:11:58] httpx.dispatch.http2 - receive_event stream_id=1 event=<ResponseReceived stream_id:1, headers:[(b':status', b'200'), (b'server', b'nginx/1.13.5'), (b'date', b'Sun, 17 Nov 2019 11:11:58 GMT'), (b'content-type', b'application/json'), (b'strict-transport-security', b'max-age=15724800; includeSubDomains;'), (b'content-encoding', b'gzip')]>
TRACE [2019-11-17 12:11:58] httpx.dispatch.http2 - receive_event stream_id=1 event=<DataReceived stream_id:1, flow_controlled_length:2006, data:1f8b08000000000000034c974d8e64450c84efc2>
TRACE [2019-11-17 12:11:58] httpx.dispatch.http2 - receive_event stream_id=1 event=<DataReceived stream_id:1, flow_controlled_length:1940, data:54996d92eb2610453734492101022d28fbdf42ce>
TRACE [2019-11-17 12:11:58] httpx.dispatch.http2 - receive_event stream_id=1 event=<DataReceived stream_id:1, flow_controlled_length:1894, data:54da4996a4381004d00bf502100271ff8bb57d97>
TRACE [2019-11-17 12:11:58] httpx.dispatch.http2 - receive_event stream_id=1 event=<DataReceived stream_id:1, flow_controlled_length:1849, data:64db4992e3380c05d00bf5c2a2e6fb5facf13e28>
TRACE [2019-11-17 12:11:58] httpx.dispatch.http2 - receive_event stream_id=1 event=<DataReceived stream_id:1, flow_controlled_length:1861, data:5cdc59d69c2a1405e00965dda52828f39fd8e5db>
TRACE [2019-11-17 12:11:58] httpx.dispatch.http2 - receive_event stream_id=1 event=<DataReceived stream_id:1, flow_controlled_length:1861, data:54dc59b2a4380c05d00d755430d80cfbdf58fb5c>
TRACE [2019-11-17 12:11:58] httpx.dispatch.http2 - receive_event stream_id=1 event=<DataReceived stream_id:1, flow_controlled_length:1880, data:54dd5992e3380c04d00b755458bb74ff8b0d5f02>
TRACE [2019-11-17 12:11:58] httpx.dispatch.http2 - receive_event stream_id=1 event=<DataReceived stream_id:1, flow_controlled_length:1862, data:5cdd5996e3361044d11df99000c7fd6fccba9109>
TRACE [2019-11-17 12:11:58] httpx.dispatch.http2 - receive_event stream_id=1 event=<DataReceived stream_id:1, flow_controlled_length:1259, data:4cdd4992e3380c05d07d9fa516d44c1da68e5277>
TRACE [2019-11-17 12:11:58] httpx.dispatch.http2 - receive_event stream_id=1 event=<DataReceived stream_id:1, flow_controlled_length:10, data:0300b2290cf3398a0000>
TRACE [2019-11-17 12:11:58] httpx.dispatch.http2 - receive_event stream_id=1 event=<StreamEnded stream_id:1>
TRACE [2019-11-17 12:11:58] httpx.dispatch.connection_pool - release_connection connection=HTTPConnection(origin=Origin(scheme='https' host='api.garaza.io' port=443))
DEBUG [2019-11-17 12:11:58] httpx.client - HTTP Request: POST https://api.garaza.io/image/inception-v3?machine=1&session=1&retry=0 "HTTP/2 200 OK"
TRACE [2019-11-17 12:11:58] httpx.dispatch.http2 - receive_event stream_id=1 event=<DataReceived stream_id:1, flow_controlled_length:1861, data:54dc59b2a4380c05d00d755430d80cfbdf58fb5c>
TRACE [2019-11-17 12:11:58] httpx.dispatch.http2 - receive_event stream_id=1 event=<DataReceived stream_id:1, flow_controlled_length:1880, data:54dd5992e3380c04d00b755458bb74ff8b0d5f02>
TRACE [2019-11-17 12:11:58] httpx.dispatch.http2 - receive_event stream_id=1 event=<DataReceived stream_id:1, flow_controlled_length:1862, data:5cdd5996e3361044d11df99000c7fd6fccba9109>
TRACE [2019-11-17 12:11:58] httpx.dispatch.http2 - receive_event stream_id=1 event=<DataReceived stream_id:1, flow_controlled_length:1259, data:4cdd4992e3380c05d07d9fa516d44c1da68e5277>
TRACE [2019-11-17 12:11:58] httpx.dispatch.http2 - receive_event stream_id=1 event=<DataReceived stream_id:1, flow_controlled_length:10, data:0300b2290cf3398a0000>
TRACE [2019-11-17 12:11:58] httpx.dispatch.http2 - receive_event stream_id=1 event=<StreamEnded stream_id:1>
TRACE [2019-11-17 12:11:58] httpx.dispatch.connection_pool - release_connection connection=HTTPConnection(origin=Origin(scheme='https' host='api.garaza.io' port=443))
DEBUG [2019-11-17 12:11:58] httpx.client - HTTP Request: POST https://api.garaza.io/image/inception-v3?machine=1&session=1&retry=0 "HTTP/2 200 OK"
TRACE [2019-11-17 12:11:58] httpx.dispatch.connection_pool - acquire_connection origin=Origin(scheme='https' host='api.garaza.io' port=443)
TRACE [2019-11-17 12:11:58] httpx.dispatch.connection_pool - reuse_connection connection=HTTPConnection(origin=Origin(scheme='https' host='api.garaza.io' port=443))
TRACE [2019-11-17 12:11:58] httpx.dispatch.http2 - send_headers stream_id=3 method='POST' target='/image/inception-v3?machine=1&session=1&retry=0' headers=[(b':method', b'POST'), (b':authority', b'api.garaza.io'), (b':scheme', b'https'), (b':path', b'/image/inception-v3?machine=1&session=1&retry=0'), (b'user-agent', b'python-httpx/0.7.7'), (b'accept', b'*/*'), (b'accept-encoding', b'gzip, deflate'), (b'connection', b'keep-alive'), (b'content-type', b'image/jpeg'), (b'content-length', b'30959')]
TRACE [2019-11-17 12:11:58] httpx.dispatch.connection_pool - acquire_connection origin=Origin(scheme='https' host='api.garaza.io' port=443)
TRACE [2019-11-17 12:11:58] httpx.dispatch.connection_pool - reuse_connection connection=HTTPConnection(origin=Origin(scheme='https' host='api.garaza.io' port=443))
TRACE [2019-11-17 12:11:58] httpx.dispatch.http2 - send_headers stream_id=5 method='POST' target='/image/inception-v3?machine=1&session=1&retry=0' headers=[(b':method', b'POST'), (b':authority', b'api.garaza.io'), (b':scheme', b'https'), (b':path', b'/image/inception-v3?machine=1&session=1&retry=0'), (b'user-agent', b'python-httpx/0.7.7'), (b'accept', b'*/*'), (b'accept-encoding', b'gzip, deflate'), (b'connection', b'keep-alive'), (b'content-type', b'image/jpeg'), (b'content-length', b'30959')]
TRACE [2019-11-17 12:11:58] httpx.dispatch.http2 - end_stream stream_id=3
TRACE [2019-11-17 12:11:58] httpx.dispatch.http2 - end_stream stream_id=5
TRACE [2019-11-17 12:11:58] httpx.dispatch.connection - close_connection
TRACE [2019-11-17 12:11:58] httpx.dispatch.http2 - receive_event stream_id=3 event=<WindowUpdated stream_id:3, delta:2147418111>
TRACE [2019-11-17 12:11:58] httpx.dispatch.http2 - receive_event stream_id=5 event=<WindowUpdated stream_id:5, delta:2147418111>
Traceback (most recent call last):
  File "debug/read_error_prizmo.py", line 53, in <module>
    asyncio.get_event_loop().run_until_complete(cl.embedd_batch())
  File "/Users/florimond/.pyenv/versions/3.8.0/lib/python3.8/asyncio/base_events.py", line 608, in run_until_complete
    return future.result()
  File "debug/read_error_prizmo.py", line 24, in embedd_batch
    embeddings = await asyncio.gather(*requests)
  File "debug/read_error_prizmo.py", line 40, in _send_to_server
    emb = await self._send_request(client, im, url)
  File "debug/read_error_prizmo.py", line 46, in _send_request
    response = await client.post(url, headers=headers, data=image)
  File "/Users/florimond/.pyenv/versions/3.8.0/lib/python3.8/site-packages/httpx/client.py", line 477, in post
    return await self.request(
  File "/Users/florimond/.pyenv/versions/3.8.0/lib/python3.8/site-packages/httpx/client.py", line 626, in request
    response = await self.send(
  File "/Users/florimond/.pyenv/versions/3.8.0/lib/python3.8/site-packages/httpx/client.py", line 650, in send
    return await self._get_response(
  File "/Users/florimond/.pyenv/versions/3.8.0/lib/python3.8/site-packages/httpx/client.py", line 273, in _get_response
    return await get_response(request)
  File "/Users/florimond/.pyenv/versions/3.8.0/lib/python3.8/site-packages/httpx/middleware/redirect.py", line 31, in __call__
    response = await get_response(request)
  File "/Users/florimond/.pyenv/versions/3.8.0/lib/python3.8/site-packages/httpx/client.py", line 227, in get_response
    response = await dispatch.send(
  File "/Users/florimond/.pyenv/versions/3.8.0/lib/python3.8/site-packages/httpx/dispatch/connection_pool.py", line 126, in send
    raise exc
  File "/Users/florimond/.pyenv/versions/3.8.0/lib/python3.8/site-packages/httpx/dispatch/connection_pool.py", line 120, in send
    response = await connection.send(
  File "/Users/florimond/.pyenv/versions/3.8.0/lib/python3.8/site-packages/httpx/dispatch/connection.py", line 62, in send
    response = await self.h2_connection.send(request, timeout=timeout)
  File "/Users/florimond/.pyenv/versions/3.8.0/lib/python3.8/site-packages/httpx/dispatch/http2.py", line 57, in send
    status_code, headers = await self.receive_response(stream_id, timeout)
  File "/Users/florimond/.pyenv/versions/3.8.0/lib/python3.8/site-packages/httpx/dispatch/http2.py", line 176, in receive_response
    event = await self.receive_event(stream_id, timeout)
  File "/Users/florimond/.pyenv/versions/3.8.0/lib/python3.8/site-packages/httpx/dispatch/http2.py", line 211, in receive_event
    data = await self.stream.read(self.READ_NUM_BYTES, timeout, flag=flag)
  File "/Users/florimond/.pyenv/versions/3.8.0/lib/python3.8/site-packages/httpx/concurrency/asyncio.py", line 114, in read
    data = await asyncio.wait_for(self.stream_reader.read(n), read_timeout)
  File "/Users/florimond/.pyenv/versions/3.8.0/lib/python3.8/asyncio/tasks.py", line 483, in wait_for
    return fut.result()
  File "/Users/florimond/.pyenv/versions/3.8.0/lib/python3.8/asyncio/streams.py", line 701, in read
    await self._wait_for_data('read')
  File "/Users/florimond/.pyenv/versions/3.8.0/lib/python3.8/asyncio/streams.py", line 520, in _wait_for_data
    raise RuntimeError(
RuntimeError: read() called while another coroutine is already waiting for incoming data
```

It's definitely a race condition. I looked at [the code](https://github.com/encode/httpx/blob/6045ee242fbe0308ce9d5faf654f5de68e1a3dd8/httpx/dispatch/http2.py#L206), and roughly what we're doing is this:

```python
while not has_event():
    data = await stream.read(...)  # (A)
    ...
    data_to_send = ...
    await stream.write(data_to_send) # (B)
```

From the logs, it seems that streams (as in HTTP/2 streams) 3 and 5 both got to the same point — they received an event, and are going to reach point B). Here's what I think the event loop is doing:

- Stream 3 reaches B), then suspends.
- Stream 5 reaches B), then suspends.
- Stream 3 wakes up and reaches A), then suspends.
- Stream 5 wakes up and reaches A). At that point, it tries to `.read()` from the asyncio stream, but stream 3, *which is accessing the same asyncio stream*, is already trying to read data from it. => 💥

So, I guess the solution would be to put locks around all `.write()` and `.read()` calls within `SocketStream`?

Resolving patch:
diff --git a/httpx/concurrency/asyncio.py b/httpx/concurrency/asyncio.py
--- a/httpx/concurrency/asyncio.py
+++ b/httpx/concurrency/asyncio.py
@@ -51,6 +51,7 @@ def __init__(
         self.stream_reader = stream_reader
         self.stream_writer = stream_writer
         self.timeout = timeout
+        self.read_lock = asyncio.Lock()
 
         self._inner: typing.Optional[SocketStream] = None
 
@@ -111,8 +112,10 @@ async def read(
             should_raise = flag is None or flag.raise_on_read_timeout
             read_timeout = timeout.read_timeout if should_raise else 0.01
             try:
-                data = await asyncio.wait_for(self.stream_reader.read(n), read_timeout)
-                break
+                async with self.read_lock:
+                    data = await asyncio.wait_for(
+                        self.stream_reader.read(n), read_timeout
+                    )
             except asyncio.TimeoutError:
                 if should_raise:
                     raise ReadTimeout() from None
@@ -122,6 +125,8 @@ async def read(
                 # doesn't seem to allow on 3.6.
                 # See: https://github.com/encode/httpx/issues/382
                 await asyncio.sleep(0)
+            else:
+                break
 
         return data
 
diff --git a/httpx/concurrency/trio.py b/httpx/concurrency/trio.py
--- a/httpx/concurrency/trio.py
+++ b/httpx/concurrency/trio.py
@@ -32,6 +32,7 @@ def __init__(
         self.stream = stream
         self.timeout = timeout
         self.write_buffer = b""
+        self.read_lock = trio.Lock()
         self.write_lock = trio.Lock()
 
     async def start_tls(
@@ -74,7 +75,8 @@ async def read(
             read_timeout = _or_inf(timeout.read_timeout if should_raise else 0.01)
 
             with trio.move_on_after(read_timeout):
-                return await self.stream.receive_some(max_bytes=n)
+                async with self.read_lock:
+                    return await self.stream.receive_some(max_bytes=n)
 
             if should_raise:
                 raise ReadTimeout() from None


Test patch:
diff --git a/tests/concurrency.py b/tests/concurrency.py
--- a/tests/concurrency.py
+++ b/tests/concurrency.py
@@ -5,8 +5,12 @@
 
 import asyncio
 import functools
+import typing
+
+import trio
 
 from httpx import AsyncioBackend
+from httpx.concurrency.trio import TrioBackend
 
 
 @functools.singledispatch
@@ -19,13 +23,24 @@ async def _sleep_asyncio(backend, seconds: int):
     await asyncio.sleep(seconds)
 
 
-try:
-    import trio
-    from httpx.concurrency.trio import TrioBackend
-except ImportError:  # pragma: no cover
-    pass
-else:
+@sleep.register(TrioBackend)
+async def _sleep_trio(backend, seconds: int):
+    await trio.sleep(seconds)
+
+
+@functools.singledispatch
+async def run_concurrently(backend, *coroutines: typing.Callable[[], typing.Awaitable]):
+    raise NotImplementedError  # pragma: no cover
+
+
+@run_concurrently.register(AsyncioBackend)
+async def _run_concurrently_asyncio(backend, *coroutines):
+    coros = (coroutine() for coroutine in coroutines)
+    await asyncio.gather(*coros)
+
 
-    @sleep.register(TrioBackend)
-    async def _sleep_trio(backend, seconds: int):
-        await trio.sleep(seconds)
+@run_concurrently.register(TrioBackend)
+async def _run_concurrently_trio(backend, *coroutines):
+    async with trio.open_nursery() as nursery:
+        for coroutine in coroutines:
+            nursery.start_soon(coroutine)
diff --git a/tests/test_concurrency.py b/tests/test_concurrency.py
--- a/tests/test_concurrency.py
+++ b/tests/test_concurrency.py
@@ -5,6 +5,7 @@
 
 from httpx import AsyncioBackend, HTTPVersionConfig, SSLConfig, TimeoutConfig
 from httpx.concurrency.trio import TrioBackend
+from tests.concurrency import run_concurrently
 
 
 @pytest.mark.parametrize(
@@ -68,3 +69,19 @@ async def test_start_tls_on_socket_stream(https_server, backend, get_cipher):
 
     finally:
         await stream.close()
+
+
+async def test_concurrent_read(server, backend):
+    """
+    Regression test for: https://github.com/encode/httpx/issues/527
+    """
+    stream = await backend.open_tcp_stream(
+        server.url.host, server.url.port, ssl_context=None, timeout=TimeoutConfig(5)
+    )
+    try:
+        await stream.write(b"GET / HTTP/1.1\r\n\r\n")
+        await run_concurrently(
+            backend, lambda: stream.read(10), lambda: stream.read(10)
+        )
+    finally:
+        await stream.close()