web-dev-qa-db-ja.com

個別にまたは「ツリー」で実行できるタスクがあり、各タスクはブロックまたは非ブロックにすることができます

特定のタスクを実行する必要のあるフレームワークを作成しています。この例として、自動車の製造を使用します。したがって、個々のタスクは_weld metal_または_screw bolt_のようなものになります。しかし、タスクのコレクションもあります。私はそれらをjobsと呼びます。各ジョブはX個のタスクを持つことができるため、これらの2つのタスクを組み合わせると、ジョブ__build an engine_を取得できます。

_build an engine
+-- weld metal
+-- screw bolts
_

さらに、ジョブはサブジョブを持つことができます:

_build a car
+-- build an engine
|   +-- weld metal
|   +-- screw bolts
+-- build a frame
|   +-- weld metal
|   +-- screw bolts
|   +-- Paint
+-- combine engine and frame
_

つまり、基本的に私のツリーの各ノードはジョブであり、各リーフはタスクです。これは非常に簡単です。クラスは次のようになります。

_class Task:
    def do(self):
        raise NotImplementedError('abstract method')

class Job(Task):
    def __init__(self):
        self.tasks = []

    def do(self):
        for task in self.tasks:
            task.do()
_

そして今、私はWeldMetalからScrewBoltsTaskをサブクラス化するだけです。

これを並列化したいときに問題が発生します。どういうわけか、各タスク(およびジョブ)が実行をブロックすべきか、それともブロックせずに別のスレッドで実行すべきかを伝える必要があります。時々私の仕事は他の仕事からの結果も必要としますが、それらの仕事は互いに並行して実行されるかもしれません。たとえば、上の自動車の建物の場合:

  • 金属が溶接される前にねじ込むことができないため、_weld metal_はブロックする必要があります。
  • _build an engine_および_build a frame_は、独自のスレッドで実行できます(これらのスレッド内では、_weld metal_および_screw bolts_は特定のスレッドをブロックする必要があります)。
  • _combine engine and frame_は、_build an engine_および_build a frame_が完了するまで待機する必要があります。

正直なところ、どこから始めればよいかわかりません。最初はThreadPoolExecutorJob.do()で使用することを考えましたが、一部のジョブのみをブロックし、他のジョブをブロックしない方法や、別の問題についてはわかりませんつまり、タスクは、ジョブの内部に存在することなく「単独」で実行する必要がある場合があります。つまり、WeldMetal(block=False).do()が有効である必要があります。

私はこれを最初に使用してしまいました:

_class Task:
    def do(self):
        raise NotImplementedError('abstract method')

    def run(self):
        if self.block:
            self.do()
        else:
            threading.Thread(target=self.do).start()
_

そして、これはほとんどの場合に機能しますが、_combine engine and frame_が_build an engine_と_build a frame_の両方が完了するのを待つ必要があるときに問題が発生します。

2
Markus Meskanen

2つの懸念を1つの解決策にまとめようとしていますが、その解決策では実際には両方の懸念をカバーできないため、問題が発生しています。

一方で、ブロッキングタスク(同じジョブの他のタスクと並行して実行されない可能性があるタスク)があり、他方で、タスク間に依存関係があります(タスクは、1つ以上の他の特定のタスクが完了するまで開始できません。完了しました)。

ジョブの最初のタスクであるタスクをブロックするためのソリューションがありますが、依存関係や、非ブロックタスクの後に来るタスクをブロックするためのソリューションがあります(Paintがブロックしている場合でも、実行できます) _screw bolts_ジョブの一部として_build a frame_と並行して)。

ブロッキングと依存関係の両方を処理するには、次のようなものを使用できます。

_class Task:
    def __init__(self):
        self._done = threading.Event()
        self.dependencies = []

    def wait_for_completion(self):
        self._done.wait()

    def do(self):
        raise NotImplementedError('abstract method')

    def do_async(self):
        for depends in self.dependencies:
            depends.wait_for_completion()

        self.do()
        self._done.set()

    def run(self):
        threading.Thread(target=self.do_async).start()


class Job(Task):
    def __init__(self):
        super(Job, self).__init__()
        self.tasks = []

    def do(self):
        started_tasks = []
        for task in self.tasks:
            if task.block:
                self._wait_for_tasks(started_tasks)
                started_tasks = []
            else:
                started_tasks.append(task)
            task.run()
            if task.block:
                task.wait_for_completion()
        self._wait_for_tasks(self.tasks)

    def _wait_for_tasks(self, task_list):
        for task in task_list:
            task.wait_for_completion()
_

Taskクラスは、依存関係によってイベントが設定されるまでタスクのトレッドをブロックすることにより、依存関係の処理を行います。 Jobクラスを拡張して、そのジョブ内の他のタスクが実行されていない場合にのみブロックタスクが実行されるようにし、含まれているすべてのタスクが完了するまでジョブが完了として報告されないようにしました。その最後の部分は、依存関係メカニズムがジョブで正しく機能するために必要です。

スレッドをjoin() ingするのではなく、意図的にEventオブジェクトを同期に使用しました。これにより、タスクAがタスクBの前にタスクBで待機し始める場合のコードがより堅牢になります。始まった。タスクとジョブが順序付けられたシーケンスを形成する場合(単一スレッドで指定された順序ですべてを実行しても、依存関係に違反しない場合)に限り、コードはデッドロックがないことが保証されます。

asyncio は、あなたが望むことのほとんどをすでに行っているようです。例えば:

_import asyncio

async def build_a_car():
    engine_task = asyncio.create_task(build_an_engine())
    frame_task = asyncio.create_task(build_a_frame())
    engine = await engine_task
    frame = await frame_task
    await combine_engine_and_frame(engine, frame)

async def build_an_engine():
    await weld_metal()
    await screw_bolts()

async def build_a_frame():
    await weld_metal()
    await screw_bolts()
    await Paint()

asyncio.run(build_a_car())
_

asyncio.run(weld_metal())を使用して、コルーチンを単独で実行できます。階層の最上部について特別なことは何もありません。

それは私が始める場所であり、それはあなたにかなりの道を開くはずです。より重いものを探している場合は、 Celery または Kubernetes Jobs のような分散タスクキューを見ることができますが、これらは多くのコンピューティングリソースを依存関係のシーケンスを簡単にするのではなく、信頼できる方法です。

0
Karl Bielefeldt