[Async-sig] "read-write" synchronization

Dima Tisnek dimaqq at gmail.com
Mon Jun 26 15:37:19 EDT 2017


Chris, here's a simple RWLock implementation and analysis:

```
import asyncio


class RWLock:
    def __init__(self):
        self.cond = asyncio.Condition()
        self.readers = 0
        self.writer = False

    async def lock(self, write=False):
        async with self.cond:
            # write requested: there cannot be readers or writers
            # read requested: there can be other readers but not writers
            while self.readers and write or self.writer:
                self.cond.wait()
            if write: self.writer = True
            else: self.readers += 1
            # self.cond.notifyAll() would be good taste
            # however no waiters can be unblocked by this state change

    async def unlock(self, write=False):
        async with self.cond:
            if write: self.writer = False
            else: self.readers -= 1
            self.cond.notifyAll()  # notify (one) could be used `if not write:`
```

Note that `.unlock` cannot validate that it's called by same coroutine
as `.lock` was.
That's because there's no concept for "current_thread" for coroutines
-- there can be many waiting on each other in the stack.

Obv., this code could be nicer:
* separate context managers for read and write cases
* .unlock can be automatic (if self.writer: unlock_for_write()) at the
cost of opening doors wide open to bugs
* policy can be introduced if `.lock` identified itself (by an
object(), since there's no thread id) in shared state
* notifyAll() makes real life use O(N^2) for N being number of
simultaneous write lock requests

Feel free to use it :)



On 26 June 2017 at 20:21, Chris Jerdonek <chris.jerdonek at gmail.com> wrote:
> On Mon, Jun 26, 2017 at 10:02 AM, Dima Tisnek <dimaqq at gmail.com> wrote:
>> Chris, coming back to your use-case.
>> Do you want to synchronise side-effect creation/deletion for the
>> sanity of side-effects only?
>> Or do you imply that callers' actions are synchronised too?
>> In other words, do your callers use those directories out of band?
>
> If I understand your question, the former. The callers aren't / need
> not be synchronized, and they aren't aware of the underlying
> synchronization happening inside the higher-level create() and
> delete() functions they would be using. (These are the two
> higher-level functions described in my pseudocode.)
>
> The synchronization is needed inside these create() and delete()
> functions since the low-level directory operations occur in different
> threads (because they are wrapped by run_in_executor()).
>
> --Chris
>
>>
>>
>> P.S./O.T. when it comes to directories, you probably want hierarchical
>> locks rather than RW.
>>
>>
>> On 26 June 2017 at 11:28, Chris Jerdonek <chris.jerdonek at gmail.com> wrote:
>>> On Mon, Jun 26, 2017 at 1:43 AM, Dima Tisnek <dimaqq at gmail.com> wrote:
>>>> Perhaps you can share your use-case, both as pseudo-code and a link to
>>>> real code.
>>>>
>>>> I'm specifically interested to see why/where you'd like to use a
>>>> read-write async lock, to evaluate if this is something common or
>>>> specific, and if, perhaps, some other paradigm (like queue, worker
>>>> pool, ...) may be more useful in general case.
>>>>
>>>> I'm also curious if a full set of async sync primitives may one day
>>>> lead to async monitors. Granted, simple use of async monitor is really
>>>> a future/promise, but perhaps there are complex use cases in the
>>>> UI/react domain with its promise/stream dichotomy.
>>>
>>> Thank you, Dima. In my last email I shared pseudo-code for an approach
>>> to read-write synchronization that is independent of use case. [1]
>>>
>>> For the use case, my original purpose in mind was to synchronize many
>>> small file operations on disk like creating and removing directories
>>> that possibly share intermediate segments. The real code isn't public.
>>> But these would be operations like os.makedirs() and os.removedirs()
>>> that would be wrapped by loop.run_in_executor() to be non-blocking.
>>> The directory removal using os.removedirs() is the operation I thought
>>> should require exclusive access, so as not to interfere with directory
>>> creations in progress.
>>>
>>> Perhaps a simpler, dirtier approach would be not to synchronize at all
>>> and simply retry directory creations that fail until they succeed.
>>> That could be enough to handle rare cases where simultaneous creation
>>> and removal causes an error. You could view this an EAFP approach.
>>>
>>> Either way, I think the process of thinking through patterns for
>>> read-write synchronization is helpful for getting a better general
>>> feel and understanding of async.
>>>
>>> --Chris
>>>
>>>
>>>>
>>>> Cheers,
>>>> d.
>>>>
>>>> On 25 June 2017 at 23:13, Chris Jerdonek <chris.jerdonek at gmail.com> wrote:
>>>>> I'm relatively new to async programming in Python and am thinking
>>>>> through possibilities for doing "read-write" synchronization.
>>>>>
>>>>> I'm using asyncio, and the synchronization primitives that asyncio
>>>>> exposes are relatively simple [1]. Have options for async read-write
>>>>> synchronization already been discussed in any detail?
>>>>>
>>>>> I'm interested in designs where "readers" don't need to acquire a lock
>>>>> -- only writers. It seems like one way to deal with the main race
>>>>> condition I see that comes up would be to use loop.time(). Does that
>>>>> ring a bell, or might there be a much simpler way?
>>>>>
>>>>> Thanks,
>>>>> --Chris
>>>>>
>>>>>
>>>>> [1] https://docs.python.org/3/library/asyncio-sync.html
>>>>> _______________________________________________
>>>>> Async-sig mailing list
>>>>> Async-sig at python.org
>>>>> https://mail.python.org/mailman/listinfo/async-sig
>>>>> Code of Conduct: https://www.python.org/psf/codeofconduct/


More information about the Async-sig mailing list