• Hey! Guest! The 39th GMC Jam will take place between November 26th, 12:00 UTC and November 30th, 12:00 UTC. Why not join in! Click here to find out more!

GMS 2.3+ Async atomocity?

I am making a multiplayer game.
Client #1 is constantly scanning for instances of object X.
Client #2 asynchronously destroys one of those instances after it was already read on client 1. Then I get an undefined error when trying to reference it because that pointer obviously points to a deallocated instance.
instance_exists is a pretty poor bandaid-- it has huge overhead in a loop, and it still isn't atomic (it lessens the undefined error frequency, but doesn't remove it entirely).

How would I go about handling this atomically with the GML Standard Library?

Thank you
 

Nidoking

Member
Have you considered, instead of destroying the instance, having some variable that marks it to be destroyed on the next cycle? Then Client 1 will know to pretend that the instance no longer exists, but the references won't crash the game.
 
Have you considered, instead of destroying the instance, having some variable that marks it to be destroyed on the next cycle? Then Client 1 will know to pretend that the instance no longer exists, but the references won't crash the game.
At first I had a global "destroy queue" where all incoming destroys really just added that instance id to that queue, then at the end of the step, that global queue was dequeued until emptied and all of those instances were iteratively destroyed. But then I was running into the same race-y condition where numerous async events are writing to that queue while a controller is trying to synchronously clear it. So it was a dead end.

I suppose I could just "tag" the instance with a local variable, then it destroy itself next cycle. But that would require an if statement in the step event of all of these instances. That's not very efficient and isn't really feasible when I have hundreds, if not thousands, of instances simultaneously.
 

GMWolf

aka fel666
Have you actually ran into any real issues?

Async events are misnomers.
It's not documented, but (at least last I checked) they do run synchronously according to an event order.
Internally GM probably queues up async events to the run them all synchronously.


If it turns out you are running into these issues, then emulate what I outlined above.
Have async events queue entities for deletion (using a list or ds_queue) then go through that list to delete entities when needed. (I'm fairly sure modifying data structures in async events is safe)

[Edit]
But then I was running into the same race-y condition where numerous async events are writing to that queue while a controller is trying to synchronously clear it. So it was a dead end.
Oh.i didn't see that.

Googling around I'm not seeing anything about async events being changed, and the consensus is that async events get queueq up by gm internally.
 
Last edited:
Have you actually ran into any real issues?

Async events are misnomers.
It's not documented, but (at least last I checked) they do run synchronously according to an event order.
Internally GM probably queues up async events to the run them all synchronously.
Yeah I get crashes when looping on client #1:
GML:
for (i = 0; i < instance_number(obj_thingy); i++) {
   temp_inst = instance_find(obj_thingy, i);
   temp_inst.x ....
} //crash
This occurs when client #2 async destroys one of the instances. At first, I thought maybe I just had an innate bug, but slapping instance_exist(temp_inst) in there *reduced* the frequency of crashes.
 

GMWolf

aka fel666
What do you mean by async destroy?
Do you mean destroy in an async event?
If that's the case then it might be a bug with GM itself.
There are not mutexes or other such primitives in GM.
If you really need them they would need to be implemented through an extension.



As a sanity check, could you set a variable to true at the start of a step event, do a bunch of work, then set that variable to false.
In the async event, check if that variable is ever true and show an error if it is.
 
Last edited:
Googling around I'm not seeing anything about async events being changed, and the consensus is that async events get queueq up by gm internally.
Async events may be queued relative to one another, but are they queued with the rest of the synchronous events (i.e. Step)?
 

GMWolf

aka fel666
Async events may be queued relative to one another, but are they queued with the rest of the synchronous events (i.e. Step)?
last time i tested it it sure seemed that way.
I havent found anything in the manual cautioning you of any sort of race conditions.
 

rytan451

Member
Yeah I get crashes when looping on client #1:
GML:
for (i = 0; i < instance_number(obj_thingy); i++) {
   temp_inst = instance_find(obj_thingy, i);
   temp_inst.x ....
} //crash
This occurs when client #2 async destroys one of the instances. At first, I thought maybe I just had an innate bug, but slapping instance_exist(temp_inst) in there *reduced* the frequency of crashes.
Just a quick note regarding this loop: it has a hidden O(n^2) time. instance_find, last I recall, takes O(i) time (i being the second parameter), so iterating over instance_number(obj_thingy) with instance_find inside would actually result in an O(n^2) running time. Instead, you should probably use a with statement, since that reduces it to linear time (a whole O(n) speedup!)
 
Just a quick note regarding this loop: it has a hidden O(n^2) time. instance_find, last I recall, takes O(i) time (i being the second parameter), so iterating over instance_number(obj_thingy) with instance_find inside would actually result in an O(n^2) running time. Instead, you should probably use a with statement, since that reduces it to linear time (a whole O(n) speedup!)
A) This is just an example loop I made up. I don't even see your point on "use a with statement." That doesn't help me find all of the instance ids where this is being called from, say, inside a controller.
B) Although I appreciate your suggestion at a runtime speedup, that's not really what I'm here for. I'm looking for atomicity.
 
Last edited:

rytan451

Member
I've come up with a GML2.3 semaphore-based method to avoid concurrency issues no matter when an asynchronous event comes in to add a new instance:

GML:
function SafeInstanceCreator() constructor {
  isFirstQueueWritable = true;
  firstQueue = ds_queue_create();
  secondQueue = ds_queue_create();
  /// @func instanceCreateLayer(x, y, layer, obj)
  static instanceCreateLayer = function(_x, _y, _layer, obj) {
    var q;
    if (firstQueueWritable) {
      q = firstQueue;
    } else {
      q = secondQueue;
    }
    ds_queue_enqueue(q, _x);
    ds_queue_enqueue(q, _y);
    ds_queue_enqueue(q, _layer);
    ds_queue_enqueue(q, obj);
  };

  static flush = function(arr) {
    var q, _x, _y, _layer, obj, i;
    isFirstQueueWritable = !isFirstQueueWritable;
    if (firstQueueWritable) {
      q = secondQueue;
    } else {
      q = firstQueue;
    }
    i = 0;
    while (!ds_queue_empty(q)) {
      _x = ds_queue_dequeue(q);
      _y = ds_queue_dequeue(q);
      _layer = ds_queue_dequeue(q);
      obj = ds_queue_dequeue(q);
      if (is_array(arr)) {
        arr[i++] = instance_create_layer(_x, _y, _layer, obj);
      } else {
        instance_create_layer(_x, _y, _layer, obj);
        i++;
      }
    }
    return i;
  }
  static destroy = function() {
    ds_queue_destroy(firstQueue);
    ds_queue_destroy(secondQueue);
  }
}
Using a semaphore isFirstQueueWritable, the above code ensures that both queues are either readable or writable, but not both. So, when flush is called, the queue that was previously set as writable is set to readable, then it is iteratively cleared, creating instances as it is cleared. If an instance is added even while flush is still in progress, then the instance is added to the other queue, and will wait until the next time flush is called.

This isn't perfect, but it should cut down on problems dramatically.

Examples using the with statement:

GML:
with (obj_thingy) {
  ds_list_add(other.thingy_list, id);
}
// appended all ids of obj_thingy into the list thingy_list (which is an instance variable); OR:

var thingies = array_create(instance_number(obj_thingy));
var i = 0;
with (obj_thingy) {
  thingies[i++] = id;
}
// Creates a list thingies which includes all ids of obj_thingy; OR:

with (obj_thingy) {
  with (other) {
    // Do something. Instead of using temp_inst, just use other. If you must get ids, then use other.id.    
  }
}
I was concerned because of your instance_number/instance_find loop.
 

Nidoking

Member
I don't even see your point on "use a with statement." That doesn't help me find all of the instance ids where this is being called from, say, inside a controller.
That's exactly what a with statement does. That's like saying "I don't understand the point of keys. They don't help me open doors."
 

Nocturne

Friendly Tyrant
Forum Staff
Admin
Moderator
Async events are misnomers.
It's not documented, but (at least last I checked) they do run synchronously according to an event order.
Internally GM probably queues up async events to the run them all synchronously.
Just want to point out that this is correct. :)

All GML code is synchronous, and as such all async events are queued. In general GameMaker is a single threaded environment and the only things on multiple threads are separate from GML, eg: audio mixing, audio streaming, and GC collection.
 

GMWolf

aka fel666
Just want to point out that this is correct. :)

All GML code is synchronous, and as such all async events are queued. In general GameMaker is a single threaded environment and the only things on multiple threads are separate from GML, eg: audio mixing, audio streaming, and GC collection.
Right so something else is going on.

Make sure that loop isn't destroying anything.
Could you share the exact code you are experiencing issues with, and Anny error message you are getting? (First post sounded like it was a hard crash but those are rare)
 
I've come up with a GML2.3 semaphore-based method to avoid concurrency issues no matter when an asynchronous event comes in to add a new instance:

GML:
function SafeInstanceCreator() constructor {
  isFirstQueueWritable = true;
  firstQueue = ds_queue_create();
  secondQueue = ds_queue_create();
  /// @func instanceCreateLayer(x, y, layer, obj)
  static instanceCreateLayer = function(_x, _y, _layer, obj) {
    var q;
    if (firstQueueWritable) {
      q = firstQueue;
    } else {
      q = secondQueue;
    }
    ds_queue_enqueue(q, _x);
    ds_queue_enqueue(q, _y);
    ds_queue_enqueue(q, _layer);
    ds_queue_enqueue(q, obj);
  };

  static flush = function(arr) {
    var q, _x, _y, _layer, obj, i;
    isFirstQueueWritable = !isFirstQueueWritable;
    if (firstQueueWritable) {
      q = secondQueue;
    } else {
      q = firstQueue;
    }
    i = 0;
    while (!ds_queue_empty(q)) {
      _x = ds_queue_dequeue(q);
      _y = ds_queue_dequeue(q);
      _layer = ds_queue_dequeue(q);
      obj = ds_queue_dequeue(q);
      if (is_array(arr)) {
        arr[i++] = instance_create_layer(_x, _y, _layer, obj);
      } else {
        instance_create_layer(_x, _y, _layer, obj);
        i++;
      }
    }
    return i;
  }
  static destroy = function() {
    ds_queue_destroy(firstQueue);
    ds_queue_destroy(secondQueue);
  }
}
Using a semaphore isFirstQueueWritable, the above code ensures that both queues are either readable or writable, but not both. So, when flush is called, the queue that was previously set as writable is set to readable, then it is iteratively cleared, creating instances as it is cleared. If an instance is added even while flush is still in progress, then the instance is added to the other queue, and will wait until the next time flush is called.

This isn't perfect, but it should cut down on problems dramatically.

Examples using the with statement:

GML:
with (obj_thingy) {
  ds_list_add(other.thingy_list, id);
}
// appended all ids of obj_thingy into the list thingy_list (which is an instance variable); OR:

var thingies = array_create(instance_number(obj_thingy));
var i = 0;
with (obj_thingy) {
  thingies[i++] = id;
}
// Creates a list thingies which includes all ids of obj_thingy; OR:

with (obj_thingy) {
  with (other) {
    // Do something. Instead of using temp_inst, just use other. If you must get ids, then use other.id.   
  }
}
I was concerned because of your instance_number/instance_find loop.
Thanks for the queue example! That is something clever to work off of, greatly appreciated.
As for the with statement, I had always assumed it was an instance function. I never knew you could pass in an object, and it will iteratively call all of the instances, run the code, then return. That is very useful.

That's exactly what a with statement does. That's like saying "I don't understand the point of keys. They don't help me open doors."
I had always assumed "with" required an instance id. It doesn't intuitively seem to be an Object call, but now I see how that can cut down on runtime.
 

GMWolf

aka fel666
I've come up with a GML2.3 semaphore-based method to avoid concurrency issues no matter when an asynchronous event comes in to add a new instance:

GML:
function SafeInstanceCreator() constructor {
  isFirstQueueWritable = true;
  firstQueue = ds_queue_create();
  secondQueue = ds_queue_create();
  /// @func instanceCreateLayer(x, y, layer, obj)
  static instanceCreateLayer = function(_x, _y, _layer, obj) {
    var q;
    if (firstQueueWritable) {
      q = firstQueue;
    } else {
      q = secondQueue;
    }
    ds_queue_enqueue(q, _x);
    ds_queue_enqueue(q, _y);
    ds_queue_enqueue(q, _layer);
    ds_queue_enqueue(q, obj);
  };

  static flush = function(arr) {
    var q, _x, _y, _layer, obj, i;
    isFirstQueueWritable = !isFirstQueueWritable;
    if (firstQueueWritable) {
      q = secondQueue;
    } else {
      q = firstQueue;
    }
    i = 0;
    while (!ds_queue_empty(q)) {
      _x = ds_queue_dequeue(q);
      _y = ds_queue_dequeue(q);
      _layer = ds_queue_dequeue(q);
      obj = ds_queue_dequeue(q);
      if (is_array(arr)) {
        arr[i++] = instance_create_layer(_x, _y, _layer, obj);
      } else {
        instance_create_layer(_x, _y, _layer, obj);
        i++;
      }
    }
    return i;
  }
  static destroy = function() {
    ds_queue_destroy(firstQueue);
    ds_queue_destroy(secondQueue);
  }
}
Using a semaphore isFirstQueueWritable, the above code ensures that both queues are either readable or writable, but not both. So, when flush is called, the queue that was previously set as writable is set to readable, then it is iteratively cleared, creating instances as it is cleared. If an instance is added even while flush is still in progress, then the instance is added to the other queue, and will wait until the next time flush is called.

This isn't perfect, but it should cut down on problems dramatically.

Examples using the with statement:

GML:
with (obj_thingy) {
  ds_list_add(other.thingy_list, id);
}
// appended all ids of obj_thingy into the list thingy_list (which is an instance variable); OR:

var thingies = array_create(instance_number(obj_thingy));
var i = 0;
with (obj_thingy) {
  thingies[i++] = id;
}
// Creates a list thingies which includes all ids of obj_thingy; OR:

with (obj_thingy) {
  with (other) {
    // Do something. Instead of using temp_inst, just use other. If you must get ids, then use other.id.  
  }
}
I was concerned because of your instance_number/instance_find loop.
Don't you still have a race condition on isFirstQueu writeable?
A =! A is not atomic.

@ClosestExaminer
You haven't commented on the fact async events are not async.

Could we see the actual code used and the error message? It would help figure out what the actual issue is.
 
Top