Encapsulated a global lock based on atomics - how to avoid dump?

I’ve encapsulated a more efficient global lock based on atomics. To make it work efficiently, I’ve encapsulated an nif to convert Pid to integer, as well as support to convert integer to pid. the code like this:

#include "erl_nif.h"

static ERL_NIF_TERM pidToInt(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) {
    ErlNifUInt64 TermInt = (ErlNifUInt64)argv[0];
    return enif_make_uint64(env, TermInt);
}

static ERL_NIF_TERM intToPid(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) {
    ErlNifUInt64 Pid;
    enif_fprintf(stdout, "IMY************put0001  %T \n", argv[0]);
    if (!enif_get_uint64(env, argv[0], &Pid)){
        enif_fprintf(stdout, "IMY************put0002  %T \n", argv[0]);
        return enif_make_badarg(env);
    }
    enif_fprintf(stdout, "IMY************put0003  %d \n", Pid);
    ERL_NIF_TERM TermPid = (ERL_NIF_TERM)Pid;
    enif_fprintf(stdout, "IMY************put0004  %T \n", TermPid);
    return (ERL_NIF_TERM)Pid;
}

static ErlNifFunc nif_funcs[] = {
    {"pidToInt", 1, pidToInt},
    {"intToPid", 1, intToPid}
};

ERL_NIF_INIT(eGPidInt, nif_funcs, NULL, NULL, NULL, NULL);

But there is a strange problem, passing in an illegal integer will cause the system to dump, exmaple:

Eshell V14.3 (press Ctrl+G to abort, type help(). for help)
1> Pid = self().
<0.154.0>
2> PidInt = eGPidInt:pidToInt(Pid).
19715
3> eGPidInt:intToPid(PidInt).
IMY************put0001  19715
IMY************put0003  19715
IMY************put0004  <0.154.0>
<0.154.0>
4> eGPidInt:intToPid(PidInt + 1).
IMY************put0001  19716
IMY************put0003  19716
IMY************put0004  <cp/header:0x0000000000004d04>
Segmentation fault (core dumped)

I think in this case how to avoid the dump?

if you want run this code, you can get it on here: GitHub - ErlGameWorld/eGLock: erlang's global lock
if you compile the nif error you may need rebuild eNpc(GitHub - ErlGameWorld/eNpc: Erlang Nif or port_driver generic compilation scripts for Windows Linux Mac), and replace in c_src/eNpc

There is no safe way to do this, you will always risk crashing the emulator when messing around with the term representation.

Edit: just casting an ERL_NIF_TERM to anything else counts as messing around, so you’re better off abandoning this approach entirely.

1 Like

If I can make sure that the argument to eGPidInt:intToPid comes from eGPidInt:pidToInt should still work?
The reason I wrote this code is because it is efficient to write this code without converting pid to lists and then parsing a, b, c into integers and then calling c:pid(a, b, c) back to pid

No, it would not.

I wrote this test code that works fine

ppp() ->
	S =rand:uniform(268435456) - 1,
	N =rand:uniform(4294967296) - 1,
	Pid = c:pid(0, S, N),
	not (Pid == eGPidInt:intToPid(eGPidInt:pidToInt(Pid))) andalso io:format("error ~p ~n", [Pid]).

4> utTc:tm(100, 1000000, test, ppp, []).
=====================
execute test:ppp().
execute LoopTime:1000000
execute ProcCnts:100
PMaxTime:     21301113991(ns)            21.3(s)
PMinTime:     16417212450(ns)           16.42(s)
PSumTime:   2047224517419(ns)         2047.22(s)
PAvgTime:  20472245174.19(ns)           20.47(s)
FAvgTime:        20472.25(ns)             0.0(s)
PGrar   :              49(cn)            0.49(%)
PLess   :              51(cn)            0.51(%)
=====================
ok
5> utTc:tm(100, 1000000, test, ppp, []).
=====================
execute test:ppp().
execute LoopTime:1000000
execute ProcCnts:100
PMaxTime:     21257973120(ns)           21.26(s)
PMinTime:     13087124683(ns)           13.09(s)
PSumTime:   2045522952699(ns)         2045.52(s)
PAvgTime:  20455229526.99(ns)           20.46(s)
FAvgTime:        20455.23(ns)             0.0(s)
PGrar   :              49(cn)            0.49(%)
PLess   :              51(cn)            0.51(%)
=====================
ok

That will work fine until it doesn’t, and then you will get super-weird crashes that will take you weeks or even months to debug.

I can’t prevent you from shooting yourself in the foot, I can only recommend that you don’t.

You don’t say why to want to convert between Pids and integers, or what the constraints on those integers are, but several options come to mind:

  • erlang:phash2
  • erlang:term_to_binary
  • Ulf Wiger’s sext library

Unless you want to play with NIFs there’s no reason to do this thing in a NIF.

it sounds terrible. how can it “That will work fine until it doesn’t” appear, Can I simulate it?

I used it for this project:GitHub - ErlGameWorld/eGLock: erlang's global lock only for the sake of efficiency
I need to convert this pid to an integer so that it can be recorded in the atomics value and then release the lock if the process unexpectedly dies

For example, if an external pid is used to take a lock the best case scenario is that the emulator crashes immediately, and the worst case scenario is subtle memory corruption.

It can also break at any point in the future if we (OTP) decide to make changes in the area. I strongly recommend that you don’t do this. Use the documented interfaces as intended.

This will only be used for local Pids, but actually this is used for a feature that the local Pids can be expressed in uint64. I couldn’t think of a faster and more efficient way to implement single-node global locks。Speaking of which, I am a little curious about the have the pid_to_list and list_to_pid functions, but not pid_to_integer and interger_to_pid. This, These two functions can also be implemented.

It will break for local pids as well, but will not do so as immediately as external pids will. Most importantly it can break any time we at OTP make a change in the area. Whenever you go outside the documented interfaces, you are setting yourself up for failure later on.

Again: I cannot stop from shooting yourself in the foot, I can only recommend that you don’t.

Speaking of which, I am a little curious about the have the pid_to_list and list_to_pid functions, but not pid_to_integer and interger_to_pid. This, These two functions can also be implemented.

They could, but doing it safely and correctly there would be little performance benefit over the list versions, and the result would certainly not fit into 64 bits.

What type of performance are you looking for here?

I recently gave a presentation at a BEAM meetup in Stockholm, where I described 3 ways of implementing a mutex in Erlang. All of them are safe, and the cost seems to be somewhere around 5-40 us (with low contention). They differ somewhat in semantics and scalability.

  1. Use ets:update_counter/3 as a semaphore - fast, but no ordering
  2. Use true ets bags as a queue - semi-fast, ordered, but scales badly
  3. Use a gen_server with a map of FIFO queues - fast enough, ordered, scales

The first alternative is used by gproc_pool (also see setup_wait() in the same module)

The third alternative is used in mnesia_rocksdb, and I also extracted it as a separate library, ordered_mutex.

1 Like

What I’m looking for is similar to what you listed, except I’m looking for a more efficient way to do it

Can I simulate a local pid break

I wrote this test code and ran it for a long time without break

loop() ->
	spawn(fun() -> loop1() end).

loop1() ->
	spawn(fun() -> Pid = self(), not (Pid == eGPidInt:intToPid(eGPidInt:pidToInt(Pid))) andalso io:format("IMY******error ~p ~n", [Pid]) end),
	loop1().

Are you sure you actually need this?

Not knowing exactly what you need it for, my spontaneous reaction is still that you should either consider a different language, or think about whether you should modify your process model.

There are certainly situations where a fast mutex can come in handy in Erlang, but at least for the situations I’ve run into, a few tens of microseconds is plenty fast.

In the slg game, all players move in a huge map, they will build buildings on the map grid, attack resource fields, attack wild monsters, attack cities and attack each other. This involves a lot of mutual exclusion and concurrent modification of data. There may be a system bottleneck if managed by a single process. And some processes are not easy to deal with, such as the world shop, the number of goods in the shop will be uncertain to increase or decrease, the price will change, the player’s money is hosted on a third-party server, may also change at any time, the player to buy these goods, will also be a tedious process. if i have the global lock, all of these problems can be easily solved by writing code like this:

logic_in_player_process() ->
	Fun = fun() ->
		%% get global data, general it's in the ets table
		%% do some check
		%% do some logic
		%% rewrite the global data
		ok
	end,
	LockKeys = someLockKeys,
	eGLock:lockApply(LockKeys, {Fun, []}),
	
	
	%% Or it can be
	try
		ok = eGLock:tryLock(LockKeys),
		%% get global data, general it's in the ets table
		%% do some check
		%% do some logic
		%% rewrite the global data
		ok
	catch _C:_R:_S ->
		% something
		ok
	after
		eGLock:releaseLock(LockKeys)
	end.

This avoids process overflows or message queue explosions, and also simplifies some process handling logic.

Of course, I think the performance of lock based on ets tables is sufficient, but it would be better to have cheap and faster ones

It does not look concurrent to me. In fact, these changes are expected to be serialized, and this sounds like a great use for messaging (gen_server:call or gen_server:cast if you aren’t expecting a reply).

In general, many concurrent data modification functions can be used with gen_server:call or gen_server:cast, but this requires a management process to handle the logic, but the map is too large or too many players, and a single process will have a mailbox heap that takes up a lot of memory Or it’s too slow causing the request to time out

I would suggest the process-based mutex if you want to try one of the ones I described.

The ets-based mutex is the fastest in raw benchmarks, but the lack of ordering introduces a starvation risk, and with jobs of significant cost, this can become an issue (did in the mnesia_rocksdb case). The gen_server-based version has nearly the same low-level performance, but actually serves the requests in order, which can make a huge difference.

It’s usually best to start with a robust solution, and then optimizing only when it’s clear through profiling that it’s a significant bottleneck.

1 Like