Skip to content

Commit e984fdc

Browse files
committed
Adding docs
1 parent 21912d7 commit e984fdc

File tree

5 files changed

+55
-12
lines changed

5 files changed

+55
-12
lines changed

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ the number of requests to acquire the lock, and the other holds the number of re
4141
This approach offers several advantages over conventional spinlock implementations. Firstly,
4242
lock acquisition is ordered, meaning locks are acquired in the same sequence they are requested.
4343
This feature ensures consistent and predictable latency. Secondly, it tracks the number of
44-
processes concurrently busy-waiting to acquire a lock. By allowing only the initial few processes
44+
processes concurrently busy-waiting to acquire a lock. By allowing only the next process
4545
to busy-wait in a tight loop and interrupting spinning for the others with `erlang:yield/0`,
4646
this method reduces spinlock starvation and offers other processes more opportunities to run.
4747

rebar.config

+6
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,9 @@
88
{plugins, [covertool]}.
99

1010
{covertool, [{coverdata_files, ["ct.coverdata"]}]}.
11+
12+
{ex_doc, [
13+
{source_url, <<"https://github.com/farhadi/spinlock">>},
14+
{extras, [<<"README.md">>, <<"LICENSE">>]},
15+
{main, <<"readme">>}
16+
]}.

src/spinlock.app.src

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{application, spinlock, [
22
{description, "Spinlock for Erlang and Elixir"},
3-
{vsn, "0.2.0"},
3+
{vsn, "0.2.1"},
44
{registered, []},
55
{applications, [
66
kernel,

src/spinlock.erl

+43-6
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,25 @@
1919

2020
-export_type([spinlock/0]).
2121

22+
%% @equiv new([])
2223
-spec new() -> #spinlock{}.
2324
new() ->
2425
new([]).
2526

26-
-spec new([option()]) -> #spinlock{}.
27+
%% @doc Creates a new spinlock instance with the given options.
28+
%%
29+
%% Possible options are:
30+
%% <ul>
31+
%% <li>`{max_retry, MaxRetry}'
32+
%% <p>The lock is forecibly released after `MaxRetry' number of attempts.</p>
33+
%% </li>
34+
%% <li>`{atomics_ref, AtomicsRef}'
35+
%% <p>Uses the first two index of the given atomics array to store the state of the lock.
36+
%% If you want to use spinlock to implement transactions for an atomics array, you can use this
37+
%% option to avoid creating an extra atomics array.</p>
38+
%% </li>
39+
%% </ul>
40+
-spec new(Options :: [option()]) -> #spinlock{}.
2741
new(Options) ->
2842
MaxRetry = proplists:get_value(max_retry, Options, 100_000),
2943
is_integer(MaxRetry) andalso MaxRetry > 0 orelse error(badarg),
@@ -37,20 +51,35 @@ new(Options) ->
3751
max_retry = MaxRetry
3852
}.
3953

40-
-spec acquire(#spinlock{}) -> lock_id().
54+
%% @doc Acquires a lock for the current process.
55+
%%
56+
%% This will busy-wait until a lock can be acquired, or a maximum configured number
57+
%% of attemps is reached. Returned `lock_id' is used to release the lock later.
58+
-spec acquire(Lock :: #spinlock{}) -> lock_id().
4159
acquire(Lock = #spinlock{ref = Ref}) ->
4260
LockId = atomics:add_get(Ref, 1, 1),
4361
spin(Lock, LockId - 1, undefined, 0).
4462

45-
-spec release(#spinlock{}, lock_id()) -> ok | {error, already_released | invalid_lock_id}.
63+
%% @doc Releases an already acquired lock.
64+
%%
65+
%% This will release the lock for the given LockId.
66+
%% Returns `ok' if the lock is successfully released. Otherwise returns
67+
%% `{error, already_released}' if the lock is already released
68+
%% or `{error, invalid_lock_state}' if the given LockId has never been acquired.
69+
-spec release(Lock :: #spinlock{}, LockId :: lock_id()) ->
70+
ok | {error, already_released | invalid_lock_id}.
4671
release(#spinlock{ref = Ref}, LockId) ->
4772
case atomics:compare_exchange(Ref, 2, LockId - 1, LockId) of
4873
ok -> ok;
4974
Id when Id >= LockId -> {error, already_released};
5075
_ -> {error, invalid_lock_id}
5176
end.
5277

53-
-spec transaction(#spinlock{}, fun(() -> any())) -> any().
78+
%% @doc Executes the given function in a transaction.
79+
%%
80+
%% Acquires the lock, executes the given function, and releases the lock when the function has returend.
81+
%% The lock is released even if the function fails with an exception.
82+
-spec transaction(Lock :: #spinlock{}, Fun :: fun(() -> any())) -> any().
5483
transaction(Lock, Fun) ->
5584
LockId = acquire(Lock),
5685
try Fun() of
@@ -59,7 +88,11 @@ transaction(Lock, Fun) ->
5988
release(Lock, LockId)
6089
end.
6190

62-
-spec status(#spinlock{}) ->
91+
%% @doc Returns the status of the given lock.
92+
%%
93+
%% You can use this function for debugging purposes, to check the current status of the lock,
94+
%% and to see how many process are waiting to acquire the lock.
95+
-spec status(Lock :: #spinlock{}) ->
6396
#{released => non_neg_integer(), is_locked => boolean(), waiting => non_neg_integer()}.
6497
status(#spinlock{ref = Ref}) ->
6598
Released = atomics:get(Ref, 2),
@@ -70,13 +103,17 @@ status(#spinlock{ref = Ref}) ->
70103
waiting => max(Total - Released - 1, 0)
71104
}.
72105

106+
%%%-------------------------------------------------------------------
107+
%% Internal functions
108+
%%%-------------------------------------------------------------------
109+
73110
spin(Lock = #spinlock{ref = Ref, max_retry = MaxRetry}, ExpectedId, ReleasedId, MaxRetry) ->
74111
atomics:compare_exchange(Ref, 2, ReleasedId, ReleasedId + 1),
75112
spin(Lock, ExpectedId, ReleasedId, 0);
76113
spin(Lock = #spinlock{ref = Ref}, ExpectedId, LastReleasedId, Retry) ->
77114
case atomics:get(Ref, 2) of
78115
LastReleasedId ->
79-
LastReleasedId < ExpectedId - 10 andalso erlang:yield(),
116+
LastReleasedId < ExpectedId - 1 andalso erlang:yield(),
80117
spin(Lock, ExpectedId, LastReleasedId, Retry + 1);
81118
ExpectedId ->
82119
ExpectedId + 1;

test/spinlock_SUITE.erl

+4-4
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ receive_msg() ->
2727
all() ->
2828
[
2929
new,
30-
acquire_and_release,
31-
ordered_acquire,
30+
lock_status,
31+
waiting_status,
3232
concurrent_acquire,
3333
acquire_timeout,
3434
invalid_lock_id,
@@ -45,7 +45,7 @@ new(_Config) ->
4545
Lock3 = spinlock:new([{atomics_ref, Ref}]),
4646
?assertMatch(#spinlock{ref = Ref}, Lock3).
4747

48-
acquire_and_release(_Config) ->
48+
lock_status(_Config) ->
4949
Lock = spinlock:new(),
5050
?assertEqual(#{is_locked => false, released => 0, waiting => 0}, spinlock:status(Lock)),
5151
LockId = spinlock:acquire(Lock),
@@ -56,7 +56,7 @@ acquire_and_release(_Config) ->
5656
?assertEqual({error, already_released}, spinlock:release(Lock, LockId)),
5757
?assertEqual(#{is_locked => false, released => 1, waiting => 0}, spinlock:status(Lock)).
5858

59-
ordered_acquire(_Config) ->
59+
waiting_status(_Config) ->
6060
Lock = spinlock:new(),
6161
LockId = spinlock:acquire(Lock),
6262
?assertEqual(1, LockId),

0 commit comments

Comments
 (0)