Сравнительные тесты Concurrent.futures и SQLAlchemy и синхронный код - PullRequest
0 голосов
/ 12 февраля 2020

У меня есть проект, в котором мне нужно загрузить ~ 70 файлов в мое приложение flask. Я изучаю параллелизм прямо сейчас, так что это похоже на идеальную практику. При использовании операторов печати одновременная версия этой функции примерно в 2–2,5 раза быстрее, чем синхронная функция.

Хотя при реальной записи в базу данных SQLite это занимает примерно столько же времени.

Оригинальное веселье c:

@app.route('/test_sync')
def auto_add():

    t0 = time.time()

    # Code does not work without changing directory. better option?
    os.chdir('my_app/static/tracks')

    list_dir = os.listdir('my_app/static/tracks')

    # list_dir consists of .mp3 and .jpg files
    for filename in list_dir:
        if filename.endswith('.mp3'):
            try:
                thumbnail = [thumb for thumb in list_dir if thumb == filename[:-4] + '.jpg'][0]
            except Exception:
                print(f'ERROR - COULD NOT FIND THUMB for { filename }')

            resize_image(thumbnail)

            with open(filename, 'rb') as f, open(thumbnail, 'rb') as t:

                track = Track(

                    title=filename[15:-4], 
                    artist='Sam Gellaitry',
                    description='No desc.', 
                    thumbnail=t.read(),
                    binary_audio=f.read()
                )

        else:
            continue


        db.session.add(track)

    db.session.commit()
    elapsed = time.time() - t0

    return f'Uploaded all tracks in {elapsed} seconds.'

Параллельное веселье c (s):

@app.route('/test_concurrent')
def auto_add_concurrent():

    t0 = time.time()
    MAX_WORKERS = 40

    os.chdir('/my_app/static/tracks')
    list_dir = os.listdir('/my_app/static/tracks')
    mp3_list = [x for x in list_dir if x.endswith('.mp3')]

    with futures.ThreadPoolExecutor(MAX_WORKERS) as executor:
        res = executor.map(add_one_file, mp3_list)

    for x in res:
        db.session.add(x)

    db.session.commit()
    elapsed = time.time() - t0

    return f'Uploaded all tracks in {elapsed} seconds.'

----- 

def add_one_file(filename):

    list_dir = os.listdir('/my_app/static/tracks')

    try:
        thumbnail = [thumb for thumb in list_dir if thumb == filename[:-4] + '.jpg'][0]

    except Exception:
        print(f'ERROR - COULD NOT FIND THUMB for { filename }')

    resize_image(thumbnail)

    with open(filename, 'rb') as f, open(thumbnail, 'rb') as t:

        track = Track(

            title=filename[15:-4], 
            artist='Sam Gellaitry',
            description='No desc.', 
            thumbnail=t.read(),
            binary_audio=f.read()
        )

    return track

Вот веселье resize_image c для полноты

def resize_image(thumbnail):

    with Image.open(thumbnail) as img:
        img.resize((500, 500))
        img.save(thumbnail)

    return thumbnail

И тесты:

/test_concurrent (with print statements)
Uploaded all tracks in 0.7054300308227539 seconds.

/test_sync
Uploaded all tracks in 1.8661110401153564 seconds.

------
/test_concurrent (with db.session.add/db.session.commit)
Uploaded all tracks in 5.303245782852173 seconds.

/test_sync 
Uploaded all tracks in 6.123792886734009 seconds.

Что я делаю не так с этим параллельным кодом и как я могу его оптимизировать?

1 Ответ

2 голосов
/ 13 февраля 2020

Кажется, что записи в БД доминируют над вашими временами, и они обычно не выигрывают от распараллеливания при записи множества строк в одну и ту же таблицу или в случае SQLite для одной и той же БД. Вместо добавления объектов ORM 1 на 1 в сеанс выполните массовую вставку:

db.session.bulk_save_objects(list(res))

В вашем текущем коде ORM должен вставлять объекты Track по одному во время гриппа sh непосредственно перед фиксацией, чтобы получить их первичные ключи после вставки. Session.bulk_save_objects по умолчанию этого не делает, что означает, что объекты становятся менее пригодными после использования - они, например, не добавляются в сеанс, - но в вашем случае это не кажется проблемой.

«Я вставляю 400 000 строк с помощью ORM, и это действительно медленно!» - хорошее прочтение на эту тему.


В качестве примечания, при работе с файлами лучше по возможности избегать ситуаций TOCTOU . Другими словами, не используйте

thumbnail = [thumb for thumb in list_dir if thumb == filename[:-4] + '.jpg'][0]

, чтобы проверить, существует ли файл, используйте os.path.isfile() или что-то подобное, если нужно, но вы должны просто попытаться открыть его, а затем обработать ошибку, если она не может быть открыта:

thumbnail = filename[:-4] + '.jpg'

try:
    resize_image(thumbnail)

except FileNotFoundError:
    print(f'ERROR - COULD NOT FIND THUMB for { filename }')
    # Note that the latter open attempt will fail as well, if this fails

...
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...