asyncio VS mehrere threads

Sockets, TCP/IP, (XML-)RPC und ähnliche Themen gehören in dieses Forum
Antworten
LMintUser
User
Beiträge: 4
Registriert: Freitag 8. November 2019, 21:22

Mittwoch 20. November 2019, 22:58

Ich steh momentan etwas auf dem Schlauch weil ich für mein Problem die maximale Performance erreichen möchte und bisher eine Lösung sowohl mit asyncio habe als auch durch den Einsatz mehrerer threads - gefühlt ist die Performance bisher gleich mies (vielleicht liegts auch am Internet hier). Ich möchte zeitgleich von mehreren Kryptohandelsplätzen die Orderbücher beziehen und dies soll nach Möglichkeit relativ performant ablaufen.

Lösung mit mehreren threads:

Code: Alles auswählen

import requests
import concurrent.futures

output1 = "global"
output2 = "global2"
output3 = "global3"
output4 = "global4"
output5 = "global5"
output6 = "global6"
output7 = "global7"
output8 = "global8"

def task1():
    response1 = requests.get('https://anycoindirect.eu/api/public/buyprices?CoinCode=BTC&FiatCode=EUR&CoinAmount=1')
    global output1
    output1 = response1

task1()

def task2():
    response2 = requests.get('https://anycoindirect.eu/api/public/buyprices?CoinCode=ETH&FiatCode=EUR&CoinAmount=1')
    global output2
    output2 = response2

task2()

def task3():
    response3 = requests.get('https://anycoindirect.eu/api/public/buyprices?CoinCode=XRP&FiatCode=EUR&CoinAmount=1')
    global output3
    output3 = response3

task3()

def task4():
    response4 = requests.get('https://anycoindirect.eu/api/public/buyprices?CoinCode=BCH&FiatCode=EUR&CoinAmount=1')
    global output4
    output4 = response4

task4()

def task5():
    response5 = requests.get('https://anycoindirect.eu/api/public/buyprices?CoinCode=LTC&FiatCode=EUR&CoinAmount=1')
    global output5
    output5 = response5

task5()

def task6():
    response6 = requests.get('https://anycoindirect.eu/api/public/buyprices?CoinCode=EOS&FiatCode=EUR&CoinAmount=1')
    global output6
    output6 = response6

task6()

def task7():
    response7 = requests.get('https://anycoindirect.eu/api/public/buyprices?CoinCode=XLM&FiatCode=EUR&CoinAmount=1')
    global output7
    output7 = response7

task7()

def task8():
    response8 = requests.get('https://anycoindirect.eu/api/public/buyprices?CoinCode=ADA&FiatCode=EUR&CoinAmount=1')
    global output8
    output8 = response8

task8()

def main():
    executor = concurrent.futures.ThreadPoolExecutor(max_workers=9)
    run1 = executor.submit(task1())
    run2 = executor.submit(task2())
    run3 = executor.submit(task3())
    run4 = executor.submit(task4())
    run5 = executor.submit(task5())
    run6 = executor.submit(task6())
    run7 = executor.submit(task7())
    run8 = executor.submit(task8())
    print(output1.text)
    print(output2.text)
    print(output3.text)
    print(output4.text)
    print(output5.text)
    print(output6.text)
    print(output7.text)
    print(output8.text)

if __name__ == '__main__':
    main()
Hab den Code hier mal stark gekürzt die Anzahl der APIs ist deutlich länger aber es geht ja nur ums Prinzip.


Lösung mit asyncio:

Code: Alles auswählen

import aiohttp
import asyncio

async def fetch(session, url):
    async with session.get(url) as response:
        return await response.text()

async def main():
    async with aiohttp.ClientSession() as session:
        list = [
                'https://anycoindirect.eu/api/public/buyprices?CoinCode=BTC&FiatCode=EUR&CoinAmount=1',
                'https://anycoindirect.eu/api/public/buyprices?CoinCode=ETH&FiatCode=EUR&CoinAmount=1',
                'https://anycoindirect.eu/api/public/buyprices?CoinCode=XRP&FiatCode=EUR&CoinAmount=1',
                'https://anycoindirect.eu/api/public/buyprices?CoinCode=BCH&FiatCode=EUR&CoinAmount=1',
                'https://anycoindirect.eu/api/public/buyprices?CoinCode=LTC&FiatCode=EUR&CoinAmount=1',
                'https://anycoindirect.eu/api/public/buyprices?CoinCode=EOS&FiatCode=EUR&CoinAmount=1',
                'https://anycoindirect.eu/api/public/buyprices?CoinCode=XLM&FiatCode=EUR&CoinAmount=1',
                'https://anycoindirect.eu/api/public/buyprices?CoinCode=ADA&FiatCode=EUR&CoinAmount=1',
                'https://anycoindirect.eu/api/public/buyprices?CoinCode=XMR&FiatCode=EUR&CoinAmount=1',
                'https://bittrex.com/api/v1.1/public/getorderbook?market=USD-BTC&type=sell',
                'https://bittrex.com/api/v1.1/public/getorderbook?market=USD-ETH&type=sell',
                'https://bittrex.com/api/v1.1/public/getorderbook?market=USD-XRP&type=sell',
                'https://bittrex.com/api/v1.1/public/getorderbook?market=USD-BCH&type=sell',
                'https://bittrex.com/api/v1.1/public/getorderbook?market=USD-LTC&type=sell',
                'https://bittrex.com/api/v1.1/public/getorderbook?market=USD-ADA&type=sell',
                'https://api.bithumb.com/public/orderbook/BTC',
                'https://api.bithumb.com/public/orderbook/ETH',
                'https://api.bithumb.com/public/orderbook/XRP',
                'https://api.bithumb.com/public/orderbook/BCH',
                'https://api.bithumb.com/public/orderbook/LTC',
                'https://api.bithumb.com/public/orderbook/EOS',
                'https://api.bithumb.com/public/orderbook/XLM',
                'https://api.bithumb.com/public/orderbook/ADA',
                'https://api.bithumb.com/public/orderbook/XMR'
                ]

        for counter in range(len(list)):
            html = await fetch(session, list[counter])
            print(html)

if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())
Hab den Code hier ebenfalls stark gekürzt (API Liste ist deutlich länger) aber ich denke das Prinzip ist klar. Da ich gerade frisch in Python einsteige ist auch die Syntax wohl noch stark verbesserungswürdig - komme aus der Java Welt deshalb sind so Sachen wie das anlegen globaler Variablen für mich noch etwas gewöhnungsbedürftig in Python (wenn es eine elegantere Methode gibt gerne Links und Hinweise).
__deets__
User
Beiträge: 6861
Registriert: Mittwoch 14. Oktober 2015, 14:29

Mittwoch 20. November 2019, 23:22

Globale Variablen sollte man selten bis nie anlegen. Weder in Java noch in Python.

Und in beiden Fällen ist der Code fehlerhaft. Bei den Threads solltest du callables (das equivalent in Java sind Runnable) übergeben. Du übergibst nix, weil den Rückgabewert None, und rufst einfach alles seriell auf - kein Gewinn 🤷🏼‍♂️

Und gleiches gilt für das asyncio Zeug. Schön brav asynchron eines nach dem anderen awaiten arbeitet eben alles schön brav nacheinander ab. Kein Gewinn - 🤷🏼‍♂️🤷🏼‍♂️

Hier ein Beispiel von mir wie es in asyncio besser geht: viewtopic.php?f=1&t=46901#p355589
Benutzeravatar
__blackjack__
User
Beiträge: 4681
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Mittwoch 20. November 2019, 23:33

Beides ist totaler Unsinn weil bei beiden soweit ich das sehe nichts auch nur ansatzweise parallel läuft.

Im ersten Beispiel wird alles zweimal ausgeführt, einmal weil Du nach jeder Funktionsdefinition die Funktion gleich einmal synchron aufrufst, und dann in der Hauptfunktion noch einmal jede Funktion *synchron* aufrufst, und ihren *Rückgabewert* an den `executor` übergibst, also `None` was aber nichts aufrufbares ist.

Und beim `asyncio` rufst Du die ``async``-Funktion auf und wartest dann sofort mit ``await`` das sie komplett abgearbeitet wird, bevor Du den nächsten Aufruf machst.

Warum hast Du im ersten Beispiel so viele nummerierte Funktionen statt auch eine Liste mit URLs und *eine* Funktion? Das erste ist überhaupt ganz gruselig mit den ganzen nummerierten Namen, kopierten Code, und *global*. So sollte nicht nur in Python gewöhnungsbedürftig sein, sondern grundsätzlich sollte man das in keiner modernen Programmiersprache machen. Überlege doch mal was `run1` bis `run8` sein könnte. Beziehungsweise steht das gleich am Anfang der Dokumentation vom `futures.concurrent`-Modul wo die `Executor.submit()`-Methode dokumentiert ist. Mit Beispiel.
“Give a man a fire and he's warm for a day, but set fire to him and he's warm for the rest of his life.”
— Terry Pratchett, Jingo
Sirius3
User
Beiträge: 10900
Registriert: Sonntag 21. Oktober 2012, 17:20

Mittwoch 20. November 2019, 23:34

Nummeriere keine Variablen oder Funktionen durch. Deine ganzen Funktionen sind sowieso fast identisch, können also einfach zu einer zusammengefasst werden. Variablen nicht mit irgendwelchen Dummy-Werten belegen, das ist unnötig. Warum speicherst Du den Response erst in einer Variable und referenzierst dessen Wert dann mit der output-Variable?
Benutze keine globalen Variablen, erst recht nicht mit Threads. Bei Threads kommuniziert man am besten über Queues.
Du rufst task2() gleich nach der Definition einmal auf und dann später, bevor Du mit dem Rückgabewert `submit` aufrufst. Benutze executor.map.
LMintUser
User
Beiträge: 4
Registriert: Freitag 8. November 2019, 21:22

Donnerstag 21. November 2019, 19:27

So vielen vielen Dank für eure Tipps - ich seh schon ein klasse Forum hier. Ich habe jetzt nochmal an meinem Code gewerkelt und eure Verbesserungsvorschläge berücksichtigt. Das ganze sieht auch deutlich besser aus und die Performance ist auch spürbar besser geworden. Wie man gesehen hat hab ich noch so meine Probleme mit der Python Syntax und ich glaube das könnte auch beim nachfolgenden Code noch ein Problem sein.

Version 1 Verbesserung:

Code: Alles auswählen

import requests
import concurrent.futures
import asyncio

list = []
listmarket = [
                'https://anycoindirect.eu/api/public/buyprices?CoinCode=BTC&FiatCode=EUR&CoinAmount=1',
                'https://anycoindirect.eu/api/public/buyprices?CoinCode=ETH&FiatCode=EUR&CoinAmount=1',
                'https://anycoindirect.eu/api/public/buyprices?CoinCode=XRP&FiatCode=EUR&CoinAmount=1',
                'https://anycoindirect.eu/api/public/buyprices?CoinCode=BCH&FiatCode=EUR&CoinAmount=1',
                'https://anycoindirect.eu/api/public/buyprices?CoinCode=LTC&FiatCode=EUR&CoinAmount=1',
                'https://anycoindirect.eu/api/public/buyprices?CoinCode=EOS&FiatCode=EUR&CoinAmount=1',
                'https://anycoindirect.eu/api/public/buyprices?CoinCode=XLM&FiatCode=EUR&CoinAmount=1',
                'https://anycoindirect.eu/api/public/buyprices?CoinCode=ADA&FiatCode=EUR&CoinAmount=1',
                'https://anycoindirect.eu/api/public/buyprices?CoinCode=XMR&FiatCode=EUR&CoinAmount=1',
                'https://bittrex.com/api/v1.1/public/getorderbook?market=USD-BTC&type=sell',
                'https://bittrex.com/api/v1.1/public/getorderbook?market=USD-ETH&type=sell',
                'https://bittrex.com/api/v1.1/public/getorderbook?market=USD-XRP&type=sell',
                'https://bittrex.com/api/v1.1/public/getorderbook?market=USD-BCH&type=sell',
                'https://bittrex.com/api/v1.1/public/getorderbook?market=USD-LTC&type=sell',
                'https://bittrex.com/api/v1.1/public/getorderbook?market=USD-ADA&type=sell',
                'https://api.bithumb.com/public/orderbook/BTC',
                'https://api.bithumb.com/public/orderbook/ETH',
                'https://api.bithumb.com/public/orderbook/XRP',
                'https://api.bithumb.com/public/orderbook/BCH',
                'https://api.bithumb.com/public/orderbook/LTC',
                'https://api.bithumb.com/public/orderbook/EOS',
                'https://api.bithumb.com/public/orderbook/XLM',
                'https://api.bithumb.com/public/orderbook/ADA',
                'https://api.bithumb.com/public/orderbook/XMR'
                ]


def task(index):
    response1 = requests.get(listmarket[index])
    list.append(response1)


def task1():
    response1 = requests.get('https://anycoindirect.eu/api/public/buyprices?CoinCode=BTC&FiatCode=EUR&CoinAmount=1')
    list.append(response1)


def task2():
    response2 = requests.get('https://anycoindirect.eu/api/public/buyprices?CoinCode=ETH&FiatCode=EUR&CoinAmount=1')
    list.append(response2)


def task3():
    response3 = requests.get('https://anycoindirect.eu/api/public/buyprices?CoinCode=XRP&FiatCode=EUR&CoinAmount=1')
    list.append(response3)


def task4():
    response4 = requests.get('https://anycoindirect.eu/api/public/buyprices?CoinCode=BCH&FiatCode=EUR&CoinAmount=1')
    list.append(response4)


def task5():
    response5 = requests.get('https://anycoindirect.eu/api/public/buyprices?CoinCode=LTC&FiatCode=EUR&CoinAmount=1')
    list.append(response5)


def task6():
    response6 = requests.get('https://anycoindirect.eu/api/public/buyprices?CoinCode=EOS&FiatCode=EUR&CoinAmount=1')
    list.append(response6)


def task7():
    response7 = requests.get('https://anycoindirect.eu/api/public/buyprices?CoinCode=XLM&FiatCode=EUR&CoinAmount=1')
    list.append(response7)


def task8():
    response8 = requests.get('https://anycoindirect.eu/api/public/buyprices?CoinCode=ADA&FiatCode=EUR&CoinAmount=1')
    list.append(response8)


def main():
    executor = concurrent.futures.ThreadPoolExecutor(max_workers=len(listmarket))
    for i in range(len(listmarket)):
        executor.submit(task, i)
    #executor.submit(task1)
    #executor.submit(task2)
    #executor.submit(task3)
    #executor.submit(task4)
    #executor.submit(task5)
    #executor.submit(task6)
    #executor.submit(task7)
    executor.submit(task8())
    for x in list:
        print(x.text)

if __name__ == '__main__':
    main()
Version 2 Verbesserung:

Code: Alles auswählen

import requests
import concurrent.futures

list = []
listmarket = [
                'https://anycoindirect.eu/api/public/buyprices?CoinCode=BTC&FiatCode=EUR&CoinAmount=1',
                'https://anycoindirect.eu/api/public/buyprices?CoinCode=ETH&FiatCode=EUR&CoinAmount=1',
                'https://anycoindirect.eu/api/public/buyprices?CoinCode=XRP&FiatCode=EUR&CoinAmount=1',
                'https://anycoindirect.eu/api/public/buyprices?CoinCode=BCH&FiatCode=EUR&CoinAmount=1',
                'https://anycoindirect.eu/api/public/buyprices?CoinCode=LTC&FiatCode=EUR&CoinAmount=1',
                'https://anycoindirect.eu/api/public/buyprices?CoinCode=EOS&FiatCode=EUR&CoinAmount=1',
                'https://anycoindirect.eu/api/public/buyprices?CoinCode=XLM&FiatCode=EUR&CoinAmount=1',
                'https://anycoindirect.eu/api/public/buyprices?CoinCode=ADA&FiatCode=EUR&CoinAmount=1',
                'https://anycoindirect.eu/api/public/buyprices?CoinCode=XMR&FiatCode=EUR&CoinAmount=1',
                'https://bittrex.com/api/v1.1/public/getorderbook?market=USD-BTC&type=sell',
                'https://bittrex.com/api/v1.1/public/getorderbook?market=USD-ETH&type=sell',
                'https://bittrex.com/api/v1.1/public/getorderbook?market=USD-XRP&type=sell',
                'https://bittrex.com/api/v1.1/public/getorderbook?market=USD-BCH&type=sell',
                'https://bittrex.com/api/v1.1/public/getorderbook?market=USD-LTC&type=sell',
                'https://bittrex.com/api/v1.1/public/getorderbook?market=USD-ADA&type=sell',
                'https://api.bithumb.com/public/orderbook/BTC',
                'https://api.bithumb.com/public/orderbook/ETH',
                'https://api.bithumb.com/public/orderbook/XRP',
                'https://api.bithumb.com/public/orderbook/BCH',
                'https://api.bithumb.com/public/orderbook/LTC',
                'https://api.bithumb.com/public/orderbook/EOS',
                'https://api.bithumb.com/public/orderbook/XLM',
                'https://api.bithumb.com/public/orderbook/ADA',
                'https://api.bithumb.com/public/orderbook/XMR'
                ]


def task(index):
    list.append(requests.get(listmarket[index]))


def main():
    executor = concurrent.futures.ThreadPoolExecutor(max_workers=len(listmarket))
    for i in range(len(listmarket)):
        executor.submit(task, i)

    executor.shutdown(wait=True)
    for x in list:
        print(x.text)

if __name__ == '__main__':
    main()
Das komische ist die zweite Version läuft bei mir von der Performance deutlich schlechter :roll: woran könnte das liegen? Vielleicht hatte ich auch echt Pech und es lag an der Reaktion vom Server aber hatte jetzt mehrere Testdurchläufe und gefühlt immer das gleiche Ergebnis - die erste Version lief gefühlt flotter durch. Muss heute Abend mal schauen ob es da in python eine Funktion gibt um das genau zu messen. Bzgl. asyncio steig ich noch nicht so ganz durch habe mir jetzt ein ebook von Packt geholt "Learning Concurrency in Python" - im neunten Kapitel des Buchs geht es um asyncio das lese ich mir heute Abend mal durch. Könnt ihr mir noch andere gute Literatur diesbezüglich empfehlen?
__deets__
User
Beiträge: 6861
Registriert: Mittwoch 14. Oktober 2015, 14:29

Donnerstag 21. November 2019, 20:18

Das ist etwas Kraut und Rüben. Du rufst schon wieder deinen Task auf - in diesem Fall task8(). Und ansonsten sehe ich eigentlich keinen Unterschied, ausser das du in dem einen Code lauter Funktionen definiert hast, die du eh nicht aufrufst.

Du kannst dir auch sparen, einen range zu bauen. Sondern gleich

Code: Alles auswählen

for url in listmarket:         
    executor.submit(task, url)
machen. Und musst dann im task noch nicht mal mehr auf die globale Liste zugreifen. Und gleichzeitig kannst du auch ein Ergebnisobjekt mit reingeben mit submit, und dann braucht der Task auch nicht auf die zweite globale Variable zugreifen. Die auch noch auesserst ungeschickt benannt ist, denn list ist ein eingebauter Python-Typ, und unter dem Namen auch erreichbar. Ausser bei dir.

Fuer mein Dafuerhalten sind die Varianzen die du siehst nicht durch Code, sondern auessere Einflussfaktoren erklaerbar. Fuehlen muss man uebrigens nicht, man kann auch die Zeit messen. Mit time, und mit timeit, etc. Wenn du benchmarken willst, solltest du es schon halbwegs richtig machen.
Sirius3
User
Beiträge: 10900
Registriert: Sonntag 21. Oktober 2012, 17:20

Donnerstag 21. November 2019, 20:40

Du benutzt immer noch, verbotenerweise globale Variablen. Ich hatte Dich schonmal auf executor.map aufmerksam gemacht.
__deets__
User
Beiträge: 6861
Registriert: Mittwoch 14. Oktober 2015, 14:29

Donnerstag 21. November 2019, 22:30

Achso, bei Skript 1 gibt’s eine Grund warum das so viel schneller ist. Es tut weniger. Du wartest nicht auf den executor...
Jankie
User
Beiträge: 180
Registriert: Mittwoch 26. September 2018, 14:06

Freitag 22. November 2019, 07:16

Außerdem ist "list" kein guter Variablenname, da du so die gleichnamige Funktion list() "überschreibst".
Antworten