在异步/多线程中处理可变的共享对象。替代“ .copy()”?

时间:2019-02-18 21:31:07

标签: python asynchronous reference shared-memory python-multithreading

我正在编写一个程序,该程序创建了一堆工作程序以使用aiohttp异步调用API。但是,这个问题是关于共享对象的。我认为如果我使用多线程,将会遇到相同或相似的问题。

我有一组所有工作人员共享的默认URL参数,但是这些参数的两个值在工作人员之间有所不同:

DEFAULT_PARAMS = {
    'q' : None,                         #<==CHANGES per worker
    'offset' : '0',                     #<==CHANGES per worker
    'mkt' : 'en-US',                    #<==STATIC for all workers
    'moreParams' : '<most of the data>' #<==STATIC for all workers
}  

这是我初始化Worker()类的方式:

class Worker(object):
    def __init__(self, q):
        # this copy iexpensive when > 100 workers.
        self.initial_params = DEFAULT_PARAMS.copy()
        # but witout copying entire default params dict, the next line
        # would add alter the 'q' value for all instances of Worker.
        self.initial_params.update({'q' : q})

我正在寻找一种替代方法,可以为我创建的每个新工作人员调用DEFAULT_PARAMS.copy()

弄清楚如何提出这个问题一直是一个挑战。我怀疑我的答案可能是通过实例属性在类中的某个地方。

这是我程序的一个非常准系统的例子:

import aiohttp
import asyncio

DEFUALT_PARAMS = {
    'q' : None, #<==CHANGES per worker
    'offset' : '0', #<==CHANGES per worker
    'mkt' : 'en-US', #<==STATIC for all workers
    'moreParams' : '<most of the data>' #<==STATIC for all workers
}

class Worker(object):
    def __init__(self, q):
        self.initial_params = DEFUALT_PARAMS.copy() # <==expensive
        self.initial_params.update({'q' : q}) #<==without copying, overwrites ref for all classes.

    async def call_api(self):
        async with aiohttp.ClientSession() as sesh:
            async with sesh.get(
                'https://somesearchengine.com/search?',
                params=self.initial_params
            ) as resp:
                assert resp.status == 200
                print(await resp.json())


async def main(workers, *, loop=None):
    tasks = (asyncio.ensure_future(i.call_api(), loop=loop) for i in workers)
    await asyncio.gather(*tasks)

if __name__ == "__main__":
    loop = asyncio.get_event_loop()
    queries = ['foo', 'bar', 'baz']
    workers = (Worker(i) for i in queries)
    loop.run_until_complete(main(workers, loop=loop))

2 个答案:

答案 0 :(得分:1)

即使是100名工人,复制字典也不是那么昂贵。您可以在7微秒内创建1000键字典的副本 并对其进行更新:

>>> from timeit import Timer
>>> from secrets import token_urlsafe
>>> test_dict = {token_urlsafe(): token_urlsafe() for _ in range(1000)}
>>> len(test_dict)
1000
>>> count, total = Timer("p = d.copy(); p.update({'q' : q})", "from __main__ import test_dict as d; q = 42").autorange()
>>> print(total/count*1000000)  # microseconds are 10**-6 seconds
7.146239580000611

所以我想说这里真的没有问题。

但是,您实际上是 layering 词典的内容;每个工人最多只能调整一两个键。除了创建副本之外,您可以使用collections.ChainMap() object来处理分层。 ChainMap()对象使用多个词典,并将在其中查找键,直到找到一个值。不会创建任何副本,并且在对地图进行突变时会使用最顶层的字典来设置值:

from collections import ChainMap

# ...
self.initial_params = ChainMap({'q': q}, DEFAULT_PARAMS)

创建ChainMap()对象仍然便宜:

>>> count, total = Timer("p = ChainMap({'q': q}, d)", "from __main__ import test_dict as d; q = 42; from collections import ChainMap").autorange()
>>> print(total/count*1000000)
0.5310121239999717

所以只有半微秒。当然,这是以较慢的迭代和每个键访问为代价的。这取决于aiohttp的处理方式,我建议您使用timeit模块进行自己的微基准测试,以衡量代码正在执行的实际操作的性能。

但是请注意,在尝试以这种方式处理共享状态时,始终要付出代价,对于任何并发模型,即使没有并发,在实例之间共享字典的总是有问题的

答案 1 :(得分:-2)

如果q由worker拥有,为什么不直接将其设为Worker本身的实例变量。

class Worker(object):
    def __init__(self, q):
      self.q = q

以及您想qself.q的任何地方