it-swarm.com.de

Warum verwalten Programmiersprachen das synchrone / asynchrone Problem nicht automatisch?

Ich habe nicht viele Ressourcen dazu gefunden: Ich habe mich gefragt, ob es möglich/eine gute Idee ist, asynchronen Code synchron schreiben zu können.

Hier ist beispielsweise ein JavaScript-Code, der die Anzahl der in einer Datenbank gespeicherten Benutzer abruft (eine asynchrone Operation):

getNbOfUsers(function (nbOfUsers) { console.log(nbOfUsers) });

Es wäre schön, so etwas schreiben zu können:

const nbOfUsers = getNbOfUsers();
console.log(getNbOfUsers);

Der Compiler wartet also automatisch auf die Antwort und führt dann console.log. Es wird immer darauf gewartet, dass die asynchronen Vorgänge abgeschlossen sind, bevor die Ergebnisse an anderer Stelle verwendet werden müssen. Wir würden Rückrufversprechen, Async/Warten oder was auch immer, so viel weniger nutzen und müssten uns niemals Sorgen machen, ob das Ergebnis einer Operation sofort verfügbar ist oder nicht.

Fehler wären immer noch mit try/catch oder ähnlichen Optionen wie in der Sprache Swift handhabbar (hat nbOfUsers eine Ganzzahl oder einen Fehler erhalten?).

Ist es möglich? Es kann eine schreckliche Idee/eine Utopie sein ... Ich weiß es nicht.

29
Cinn

Async/await ist genau das automatisierte Management, das Sie vorschlagen, allerdings mit zwei zusätzlichen Schlüsselwörtern. Warum sind sie wichtig? Abgesehen von der Abwärtskompatibilität?

  • Ohne explizite Punkte, an denen eine Coroutine ausgesetzt und wieder aufgenommen werden kann, benötigen wir ein Typsystem, um zu erkennen, wo ein wartender Wert erwartet werden muss. Viele Programmiersprachen haben kein solches Typsystem.

  • Indem wir das Warten auf einen Wert explizit machen, können wir auch erwartete Werte als erstklassige Objekte weitergeben: Versprechen. Dies kann beim Schreiben von Code höherer Ordnung sehr nützlich sein.

  • Asynchroner Code hat sehr tiefgreifende Auswirkungen auf das Ausführungsmodell einer Sprache, ähnlich wie das Fehlen oder Vorhandensein von Ausnahmen in der Sprache. Insbesondere kann eine asynchrone Funktion nur von asynchronen Funktionen erwartet werden. Dies betrifft alle aufrufenden Funktionen! Was aber, wenn wir eine Funktion am Ende dieser Abhängigkeitskette von nicht asynchron in asynchron ändern? Dies wäre eine rückwärts inkompatible Änderung… es sei denn, alle Funktionen sind asynchron und jeder Funktionsaufruf wird standardmäßig erwartet.

    Und das ist höchst unerwünscht, weil es sehr schlechte Auswirkungen auf die Leistung hat. Sie könnten nicht einfach billige Werte zurückgeben. Jeder Funktionsaufruf würde viel teurer werden.

Async ist großartig, aber eine Art implizites Async funktioniert in der Realität nicht.

Reine funktionale Sprachen wie Haskell haben eine gewisse Notluke, da die Ausführungsreihenfolge weitgehend nicht spezifiziert und nicht beobachtbar ist. Oder anders formuliert: Jede bestimmte Reihenfolge von Operationen muss explizit codiert werden. Dies kann für reale Programme ziemlich umständlich sein, insbesondere für E/A-schwere Programme, für die asynchroner Code sehr gut geeignet ist.

65
amon

Was Sie vermissen, ist der Zweck von asynchronen Operationen: Sie ermöglichen es Ihnen, Ihre Wartezeit zu nutzen!

Wenn Sie eine asynchrone Operation, wie das Anfordern einer Ressource von einem Server, in eine synchrone Operation umwandeln, indem Sie implizit und sofort auf die Antwort warten , kann Ihr Thread mit der Wartezeit nichts anderes tun . Wenn der Server 10 Millisekunden benötigt, um zu antworten, werden etwa 30 Millionen CPU-Zyklen verschwendet. Die Latenz der Antwort wird zur Ausführungszeit für die Anforderung.

Der einzige Grund, warum Programmierer asynchrone Operationen erfunden haben, besteht darin, die Latenz von inhärent lang laufenden Aufgaben hinter anderen nützlichen Berechnungen zu verbergen. Wenn Sie die Wartezeit mit nützlicher Arbeit füllen können, spart dies CPU-Zeit. Wenn Sie nicht können, geht nichts verloren, wenn die Operation asynchron ist.

Daher empfehle ich, die asynchronen Vorgänge zu nutzen, die Ihre Sprachen Ihnen bieten. Sie sind da, um Ihnen Zeit zu sparen.

Einige tun.

Sie sind (noch) kein Mainstream, da Async eine relativ neue Funktion ist, für die wir gerade erst ein gutes Gefühl bekommen haben, ob es überhaupt eine gute Funktion ist oder wie man sie Programmierern auf eine freundliche/benutzerfreundliche/präsentierende Weise präsentiert. ausdrucksstark/etc. Bestehende asynchrone Funktionen sind weitgehend mit vorhandenen Sprachen verknüpft, die einen etwas anderen Entwurfsansatz erfordern.

Das heißt, es ist nicht klar eine gute Idee, überall zu tun. Ein häufiger Fehler besteht darin, asynchrone Aufrufe in einer Schleife auszuführen und deren Ausführung effektiv zu serialisieren. Wenn implizite asynchrone Aufrufe vorliegen, kann diese Art von Fehler möglicherweise verdeckt werden. Wenn Sie impliziten Zwang von Task<T> (Oder dem Äquivalent Ihrer Sprache) zu T unterstützen, kann dies Ihrem Typechecker und der Fehlerberichterstattung ein wenig Komplexität/Kosten hinzufügen, wenn unklar ist, welche der zwei wollte der Programmierer wirklich.

Das sind aber keine unüberwindlichen Probleme. Wenn Sie dieses Verhalten unterstützen wollten, könnten Sie es mit ziemlicher Sicherheit tun, obwohl es Kompromisse geben würde.

13
Telastyn

Es gibt Sprachen, die dies tun. Es besteht jedoch tatsächlich kein großer Bedarf, da dies mit vorhandenen Sprachfunktionen leicht erreicht werden kann.

Solange Sie einige Möglichkeiten haben, Asynchronität auszudrücken, können Sie Futures oder Versprechen nur als Bibliotheksfunktion implementieren, Sie benötigen keine spezielle Sprachfunktionen. Und solange Sie einige zum Ausdruck bringen Transparente Proxies haben, können Sie die beiden Funktionen zusammenfügen und Sie haben Transparente Zukünfte.

Zum Beispiel kann in Smalltalk und seinen Nachkommen ein Objekt seine Identität ändern, es kann buchstäblich ein anderes Objekt "werden" (und tatsächlich heißt die Methode, die dies tut, Object>>become:).

Stellen Sie sich eine lang laufende Berechnung vor, die einen Future<Int> Zurückgibt. Dieses Future<Int> Hat alle die gleichen Methoden wie Int, außer mit unterschiedlichen Implementierungen. Die Future<Int> - Methode von + Fügt keine weitere Zahl hinzu und gibt das Ergebnis zurück. Sie gibt ein neues Future<Int> Zurück, das die Berechnung umschließt. Und so weiter und so fort. Methoden, die durch die Rückgabe eines Future<Int> Nicht sinnvoll implementiert werden können, rufen stattdessen automatisch await das Ergebnis auf und rufen dann self become: result. Auf, wodurch das aktuell ausgeführte Objekt (self, dh das Future<Int>) wird buchstäblich zum result Objekt, dh von nun an ist die Objektreferenz, die früher ein Future<Int> war, jetzt ein Int überall völlig transparent für den Kunden.

Es sind keine speziellen asynchronen Sprachfunktionen erforderlich.

12
Jörg W Mittag

Sie tun es (nun, die meisten von ihnen). Die gesuchte Funktion heißt threads.

Threads haben jedoch ihre eigenen Probleme:

  1. Da der Code an jedem Punkt angehalten werden kann, können Sie nicht jemals davon ausgehen, dass sich die Dinge nicht "von selbst" ändern. Wenn Sie mit Threads programmieren, verschwenden Sie viel Zeit damit, darüber nachzudenken, wie Ihr Programm mit Änderungen umgehen soll.

    Stellen Sie sich vor, ein Spielserver verarbeitet den Angriff eines Spielers auf einen anderen Spieler. Etwas wie das:

    if (playerInMeleeRange(attacker, victim)) {
        const damage = calculateAttackDamage(attacker, victim);
        if (victim.health <= damage) {
    
            // attacker gets whatever the victim was carrying as loot
            const loot = victim.getInventoryItems();
            attacker.addInventoryItems(loot);
            victim.removeInventoryItems(loot);
    
            victim.sendMessage("${attacker} hits you with a ${attacker.currentWeapon} and you die!");
            victim.setDead();
        } else {
            victim.health -= damage;
            victim.sendMessage("${attacker} hits you with a ${attacker.currentWeapon}!");
        }
        attacker.markAsKiller();
    }
    

    Drei Monate später entdeckt ein Spieler dies, indem er getötet wird und sich genau dann abmeldet, wenn attacker.addInventoryItems läuft, dann victim.removeInventoryItems wird fehlschlagen, er kann seine Gegenstände behalten und der Angreifer erhält auch eine Kopie seiner Gegenstände. Er tut dies mehrmals, indem er eine Million Tonnen Gold aus der Luft schafft und die Wirtschaft des Spiels zum Erliegen bringt.

    Alternativ kann sich der Angreifer abmelden, während das Spiel eine Nachricht an das Opfer sendet, und er erhält kein "Mörder" -Tag über dem Kopf, sodass sein nächstes Opfer nicht vor ihm davonläuft.

  2. Da der Code bei an einem beliebigen Punkt angehalten werden kann, müssen Sie beim Bearbeiten von Datenstrukturen überall Sperren verwenden. Ich habe oben ein Beispiel gegeben, das offensichtliche Konsequenzen in einem Spiel hat, aber subtiler sein kann. Erwägen Sie, ein Element am Anfang einer verknüpften Liste hinzuzufügen:

    newItem.nextItem = list.firstItem;
    list.firstItem = newItem;
    

    Dies ist kein Problem, wenn Sie sagen, dass Threads nur angehalten werden können, wenn sie E/A ausführen, und zu keinem Zeitpunkt. Aber ich bin sicher, Sie können sich eine Situation vorstellen, in der es eine E/A-Operation gibt - wie zum Beispiel die Protokollierung:

    for (player = playerList.firstItem; player != null; player = item.nextPlayer) {
        debugLog("${item.name} is online, they get a gold star");
        // Oops! The player might've logged out while the log message was being written to disk, and now this will throw an exception and the remaining players won't get their gold stars.
        // Or the list might've been rearranged and some players might get two and some players might get none.
        player.addInventoryItem(InventoryItems.GoldStar);
    }
    
  3. Da der Code bei an einem beliebigen Punkt angehalten werden kann, muss möglicherweise viel Status gespeichert werden. Das System behandelt dies, indem es jedem Thread einen völlig separaten Stapel gibt. Der Stapel ist jedoch ziemlich groß, sodass Sie in einem 32-Bit-Programm nicht mehr als 2000 Threads haben können. Oder Sie können die Stapelgröße reduzieren, da die Gefahr besteht, dass sie zu klein wird.

7
user253751

Viele der Antworten hier sind irreführend, denn während die Frage buchstäblich nach asynchroner Programmierung und nicht nach nicht blockierenden E/A fragte, glaube ich nicht, dass wir in diesem speziellen Fall eine diskutieren können, ohne die andere zu diskutieren.

Während asynchrone Programmierung von Natur aus asynchron ist, besteht das Ziel der asynchronen Programmierung hauptsächlich darin, das Blockieren von Kernel-Threads zu vermeiden. Node.js verwendet Asynchronität über Rückrufe oder Promises, um Blockierungsvorgänge aus einer Ereignisschleife zu senden, und Netty in Java verwendet Asynchronität über Rückrufe oder CompletableFutures to mach etwas ähnliches.

Nicht blockierender Code erfordert jedoch keine Asynchronität. Es hängt davon ab, wie viel Ihre Programmiersprache und Laufzeit für Sie bereit ist.

Go, Erlang und Haskell/GHC können dies für Sie erledigen. Sie können so etwas wie var response = http.get('example.com/test') schreiben und einen Kernel-Thread hinter den Kulissen freigeben lassen, während Sie auf eine Antwort warten. Dies geschieht durch Goroutinen, Erlang-Prozesse oder forkIO, die beim Blockieren Kernel-Threads hinter den Kulissen loslassen, damit sie andere Dinge tun können, während sie auf eine Antwort warten.

Es ist wahr, dass die Sprache für Sie nicht wirklich mit Asynchronität umgehen kann, aber einige Abstraktionen lassen Sie weiter gehen als andere, z. unbegrenzte Fortsetzungen oder asymmetrische Coroutinen. Die Hauptursache für asynchronen Code, das Blockieren von Systemaufrufen, ist jedoch absolut kann vom Entwickler weg abstrahiert werden.

Node.js und Java Unterstützung asynchroner nicht blockierender Code, während Go und Erlang synchroner nicht blockierender Code unterstützen. Sie sind beide gültige Ansätze mit unterschiedlichen Kompromissen.

Mein eher subjektives Argument ist, dass diejenigen, die gegen Laufzeiten argumentieren, die im Namen des Entwicklers nicht blockieren, wie diejenigen sind, die gegen die Speicherbereinigung in den frühen Neunzigern argumentieren. Ja, es entstehen Kosten (in diesem Fall hauptsächlich mehr Speicher), aber es erleichtert die Entwicklung und das Debuggen und macht Codebasen robuster.

Ich persönlich würde argumentieren, dass asynchroner nicht blockierender Code in Zukunft für die Systemprogrammierung reserviert werden sollte und modernere Technologie-Stacks zu synchroner nicht blockierender Laufzeit für die Anwendungsentwicklung migriert werden sollten .

4
Louis Jackman

Wenn ich Sie richtig lese, fragen Sie nach einem synchronen Programmiermodell, aber einer Hochleistungsimplementierung. Wenn dies korrekt ist, steht uns dies bereits in Form von grünen Fäden oder Prozessen von z. Erlang oder Haskell. Ja, das ist eine hervorragende Idee, aber die Nachrüstung bestehender Sprachen kann nicht immer so reibungslos verlaufen, wie Sie es möchten.

3
monocell

Ich schätze die Frage und finde, dass die meisten Antworten lediglich den Status quo verteidigen. Im Spektrum der Sprachen mit niedrigem bis hohem Sprachniveau stecken wir seit einiger Zeit in Schwierigkeiten. Die nächsthöhere Ebene wird eindeutig eine Sprache sein, die sich weniger auf die Syntax konzentriert (die Notwendigkeit expliziter Schlüsselwörter wie Warten und Asynchronisieren) und viel mehr auf Absichten. (Offensichtlicher Verdienst von Charles Simonyi, aber ich denke an 2019 und die Zukunft.)

Wenn ich einem Programmierer sagte, dass er Code schreiben soll, der einfach einen Wert aus einer Datenbank abruft, können Sie davon ausgehen, dass ich "und übrigens, hängen Sie die Benutzeroberfläche nicht auf" und "keine anderen Überlegungen einführen, die schwer zu findende Fehler maskieren" ". Programmierer der Zukunft mit einer nächsten Generation von Sprachen und Werkzeugen werden sicherlich Code schreiben können, der einfach einen Wert in einer Codezeile abruft und von dort aus weitergeht.

Die Sprache auf höchstem Niveau würde Englisch sprechen und sich auf die Kompetenz des Auftraggebers verlassen, um zu wissen, was Sie wirklich tun möchten. (Denken Sie an den Computer in Star Trek oder fragen Sie Alexa etwas.) Wir sind weit davon entfernt, kommen aber näher, und ich gehe davon aus, dass die Sprache/der Compiler mehr dazu geeignet sein könnte, robusten, beabsichtigten Code zu generieren, ohne so weit zu gehen Ich brauche KI.

Einerseits gibt es neuere visuelle Sprachen wie Scratch, die dies tun und nicht mit allen syntaktischen technischen Details beschäftigt sind. Sicherlich wird hinter den Kulissen viel gearbeitet, sodass sich der Programmierer keine Sorgen machen muss. Trotzdem schreibe ich keine Business-Class-Software in Scratch. Daher habe ich wie Sie die gleiche Erwartung, dass es Zeit für ausgereifte Programmiersprachen ist, das synchrone/asynchrone Problem automatisch zu verwalten.

2
Mikey Wetzel

Das Problem, das Sie beschreiben, ist zweifach.

  • Das Programm, das Sie schreiben, sollte sich als Ganzes asynchron verhalten von außen betrachtet.
  • An der Aufrufstelle sollte nicht sichtbar sein, ob ein Funktionsaufruf möglicherweise die Kontrolle aufgibt oder nicht.

Es gibt verschiedene Möglichkeiten, dies zu erreichen, aber im Grunde laufen sie darauf hinaus

  1. mit mehreren Threads (auf einer bestimmten Abstraktionsebene)
  2. auf Sprachebene mehrere Arten von Funktionen haben, die alle wie folgt aufgerufen werden foo(4, 7, bar, quux).

Für (1) fasse ich mehrere Prozesse zusammen, um mehrere Kernel-Threads und Green-Thread-Implementierungen zu erzeugen, die Threads auf Sprachlaufzeitebene auf Kernel-Threads planen. Aus der Sicht des Problems sind sie gleich. In dieser Welt gibt keine Funktion gibt jemals die Kontrolle aus der Perspektive ihres Threads auf oder verliert sie. Der Thread selbst hat manchmal keine Kontrolle und läuft manchmal nicht, aber Sie geben die Kontrolle über Ihren eigenen Thread in dieser Welt nicht auf. Ein System, das zu diesem Modell passt, kann möglicherweise neue Threads erzeugen oder vorhandene Threads verbinden. Ein System, das zu diesem Modell passt, kann möglicherweise einen Thread wie Unixs forkduplizieren.

(2) ist interessant. Um dem gerecht zu werden, müssen wir über Einführungs- und Eliminierungsformen sprechen.

Ich werde zeigen, warum implizites await einer Sprache wie Javascript nicht abwärtskompatibel hinzugefügt werden kann. Die Grundidee besteht darin, dass Javascript durch die Offenlegung von Versprechungen für den Benutzer und die Unterscheidung zwischen synchronen und asynchronen Kontexten ein Implementierungsdetail durchgesickert ist, das verhindert, dass synchrone und asynchrone Funktionen einheitlich behandelt werden. Es gibt auch die Tatsache, dass Sie ein Versprechen außerhalb eines asynchronen Funktionskörpers nicht await können. Diese Entwurfsoptionen sind nicht kompatibel mit "Asynchronität für den Anrufer unsichtbar machen".

Sie können eine synchrone Funktion mit einem Lambda einführen und mit einem Funktionsaufruf entfernen.

Einführung in die synchrone Funktion:

((x) => {return x + x;})

Eliminierung synchroner Funktionen:

f(4)

((x) => {return x + x;})(4)

Sie können dies mit der Einführung und Eliminierung asynchroner Funktionen vergleichen.

Einführung in die asynchrone Funktion

(async (x) => {return x + x;})

Eliminierung asynchroner Funktionen (Hinweis: Nur gültig innerhalb einer async -Funktion)

await (async (x) => {return x + x;})(4)

Das grundlegende Problem hierbei ist, dass eine asynchrone Funktion ist auch eine synchrone Funktion, die ein Versprechungsobjekt erzeugt.

Hier ist ein Beispiel für das synchrone Aufrufen einer asynchronen Funktion in der Datei node.js repl.

> (async (x) => {return x + x;})(4)
Promise { 8 }

Sie können hypothetisch eine Sprache haben, auch eine dynamisch typisierte, in der der Unterschied zwischen asynchronen und synchronen Funktionsaufrufen am Aufrufstandort nicht sichtbar ist und möglicherweise am Definitionsstandort nicht sichtbar ist.

Wenn Sie eine solche Sprache auf Javascript reduzieren, ist es möglich, dass Sie alle Funktionen effektiv asynchron machen müssen.

1
Gregory Nisbet

Mit Go-Sprach-Goroutinen und der Go-Sprachlaufzeit können Sie den gesamten Code so schreiben, als wäre er synchronisiert. Wenn eine Operation in einer Goroutine blockiert, wird die Ausführung in anderen Goroutinen fortgesetzt. Und mit Kanälen können Sie einfach zwischen Goroutinen kommunizieren. Dies ist oft einfacher als Rückrufe wie in Javascript oder async/await in anderen Sprachen. Unter https://tour.golang.org/concurrency/1 finden Sie einige Beispiele und eine Erklärung.

Außerdem habe ich keine persönlichen Erfahrungen damit, aber ich höre, dass Erlang ähnliche Einrichtungen hat.

Ja, es gibt Programmiersprachen wie Go und Erlang, die das synchron/asynchrone Problem lösen, aber leider sind sie noch nicht sehr beliebt. Da diese Sprachen immer beliebter werden, werden die von ihnen bereitgestellten Einrichtungen möglicherweise auch in anderen Sprachen implementiert.

1
user332375

Es gibt einen sehr wichtigen Aspekt, der noch nicht angesprochen wurde: Wiedereintritt. Wenn Sie einen anderen Code (z. B. Ereignisschleife) haben, der während des asynchronen Aufrufs ausgeführt wird (und wenn nicht, warum benötigen Sie dann überhaupt asynchron?), Kann der Code den Programmstatus beeinflussen. Sie können die asynchronen Aufrufe nicht vor dem Aufrufer verbergen, da der Aufrufer möglicherweise davon abhängt, dass Teile des Programmstatus für die Dauer seines Funktionsaufrufs nicht betroffen sind. Beispiel:

function foo( obj ) {
    obj.x = 2;
    bar();
    log( "obj.x equals 2: " + obj.x );
}

Wenn bar() eine asynchrone Funktion ist, kann sich der obj.x Während seiner Ausführung möglicherweise ändern. Dies wäre ziemlich unerwartet ohne den Hinweis, dass der Balken asynchron ist und dieser Effekt möglich ist. Die einzige Alternative wäre, zu vermuten, dass jede mögliche Funktion/Methode asynchron ist, und nach jedem Funktionsaufruf jeden nicht lokalen Status erneut abzurufen und zu überprüfen. Dies ist anfällig für subtile Fehler und möglicherweise überhaupt nicht möglich, wenn ein Teil des nicht lokalen Status über Funktionen abgerufen wird. Aus diesem Grund muss der Programmierer wissen, welche der Funktionen den Programmstatus auf unerwartete Weise ändern können:

async function foo( obj ) {
    obj.x = 2;
    await bar();
    log( "obj.x equals 2: " + obj.x );
}

Jetzt ist deutlich zu erkennen, dass es sich bei bar() um eine asynchrone Funktion handelt. Die richtige Vorgehensweise besteht darin, den erwarteten Wert von obj.x Anschließend erneut zu überprüfen und eventuelle Änderungen vorzunehmen War aufgetreten.

Wie bereits in anderen Antworten erwähnt, können sich reine funktionale Sprachen wie Haskell diesem Effekt vollständig entziehen, indem sie die Notwendigkeit eines gemeinsamen/globalen Zustands überhaupt vermeiden. Ich habe nicht viel Erfahrung mit funktionalen Sprachen, daher bin ich wahrscheinlich voreingenommen, aber ich denke nicht, dass das Fehlen des globalen Zustands ein Vorteil ist, wenn ich größere Anwendungen schreibe.

1
j_kubik

Im Fall von Javascript, das Sie in Ihrer Frage verwendet haben, ist ein wichtiger Punkt zu beachten: Javascript ist Single-Threaded und die Reihenfolge der Ausführung ist garantiert, solange keine asynchronen Aufrufe vorhanden sind.

Wenn Sie also eine Sequenz wie Ihre haben:

const nbOfUsers = getNbOfUsers();

Sie werden garantiert, dass in der Zwischenzeit nichts anderes ausgeführt wird. Keine Notwendigkeit für Schlösser oder ähnliches.

Wenn getNbOfUsers jedoch asynchron ist, gilt Folgendes:

const nbOfUsers = await getNbOfUsers();

bedeutet, dass während getNbOfUsers ausgeführt wird, Ausführungserträge und anderer Code dazwischen ausgeführt werden können. Dies kann wiederum eine gewisse Sperrung erfordern, je nachdem, was Sie tun.

Es ist daher eine gute Idee, sich darüber im Klaren zu sein, wann ein Anruf asynchron ist und wann nicht, da Sie in einigen Situationen zusätzliche Vorsichtsmaßnahmen treffen müssen, die Sie nicht benötigen würden, wenn der Anruf synchron wäre.

0
jcaron