load-linked/store-exclusive パラダイムの背後にある考え方は、ストアがロードの直後にメモリ操作を介在せずに続き、その場所に他に何も触れていない場合、ストアは成功する可能性が高いということです。そうでなければ、店が確実に失敗する場所に触れました。明らかな理由もなくストアが失敗しないという保証はありません。ただし、ロードとストアの間の時間が最小限に抑えられ、その間にメモリ アクセスがない場合は、次のようなループが発生します。
do
{
new_value = __LDREXW(dest) + 1;
} while (__STREXW(new_value, dest));
通常、数回の試行で成功することが期待できます。古い値に基づいて新しい値を計算するためにかなりの計算が必要な場合は、ループを次のように書き直す必要があります。
do
{
old_value = *dest;
new_value = complicated_function(old_value);
} while (CompareAndStore(dest, new_value, old_value) != 0);
... Assuming CompareAndStore is something like:
uint32_t CompareAndStore(uint32_t *dest, uint32_t new_value, uint_32 old_value)
{
do
{
if (__LDREXW(dest) != old_value) return 1; // Failure
} while(__STREXW(new_value, dest);
return 0;
}
新しい値の計算中に *dest が変更された場合、このコードはメイン ループを再実行する必要がありますが、他の理由で __STREXW が失敗した場合は、小さなループのみを再実行する必要があります [これは、 __LDREXW と __STREXW の間には約 2 つの命令しかありません]
補遺
「古い値に基づいて新しい値を計算する」ことが複雑になる可能性がある状況の例は、「値」が事実上複雑なデータ構造への参照である場合です。コードは、古い参照を取得し、古いデータ構造から新しいデータ構造を派生させてから、参照を更新する場合があります。このパターンは、「ベア メタル」プログラミングよりもガベージ コレクションされたフレームワークで頻繁に発生しますが、ベア メタルをプログラミングする場合でも、さまざまな方法で発生する可能性があります。通常の malloc/calloc アロケーターは一般にスレッドセーフ/割り込みセーフではありませんが、固定サイズの構造体のアロケーターはしばしばそうです。2 のべき乗数 (たとえば 255) のデータ構造の「プール」がある場合、次のようなものを使用できます。
#define FOO_POOL_SIZE_SHIFT 8
#define FOO_POOL_SIZE (1 << FOO_POOL_SIZE_SHIFT)
#define FOO_POOL_SIZE_MASK (FOO_POOL_SIZE-1)
void do_update(void)
{
// The foo_pool_alloc() method should return a slot number in the lower bits and
// some sort of counter value in the upper bits so that once some particular
// uint32_t value is returned, that same value will not be returned again unless
// there are at least (UINT_MAX)/(FOO_POOL_SIZE) intervening allocations (to avoid
// the possibility that while one task is performing its update, a second task
// changes the thing to a new one and releases the old one, and a third task gets
// given the newly-freed item and changes the thing to that, such that from the
// point of view of the first task, the thing never changed.)
uint32_t new_thing = foo_pool_alloc();
uint32_t old_thing;
do
{
// Capture old reference
old_thing = foo_current_thing;
// Compute new thing based on old one
update_thing(&foo_pool[new_thing & FOO_POOL_SIZE_MASK],
&foo_pool[old_thing & FOO_POOL_SIZE_MASK);
} while(CompareAndSwap(&foo_current_thing, new_thing, old_thing) != 0);
foo_pool_free(old_thing);
}
複数のスレッド/割り込み/同時に同じものを更新しようとするものが頻繁にない場合、このアプローチにより更新を安全に実行できるはずです。同じアイテムを更新しようとする可能性のあるものの間に優先関係が存在する場合、最も優先順位の高いものは最初の試行で成功することが保証され、次に優先順位の高いものは、によってプリエンプトされないすべての試行で成功しますロックを使用している場合、更新を実行したい最も優先度の高いタスクは、優先度の低い更新が完了するまで待機する必要があります。CompareAndSwap パラダイムを使用すると、最も優先度の高いタスクは優先度の低いタスクの影響を受けません (ただし、優先度の低いタスクは無駄な作業を行う必要があります)。