How to be a programmer: Part 2

Theres actually a few things that are and aren't part of basic algorithms. In order to achieve some things efficiently with programming, you have to have a few additions (things that make the magics not really pure magic anymore - keeping the changes in mind is important when thinking about the planned lifetime of a process, thinking about how safe it is to consider the process self-contained).

- Runes can't delay. Exceptions: Asynchronous programming, signals, i/o functions

- Runes can't run more than one thing at a time. Exceptions: Spell spawning, coroutines and user threads, libraries (Generally, you shouldn't rely on anything you submit for someone else to use their own way to only have one thread instance at a time).

- Dropped packets and latency can't be accounted for by runes. Exceptions: Clock manipulation

The deeper you go into what runes can and can't do, the more hideous the implementations get.

Asynchronous spellcasting and i/o functions are built by stopping the spell and waiting for the caster or spell parent to start them again. Generally, the runes retain continuity except for the intended effect of the asynchronous spellcasting / the input given to the runes, but in between those two things the rune does nothing, so outside of the data structures in the computer that contain it in memory, it doesn't really exist anymore. It can't fix up your text while you're typing it (Ryan asked about doing this in python once) because it's outside of executing. There's nothing you can do except, well, use a function that only dies for a fraction of the wait time.

Signals do the same thing, except explicitly. However, it is also fair to think of signals as part of a larger (implied) soul structure connecting the signal waiter with the signal giver. The signal waiter may be the second part of a set of stopped threads in the function, or it may be the handler of the signal.

Example:

class library {
    signal;
    void thread_stopper(){
        A();
        signal.wait();
        B();
    }
}

A is completed as many times as thread_stopper is called. After this point the function (usually) effectively ends, but it also stops the parent thread. Once the signal is given, every B comes out at once. So this implementation uses a (usually spell parent-given) thread-object to hide the abstraction of data being stored in stack memory. The stack memory belongs to the stopped thread and is itself kind of like an object (Well, it's like a lot of objects that have to share space with each-other). Languages like Lua can actually use this to more impact because of the lack of distinction between stack memory, scope, and the use of functions as objects instead of runes.

class library {
    signal;
    queue;
    void thread_queue(){
        object Data <- set(with A() and the thread);
        add_to_queue(Data);
    }
    void signal.onsignal(){
        for(Data in queue){
            if(ready):
               trigger(Data); 
        }
    }
    void trigger(Data){
        B();
    }
}

Actually, the first approach tends to be more sophisticated because in the second approach, each trigger happens sequentially, and the user of the library in the first example gets an output (A and B can be completed) while the user of the library in the second example only gets the output of A, even though it wants both A and B to be done. To make the second example fit the first one, you would need to kill each thread by yourself and resume it in the trigger function.

Soul spawning, coroutines, and user threads are how souls manage threads and perform parallel spell-execution.

Parallel spell-execution:

A way for a task to get two tasks done.

Example:

spawn(
task1(){
    
}
);
spawn(
task2(){
    
}
);
return;

Both tasks will run at similar times (the difference is close to zero seconds). In this syntax, the spawn function accepts a function and function declarations return the function, but in C style, spawn function would accept a function pointer and the tasks are declared somewhere else. Another example:

spawn(
task1(){
    
}
);
task2(){
    
}
task2();
return;

Task 2 will usually run before task 1 due to the way that clock cycles work. This design allows two tasks to run, but yields on the second task.

Example 3:

thread1 = spawn(
task1(){
    
}
);
thread2 = spawn(
task2(){
    
}
);
yield(thread1);
yield(thread2);
return;

A yield function allows the task-giver to complete as soon as possible after both tasks are done, which also allows the task-giver to have the outputs of both tasks guaranteed. Yield functions usually go like this.

yield(thread){
    while(thread is not done){
        add signal to thread handler;
        signal.wait();
    }
}

You can see that the yield (usually) function does nothing if the thread is done, and returns control to the task-giver immediately. Threads can be used to describe the functions that they are in, but they always describe instances of functions, because anything declared within a function only belongs to the version of that function that declared it.

Main soul thread could be like this:

main()->your_function()->library()->assembly file-> (stopped to request a task from another soul)

But a coroutine (a created thread) usually looks like this.

(none) -> task1() -> your_function() -> library() -> etc

There are two points:

1. Threads are called threads because they logically connect the start of your soul, the end of your soul, and everything in between. Including the functions that are uncomfortable to look at, but you know a call was written somewhere.

In fact, threads can start and end anywhere, it's just inconvenient to randomly end threads because a spell without threads can no longer actually do anything. Basically a thread "goes back" when a function is done, and if it does not have anything to go back to then it stops itself. Your souls that you run in the "linux shell" go back to the shell's thread, which was waiting on the soul, but the threads that didn't start with the linux shell don't go back to the linux shell because they have a defined point somewhere in the soul where they started. Since the thread doesn't have a stack before that point, it ends there.

2. Threads are what store stack memory, and anywhere that a thread shares the soul that made it, that thread shares the stack memory too (but usually lacks the same starting point). Usually this is a bad idea and function nesting (which is the only way to share stack memory) or pass-by-reference is the only way to implement shared stack memory.

Here are ways to reduce your risk:

- If you are using C#, Java, or Lua, which have built-in garbage collection for objects. C and C++ will probably pop the function that created the task off of the stack (you have to take this literally it's literally a stack of variable scopes), which kills the stack variables before task1 is done using them

- If you use dynamically allocated objects that you give to the tasks and delete after yielding.

- In fact, yielding the function that's currently on the stack will prevent the stack variables from being killed for as long as the yielding happens.

- If you use pass by value from the task-giver, you can transfer data from the stack without significant risk.

Some languages and implementations prevent threads from being killed before the main soul ends, some don't.

Spell spawning is basically when you use fork(), popen() or exec() and create a new spell, or a copy of your current spell. It's especially dangerous to copy a spell and allow both to share the resources, because one copy of your soul could destroy the resource before the other is done. Stack memory and dynamic memory usually aren't shared between spells, but the after copying, the variables are identical to each-other, so without knowing which is which, you could have two spells that access the same pointer to a file.

Spell:

fopen();
fork();
fclose();

(fopen = File open, fork = File ork NO, fclose = File close)

Spell tree diagram:

a = fopen();
b = fork();
b = 0, b = pid // assume pid is a big number
fclose(a), fclose(a)

Even though you request a resource once, you are trying to close it twice. Try to be polite and either use inter-spell communication or only close it on one (the parent or the child), or preferably, use communication and only close it on one.

User threads are similar to parent spell-given threads except they are weird and chaotic. The user (You are a user, but you are not all of the users unless you also wrote the user thread system) could be doing anything. They could be embedding hidden context switches in places where you don't expect.

Clock manipulation (see: rubber banding) is done on an engine and on client-server communication to make sure that

1. Events occur in the same order (Future packets contain information about dropped packets, so that both sides are able to wait for all packets to arrive and then execute them in the right order).

2. (Optional) Non-simultaneous events occur in linear time, or approximately linear time. If the client is displaying a rock for 1 second, and the server gives the client the position of the same rock after 0.95 seconds, you don't want two rocks to show up and you don't want the rock to teleport. You are trying to tell a story here - that story is that the rock is a real thing, not an image projected by the server onto the client, or an image projected by the client onto the server. Predictions aren't perfect so lag compensation isn't perfect, but the disparity doesn't have to be noticeable.

In fact, whenever you are trying to build a dynamic model of any world in a computer (not just in video games), it is better to use clock manipulation in your model than not, because whatever you are building needs to tell a story whether you like it or not. Clock manipulation is a type of maintenance for the logic of the model.


Magics = Algorithms

Soul = Program

Spell = Process

Spellcasting = Programming

Caster = Operating System

Runes = Code

Comments

Popular Posts