Speicherzugriffsfehler: bei einer globalen Variabel, mit Wert 1500

Python auf Einplatinencomputer wie Raspberry Pi, Banana Pi / Python für Micro-Controller
Antworten
Honig
User
Beiträge: 11
Registriert: Mittwoch 9. Januar 2019, 09:18

Guten Abend,

ich möchte auf einen GPIO Pin am Raspberry Pi, die Impulse innerhalb einer Sekunde zählen und das durchgehend.

Folgenden Code habe ich momentan:

Code: Alles auswählen

import RPi.GPIO as GPIO
import sys
import time

sys.setrecursionlimit(100000)

GPIO.setmode(GPIO.BCM)
GPIO.setup(21, GPIO.IN, pull_up_down=GPIO.PUD_DOWN)

count=0

def call_count(n):
        global count
        count+=1

try:
    while 1:
    	GPIO.add_event_detect(21, GPIO.RISING)
	GPIO.add_event_callback(21, call_count)
        time.sleep(1)
        GPIO.remove_event_detect(21)
        print count
        count=0
        GPIO.add_event_detect(21, GPIO.RISING)
        GPIO.add_event_callback(21, call_count)
except KeyboardInterrupt:
    GPIO.cleanup()
Der Code funktioniert auch einwandfrei. Nur nach ein paar Sekunden und Ausgaben von so 1500, bekomme ich die Fehlermeldung Speicherzugriffsfehler.
Es klingt für mich so, als würde ich den Speicher zumüllen, aber ich wüsste nicht wie. Denn die Variabel count überschreibe ich ja jedes Mal.
Soweit ich mich schon informiert habe, soll der rekursive Programmierstil in Python nicht so gut sein, aber ich habe da keine Ahnung wie ich das sonst lösen kann.

Hat vielleicht jemand eine Idee was der Fehler ist?

LG
Benutzeravatar
sparrow
User
Beiträge: 4187
Registriert: Freitag 17. April 2009, 10:28

Wo siehst du denn in dem Code Rekursion?
Und globale Variablen verwendet man nicht. Kann man der Funktion keine Parameter übergeben?
__deets__
User
Beiträge: 14529
Registriert: Mittwoch 14. Oktober 2015, 14:29

Was du da tust sollte zwar eigentlich klappen. Aber es ist extrem ungewöhnlich und darum auch nicht so Wahnsinnig überraschend, dass es kaputt geht. Lass das halt einfach. Ein Event meldet man einmal an.
Benutzeravatar
__blackjack__
User
Beiträge: 13077
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Honig: Also erst einmal solltest Du aufhören am Rekursionslimit rumzuspielen. Warum machst Du das? Hier ist nichts rekursives im Programm!

Dann sollte auf Modulebene nur Code stehen der Konstanten, Funktionen, und Klassen definiert. Keine globalen Variablen. Vergiss, dass es ``global`` gibt. Das Hauptprogramm steht üblicherweise in einer Funktion die `main()` heisst.

Verwende `True` und `False` wenn Du Wahrheitswerte meinst, nicht 1 und 0.

``as`` beim Import benutzt man um etwas importiertes umzubenennen. Hier wird aber nichts umbenannt, sondern Du willst das `GPIO`-Modul genau unter *dem* Namen importieren: ``from RPi import GPIO``.

Wenn Du den Pin änderst, musst Du durch das gesamte Programm gehen und bei jeder 21 schauen ob die für den Pin steht und sie entsprechend ändern. Das macht Arbeit und ist fehleranfällig. Dafür definiert man sich eine Konstante. Ordentlich benannt weiss der Leser dann auch überall jetzt noch die 21 steht, was der Wert bedeuten soll.

Der `cleanup()`-Aufruf gehört in einen ``finally``-Zweig, denn man will ja nicht nur hinter sich aufräumen wenn das mit Strg+C abgebrochen wird, sondern *immer* wenn der ``try``-Block verlassen wird, aus welchen Gründen auch immer.

Ich vermute mal ganz stark das Problem liegt an dem ständigen registrieren von Callbacks. Verschwinden die denn wenn man `remove_event_detect()` aufruft? Grundsätzlich würde ich glaube ich auch das einfach sein lassen, denn was bringt Dir das denn? Lass doch einfach weiterzählen, denn *sofort* nach dem entfernen der Erkennung setzt Du sie doch wieder.

Ich würde den Zähler einfach in einer Klasse kapseln und den fröhlich vor sich hin zählen lassen. Und dann einfach

Code: Alles auswählen

    while True:
        counter.count = 0
        time.sleep(1)
        print(counter.count)
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Benutzeravatar
__blackjack__
User
Beiträge: 13077
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Ungetestet:

Code: Alles auswählen

#!/usr/bin/env python3
import time
from threading import RLock

from RPi import GPIO

PIN_NUMBER = 21


class Counter:
    
    def __init__(self, pin_number):
        self.lock = RLock()
        self._count = 0
        GPIO.setup(pin_number, GPIO.IN, pull_up_down=GPIO.PUD_DOWN)
        GPIO.add_event_detect(pin_number, GPIO.RISING)
        GPIO.add_event_callback(pin_number, self.increase_count)
    
    @property
    def count(self):
        return self._count
    
    @count.setter
    def count(self, value):
        with self.lock:
            self._count = value
    
    def increase_count(self, _channel=None):
        with self.lock:
            self.count += 1
        

def main():
    GPIO.setmode(GPIO.BCM)

    try:
        counter = Counter(PIN_NUMBER)
        while True:
            counter.count = 0
            time.sleep(1)
            print(counter.count)
    except KeyboardInterrupt:
        pass
    finally:
        GPIO.cleanup()


if __name__ == '__main__':
    main()
Edit: Lock korrigiert.
Zuletzt geändert von __blackjack__ am Samstag 19. Januar 2019, 23:10, insgesamt 1-mal geändert.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Sirius3
User
Beiträge: 17741
Registriert: Sonntag 21. Oktober 2012, 17:20

@__blackjack__: bei `increase_count` hast Du ein doppeltes Lock.
Benutzeravatar
__blackjack__
User
Beiträge: 13077
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Sirius3: Ups, hab ein „reentrant lock“ draus gemacht. Threadprogrammierung ist doof. :-)
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Honig
User
Beiträge: 11
Registriert: Mittwoch 9. Januar 2019, 09:18

Vielen Dank für eure raschen Antworten.

Ihr habt Recht, da ist nichts von Rekursion drinnen, ich hatte da etwas Falsches im Kopf abgespeichert.

@__blackjack__ auf die Idee mit einen weiteren Thread bin ich noch gar nicht gekommen. Und der Code schaut so auch wesentlich schöner aus, danke. Nur erfasst dieser Code nur ca. 1/3 der Impulse. Könne es sein, dass das an RLock liegt, wobei das für mich nicht viel Sinn ergeben würde?

Edit: Die Frequenz liegt ca bei 1,5kHz.
__deets__
User
Beiträge: 14529
Registriert: Mittwoch 14. Oktober 2015, 14:29

Was passiert denn, wenn du das RLock entfernst, inklusive der with-Aufrufe? Das resultierende Programm ist nicht mehr korrekt, weil du dadurch das zuruecksetzen des Counters auf 0 zu einer "race-condition" machst, bei der du einzelne Schritte verlieren kannst. Aber wenn das groessenordnungs-maessig dann hinkommt, dann liegt es tatsaechilch an dem Lock.

Wenn nicht, dann wuerde ich den Umstieg auf eine andere GPIO-Library empfehlen. pigpio. Wuerde ich eh, aber erstmal die Ursache ermitteln.
Benutzeravatar
__blackjack__
User
Beiträge: 13077
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Honig: Welcher weitere Thread‽

@__deets__: Die „race condition“ wird zu mehr Schritten führen wenn das Rücksetzen auf 0 deswegen nicht klappt, nicht zu weniger. Denke ich mal.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Sirius3
User
Beiträge: 17741
Registriert: Sonntag 21. Oktober 2012, 17:20

Also muß man das ganze Lockfrei programmieren:

Code: Alles auswählen

#!/usr/bin/env python3
import time
from RPi import GPIO

PIN_NUMBER = 21

class Counter:
    def __init__(self, pin_number):
        self._reset_count = 0
        self._count = 0
        GPIO.setup(pin_number, GPIO.IN, pull_up_down=GPIO.PUD_DOWN)
        GPIO.add_event_detect(pin_number, GPIO.RISING)
        GPIO.add_event_callback(pin_number, self.increase_count)
    
    @property
    def count(self):
        return self._count - self._reset_count
    
    def reset(self):
        self._reset_count = self._count
    
    def increase_count(self, _channel=None):
        self._count += 1

def main():
    GPIO.setmode(GPIO.BCM)
    try:
        counter = Counter(PIN_NUMBER)
        while True:
            counter.reset()
            time.sleep(1)
            print(counter.count)
    except KeyboardInterrupt:
        pass
    finally:
        GPIO.cleanup()

if __name__ == '__main__':
    main()
Benutzeravatar
__blackjack__
User
Beiträge: 13077
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Fühle ich mich ein wenig unwohl mit, allerdings kann das auch am alten Programmierer in mir liegen. Die Zahl wächst dann ja beständig. Aber in Python bricht die ja nicht nach 2 hoch irgendwas Bits um, und wächst wahrscheinlich auch wenn es lange läuft nicht so stark das es zu viel Speicher verbraucht. :-)
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
__deets__
User
Beiträge: 14529
Registriert: Mittwoch 14. Oktober 2015, 14:29

Alternativ koennte man in Counter einfach eine Flagge setzen, die dann die naechste Zaehloperation abfragt. Das geht natuerlich nur, wenn man garantieren kann, dass danach noch Zaehlereignisse kommen, und niemand nach dem Reset den Wert 0 / niedrige Werte erwartet.
Honig
User
Beiträge: 11
Registriert: Mittwoch 9. Januar 2019, 09:18

Danke mal für eure Mühe.
__deets__ hat geschrieben: Sonntag 20. Januar 2019, 14:33 Was passiert denn, wenn du das RLock entfernst, inklusive der with-Aufrufe? Das resultierende Programm ist nicht mehr korrekt, weil du dadurch das zuruecksetzen des Counters auf 0 zu einer "race-condition" machst, bei der du einzelne Schritte verlieren kannst. Aber wenn das groessenordnungs-maessig dann hinkommt, dann liegt es tatsaechilch an dem Lock.
Größenordnungsmäßg stimmt es dann.

Wenn ich es jetzt richtig verstehe, meint ihr, dass beim count=0, Impulse dazugezählt werden, die eigentlich nicht in diese ein Sekundenzeitspanne hineingehören, oder ist es genau andersrum?

@Sirius3 ich werde diesen Code dann auch noch ausprobieren.
__deets__ hat geschrieben: Sonntag 20. Januar 2019, 16:04 Alternativ koennte man in Counter einfach eine Flagge setzen, die dann die naechste Zaehloperation abfragt. Das geht natuerlich nur, wenn man garantieren kann, dass danach noch Zaehlereignisse kommen, und niemand nach dem Reset den Wert 0 / niedrige Werte erwartet.
@__deets__ ich verstehe nicht ganz, was du meinst.
__deets__
User
Beiträge: 14529
Registriert: Mittwoch 14. Oktober 2015, 14:29

Hast du grundsaetzlich verstanden, warum das Lock da ist? Ich vermute mal eher nicht, oder?

Nebenlaeufigkeit ist schwer. Warum? Weil man nicht garantieren kann, wann was passiert.

Eine Anweisung

counter =+ 1

sieht harmlos aus. Was aber hinter den Kulissen passiert ist

1) lade counter in ein register (das ist etwas im Prozessor, was zum Rechnen benutzt wird)
1) addiere 1 hinzu
1) speichere das Register wieder in counter

Klingt einfach. Nur wenn du jetzt gleichzeitig den Hauptthread den counter auf 0 setzen laesst, dann passiert da folgendes:

2) lade 0 in ein register
2) speicher das register in counter

Und jetzt nimmst du mal einfach alle Anweisungen her, und verschachtelst sie. Denn genau das kann beim nebenlaeufigken Programmieren passieren.


1) lade counter in ein register (das ist etwas im Prozessor, was zum Rechnen benutzt wird)
2) lade 0 in ein register
1) addiere 1 hinzu
1) speichere das Register wieder in counter
2) speicher das register in counter

So zB verlierst du eine Stelle. Denn das Ergebnis ist 0, obwohl eigentlich noch die Addition erfolgen muesste und es damit 1 waere.

Sirius3 umgeht das Problem, indem er einfach statt den counter so zu manipulieren einfach eine zweite Variable einfuehrt, deren Inhalt immer abgezogen wird. Da die Variable nur von einem Thread aus beschrieben wird, und nur im anderen gelesen (gleiches gilt fuer den counter selbst, nur mit umgedrehten Rollen der Threads) ist das sicher gegen Verlust von Zaehlereignissen.

Mein Vorschlag arbeitet leicht anders: da soll der Haupthread counter nicht manipulieren, aber setzt eine Flagge/Variable, die dann dem anderen Thread, der den Counter hochzaehlt, sagt: mach nicht nur dass, sondern resette den counter. Allerdings habe ich darueber nochmal nachgedacht, und bin mir im Moment nicht sicher, ob das wirklich hilft. Eher nicht. Denn hier kann es passieren, dass zwischen dem lesen des counters und dem danachfolgenden setzen der Flagge doch wieder ein Ereignis reinkommt - und damit zaehlt man nicht alles mit. Sirius3 Weg ist also besser.
Benutzeravatar
__blackjack__
User
Beiträge: 13077
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@__deets__: Das was Du da beschreibst kann nicht passieren weil ja beide Threads ihre ”eigenen Register” haben, also kann nicht die 1 auf die 0 addiert werden. Wenn das passieren könnte, dann ist der Code der den Kontextwechsel zwischen Threads macht ziemlich im Eimer.

@Honig: Was passieren kann wenn Du das `lock` entfernst ist das ein ``count=0`` den Counter *gar nicht* zurücksetzt. Das erhöhen von `count` um eins sind ja eigentlich zwei Schritt die nacheinander passieren: Alten Wert von `count` abfragen, und neuen um eins erhöhten Wert an `count` zuweisen. Und wenn das Rücksetzen dazwischen kommt, dann ist es als wäre es gar nicht passiert. Hier mal die beiden Threads in atomare Aktionen (bezüglich `count`) dargestellt mit thread.schritt) vor den einzelnen Schritten:

1.1) count = 0
2.1) tmp = count
2.2) count = tmp + 1

Wenn das so wie's da steht ausgeführt wird ist alles okay. `count` hätte danach den Wert 1. Auch das hier wäre okay:

2.1) tmp = count
2.2) count = tmp + 1
1.1) count = 0

`count` hätte danach den Wert 0. Problematisch ist diese dritte mögliche Variante:

2.1) tmp = count
1.1) count = 0
2.2) count = tmp + 1

Hiernach hat `count` den Wert 43 wenn es vorher den Wert `42` hatte. Die Zuweisung von 0 ist einfach verloren gegangen und es wurde weiter gezählt als wenn nichts gewesen wäre.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Honig
User
Beiträge: 11
Registriert: Mittwoch 9. Januar 2019, 09:18

@__deets__ & @__blackjack__ danke für eure ausführliche Erklärung, jetzt habe ich verstanden was das Problem ist.

Ich hätte da dann noch eine andere Frage. Wenn ich jetzt den Raspberry Zero verwende, hat der Prozessor einen CPU-Takt von 1000 MHz und nur einen Core, dazu würde ich das Raspbian lite Betriebssystem verwende, welche Frequenz kann ich dann ca. höchstens am GPIO-Pin Messen? Es geht mir dabei jetzt um keine Zahl, sondern nur um die Größenordnung. Hat da jemand Ideen/Erfahrungen?
__deets__
User
Beiträge: 14529
Registriert: Mittwoch 14. Oktober 2015, 14:29

Das ist so nicht einfach zu beantworten. Das ist ein Linux-OS. Das macht noch ca 3 andere Sachen, waehrend dein Programm laeuft. Da ist die CPU-Frequenz an sich ueberhaupt nicht das Problem. Sondern das Tuning des Gesamtsystems.

Denn wenn du 20 Sekunden lang mit 20MHz oder so GPIO Daten gesampelt hast, dann kommt der Scheduler daher & schmeisst dein Programm erstmal von seiner Zeitscheibe - weil gerade ein Netzwerkpaket von Facebook oder was auch immer reingekommen ist. Und damit bricht ploetztlich die Datenrate auf wenige kHZ ein.

Was nun genau das richtige Vorgehen fuer dein Problem ist, kommt darauf an, was das System denn genau tun soll. Nur zaehlen? Wenn ja, dann nimm dir einen Arduino. Der kann das besser und zuverlaessiger. Kannst du ja an den PI haengen und per serieller Schnittstelle oder so abfragen, was gerade der Zaehlstand ist.
__deets__
User
Beiträge: 14529
Registriert: Mittwoch 14. Oktober 2015, 14:29

@__blackjack__: yupp, da habe ich mich irgendwie verbaselt.
Honig
User
Beiträge: 11
Registriert: Mittwoch 9. Januar 2019, 09:18

@__deets__ ok danke für deine Antwort, dann werde ich noch schauen wie ich es am besten mache.

Vielen Dank an euch Sirius3, __blackjack__ und __deets__ ihr habt mir sehr geholfen.

Schönen Abend noch :-)
Antworten