Что блокирует мой основной поток в этом боте Discord?

#python #asynchronous #async-await #discord.py #python-asyncio

#питон #асинхронный #асинхронный-ожидание #discord.py #python-асинхронный

Вопрос:

Описание

Прежде всего, я довольно новичок в разработке async / await, особенно с Python. На данный момент я разрабатываю бота Discord, в котором игроки могут выполнять slots <bet> . Отображается вставка с отредактированными смайликами.

Встраивание слотов

При первом вызове команды встраивание отображает вращающийся gif-смайлик, который затем «редактируется», чтобы через несколько секунд стать настоящим смайликом (чтобы создать впечатление, что слоты «вращаются»):

введите описание изображения здесь

После его завершения игрок может выбрать функцию «повторить попытку», чтобы повторить процесс снова. Итак, в целом, поток таков:

  1. Игрок вызывает команду «слот» slots <bet>
  2. появляются 3 «вращающихся» смайлика, и через 0,5 секунды 1 после другого переключается на «статическую» версию
  3. Игрок может нажать кнопку «повторить попытку», и процесс повторяется.

Код

utilities.py

 import asyncio
from functools import wraps, partial

def sync_to_async(func):
    @wraps(func)
    async def run(*args, loop=None, executor=None, **kwargs):
        if loop is None:
            loop = asyncio.get_event_loop()
        pfunc = partial(func, *args, **kwargs)
        return await loop.run_in_executor(executor, pfunc)

    return run
  

slotmachine.py

 import asyncio
import random
from enum import Enum
from typing import Union

from discord import Color, Embed, Message, Reaction, User
from discord.ext.commands import Bot, Context

from utilities import sync_to_async

import arrow

class SlotMachine:
    class Reel(Enum):
        BANANA = "🍌"
        CHERRY = "🍒"
        GRAPE = "🍇"
        DOLLAR = "💵"
        MONEY = "🤑"

    symbols = list(Reel)
    animated_emoji = "<a:slots:765434893781041183>"
    row_of_three_payout = {
        Reel.BANANA: 2,
        Reel.CHERRY: 4,
        Reel.GRAPE: 8,
        Reel.DOLLAR: 15,
        Reel.MONEY: 20,
    }
    row_of_two_payout = {
        Reel.BANANA: 1,
        Reel.CHERRY: 2,
        Reel.GRAPE: 4,
        Reel.DOLLAR: 0,
        Reel.MONEY: 0,
    }

    def __init__(self, ctx, bot, user, coins, bet):
        self.ctx = ctx
        self.bot = bot
        self.user = user
        self.bet = bet
        self.profit = 0
        self.initial_coins = coins
        self.coins = coins
        self.message = None

    @sync_to_async
    def has_row_of_three(self, slots: list) -> bool:
        if all(i == slots[0] for i in slots):
            return SlotMachine.row_of_three_payout[slots[0]]

    @sync_to_async
    def has_row_of_two(self, slots: list) -> bool:
        if slots[0] == slots[1]:
            return True
        elif slots[1] == slots[2]:
            return True
        else:
            return False

    async def calculate_payout_multiplier(self, slots: list):
        has_three, has_two = await asyncio.gather(
            self.has_row_of_three(slots), self.has_row_of_two(slots)
        )
        if has_three:
            return SlotMachine.row_of_three_payout[slots[0]]
        elif has_two:
            return SlotMachine.row_of_two_payout[slots[1]]
        else:
            return 0

    @sync_to_async
    def generate_embed(self, embed_color, multiplier, slots):
        embed = Embed(title=f"Slots 🎰", color=embed_color)
        embed.add_field(
            name="Spin",
            value=f"{' | '.join(slots)}",
            inline=False,
        )
        multiplier = f"{multiplier}x" if multiplier > 0 else "None"
        embed.add_field(name="Multiplier", value=multiplier)
        embed.add_field(name="Profit", value=self.profit)
        embed.add_field(name="Coins", value=self.coins)
        embed.add_field(name="Again?", value="React with 🔁 to play again.")
        embed.set_footer(text=self.user, icon_url=self.user.avatar_url)
        return embed

    async def play(self):
        slots = [random.choice(SlotMachine.symbols) for _ in range(3)]
        payout_multiplier = await self.calculate_payout_multiplier(slots)
        return (self.bet * payout_multiplier, payout_multiplier, slots)

    @sync_to_async
    def generate_spin_embed(self, display) -> Embed:
        embed = Embed(title=f"Slots 🎰", color=Color.from_rgb(0, 0, 0))
        embed.add_field(
            name="Spin",
            value=" | ".join(display),
            inline=False,
        )
        embed.set_footer(text=self.user, icon_url=self.user.avatar_url)
        return embed

    async def spin_animation(self, slots) -> None:
        display = [SlotMachine.animated_emoji] * 3
        embed = await self.generate_spin_embed(display)
        await self.message.edit(embed=embed)
        for i in range(len(slots)):
            await asyncio.sleep(0.5)
            display[i] = slots[i].value
            embed = await self.generate_spin_embed(display)
            await self.message.edit(embed=embed)

    def is_valid_reaction(self, reaction: Reaction, user: User):
        return (
            user == self.user
            and str(reaction.emoji) in ("🔁")
            and reaction.message.id == self.message.id
        )

    async def game_loop(self):
        has_busted = False
        is_first = True
        self.message = await self.ctx.send(embed=Embed(title="⌛ Loading..."))
        try:
            while not has_busted:
                money_out, multiplier, slots = await self.play()
                self.profit = money_out - self.bet
                self.coins  = self.profit
                await self.spin_animation(slots)

                if self.coins <= 0:
                    self.coins = 0
                    has_busted = True
                    await self.message.edit(
                        embed=await self.generate_embed(
                            Color.red(), multiplier, [s.value for s in slots]
                        )
                    )
                else:
                    if self.profit == 0:
                        embed_color = Color.from_rgb(0, 0, 0)
                    elif self.profit > 0:
                        embed_color = Color.green()
                    else:
                        embed_color = Color.red()

                    embed = await self.generate_embed(
                        embed_color, multiplier, [s.value for s in slots]
                    )
                    await asyncio.sleep(0.5)
                    await self.message.edit(embed=embed)

                    if is_first:
                        await self.message.add_reaction("🔁")
                        is_first = False
                    else:
                        await self.message.remove_reaction("🔁", self.user)

                    if self.bet > self.coins:
                        has_busted = True
                    else:
                        await self.bot.wait_for(
                            "reaction_add", timeout=30, check=self.is_valid_reaction
                        )

            # They busted at this point
            amount = self.bet if self.bet > self.coins else self.initial_coins
            await self.bot.api.modify_gambling_profit(
                user_id=self.user.id, game="slots", amount=-1 * amount
            )
        except asyncio.TimeoutError:
            if self.initial_coins > self.coins:
                amount = -1 * (self.initial_coins - self.coins)
            else:
                amount = self.coins - self.initial_coins
            await self.bot.api.modify_gambling_profit(
                user_id=self.user.id, game="slots", amount=amount
            )
  

commands.py

     @commands.command(name="slots", aliases=["slot"])
    @commands.cooldown(1, 5, commands.BucketType.user)
    async def slots_command(self, ctx: Context, bet: int) -> None:
        user_id = ctx.author.id
        if await self.currently_playing.get(f"gambling.{user_id}"):
            await ctx.send("❌ Already in another game.")
            return
        else:
            await self.currently_playing.set(f"gambling.{user_id}", 1)

        if not await self.is_valid_bet(bet):
            await ctx.send("❌ Bet must be a positive whole number.")
            return

        stats = await self.bot.api.get_economy_user(user_id=user_id)
        coins = stats["coins"]

        if await self.has_enough_coins(user_id, coins, bet):
            await SlotMachine(ctx, self.bot, ctx.author, coins, bet).game_loop()
        else:
            await ctx.send("❌ Not enough coins")

        await self.currently_playing.delete(f"gambling.{user_id}")
  

Проблема

Когда я тестировал с 1 человеком, это работало фантастически; однако, когда несколько человек выполняют одну и ту же команду «слоты», все получают огромное отставание. Это почти так, как если бы один слот ‘run’ ожидал другого.

Вопросы

  1. Кто-нибудь видит, что заметно не так с кодом? Есть ли что-то, что блокирует основной поток? Я так полагаю?
  2. Какие методы я могу использовать для отладки / расследования этой проблемы? В настоящее время я запускаю debug на asyncio, но он не выдает никаких предупреждений о slots команде?

На данный момент я настолько расстроен, что собираюсь перейти на более «асинхронный» дружественный язык, такой как go или javascript . Хотя это немного преувеличенная / начинающая мысль 😉

Комментарии:

1. Я бы рекомендовал избегать столь интенсивного (или любого другого, если вы не знаете, что делаете) использования sync_to_async . В вашем текущем коде совершенно неясно, какая функция действительно асинхронна, какая синхронизируется, но не блокируется, а какая синхронизируется и блокируется. Например, такие методы, как has_row_of_two definitely, не нуждаются в декораторе, они могут оставаться синхронизированными, и вы можете просто вызывать их в обычном режиме.