假设源和目标架构不同,模拟器如何有效地转换内存障碍?我知道通常现代模拟器将使用JIT从源ISA转换到目标ISA,但知道哪些代码可以通过多个程序计数器访问,哪个不是很棘手,然后知道哪些指令可以安全地重新排序(由于ISA差异,JIT可能需要生成一些有效的东西),这似乎并不是非常棘手.
您甚至不能保证在指令流中找到明确的内存屏障,例如x86上的许多人依赖于对齐的字写入是原子的.模拟器是否保守地假设每个对齐的字写不能重新排序?这似乎是一个潜在的巨大开销,这让我想知道是否有任何已知的分析来解决这类问题.
multithreading emulation virtual-machine memory-barriers lockless
无效_mm_pause(无效)
下一条指令的执行被延迟特定的执行时间。该指令不会修改架构状态。此内在函数提供了特别重要的性能提升。
也就是说:
while (!acquire_spin_lock()) _mm_pause(); // code snippet 1
速度更快,功耗更低
while (!acquire_spin_lock()) continue; // code snippet 2
我可以理解为什么代码片段1的功耗比代码片段2低。
我不明白的是:
为什么代码片段1比代码片段2快?
我正在尝试使用C++ 11原子基元来实现各种原子" 线程计数器 ".基本上,我有一个关键的代码部分.在此代码块中,任何线程都可以从内存中自由读取.但是,有时,我想执行重置或清除操作,这会将所有共享内存重置为默认初始化值.
这似乎是一个使用读写锁的好机会.C++ 11不包含开箱即用的读写互斥锁,但可能更简单.我认为这个问题是一个很好的机会,可以更熟悉C++ 11原子基元.
所以我想了一会儿这个问题,在我看来,我所要做的就是:
每当线程进入临界区时,递增一个原子计数器变量
每当线程离开临界区时,减少原子计数器变量
如果线程希望将所有变量重置为默认值,则它必须原子等待计数器为0,然后原子地将其设置为某个特殊的"清除标志"值,执行清除,然后将计数器重置为0.
当然,希望递增和递减计数器的线程也必须检查清除标志.
因此,我刚才描述的算法可以用三个函数实现.increment_thread_counter()必须始终在进入临界区之前调用第一个函数.decrement_thread_counter()在离开临界区之前,必须始终调用第二个函数.最后,只有当线程计数器== 0时,clear()才能从临界区外部调用该函数.
这就是我想出的:
鉴于:
std::atomic<std::size_t> thread_counter clearing_flag设置为std::numeric_limits<std::size_t>::max()...
void increment_thread_counter()
{
std::size_t expected = 0;
while (!std::atomic_compare_exchange_strong(&thread_counter, &expected, 1))
{
if (expected != clearing_flag)
{
thread_counter.fetch_add(1);
break;
}
expected = 0;
}
}
void decrement_thread_counter()
{
thread_counter.fetch_sub(1);
}
void clear()
{
std::size_t expected = 0;
while (!thread_counter.compare_exchange_strong(expected, clearing_flag)) …Run Code Online (Sandbox Code Playgroud) 鉴于:
std::atomic<uint64_t> x;
uint64_t f()
{
x.store(20, std::memory_order::memory_order_relaxed);
x.store(10, std::memory_order::memory_order_relaxed);
return x.load(std::memory_order::memory_order_relaxed);
}
Run Code Online (Sandbox Code Playgroud)
假设只有一个线程写入,是否有可能f返回除 之外的值?对于非原子变量来说显然不是这样,但我不知道relaxed是否如此宽松以至于它会忽略同一线程中的数据依赖关系?10x
我正在设计一个新的改进的Logger组件(.NET 3.5,C#).
我想使用无锁实现.
记录事件将从(潜在地)多个线程中发送,尽管只有单个线程将执行实际输出到文件/其它存储介质.
从本质上讲,所有的编写器都将它们的数据排入某个队列,然后通过其他一些进程(LogFileWriter)进行检索.
这可以通过无锁方式实现吗?我无法在网上找到这个特定问题的直接参考.
考虑以下针对多线程搜索算法的无锁散列表的尝试(受本文启发)
struct Data
{
uint64_t key;
uint64_t value;
};
struct HashEntry
{
uint64_t key_xor_value;
uint64_t value;
};
void insert_data(Data const& e, HashEntry* h, std::size_t tableOffset)
{
h[tableOffset].key_xor_value = e.key ^ e.value;
h[tableOffset].value = e.value;
}
bool data_is_present(Data const& e, HashEntry const* h, std::size_t tableOffset)
{
auto const tmp_key_xor_value = h[tableOFfset].key_xor_value;
auto const tmp_value = h[tableOffset].value;
return e.key == (tmp_key_xor_value ^ tmp_value);
}
Run Code Online (Sandbox Code Playgroud)
这个想法是一个HashEntrystruct存储一个struct的两个64位字的XOR-ed组合Data.如果两个线程对结构的两个64位字进行交错读/写HashEntry,那么想法是读取线程可以通过再次进行异或并与原始进行比较来检测key.因此,可能由于损坏的哈希条目而导致效率损失,但是在解码的检索到的密钥与原始密钥匹配的情况下仍然保证了正确性.
该文件提到它基于以下假设:
对于本讨论的其余部分,假设64位存储器读/写操作是原子的,即在一个周期内读/写整个64位值.
我的问题是:上面的代码没有明确使用std::atomic<uint64_t>保证在C++ 11中是线程安全的吗?或者可以通过同时读/写来破坏各个64位字?即使在64位平台上?这与旧的C++ 98标准有何不同? …
我试图了解Interlocked.Exchange的正确用法,所以我实现了一个带有添加和删除功能的简单排序LinkedList.
如果这不是一个线程安全列表,显然要找到插入点,你会有类似下面的内容来找到插入新节点的正确点.
public void Insert(int newValue)
{
var prev = _header;
Node curr = _header.Next;
while(curr != null && curr.value > newValue )
{
prev = curr;
curr = curr.Next;
}
var newNode = new Node(newValue, curr);
prev.Next = newNode;
}
Run Code Online (Sandbox Code Playgroud)
下面是我对如何为并发列表执行此操作的看法.是否有太多Interlocked.Exchange正在进行?如果没有这个,插入物仍然是线程安全的吗?数百或数千个互锁操作会导致性能下降吗?
public void InsertAsync(int newValue)
{
var prev = _header;
Node curr = new Node(0, null);
Interlocked.Exchange(ref curr, _header.Next);
while (curr != null && curr.value > newValue)
{
prev = Interlocked.Exchange(ref curr, curr.Next);
}
//need some locking around prev.next …Run Code Online (Sandbox Code Playgroud) 我提出了一个想法,我试图实现一个无锁堆栈,不依赖于引用计数来解决ABA问题,并且还正确处理内存回收.它在概念上与RCU类似,并且依赖于两个特征:将列表条目标记为已删除,以及跟踪阅读器遍历列表.前者很简单,它只使用指针的LSB.后者是我对实现无限制无锁堆栈的方法的"聪明"尝试.
基本上,当任何线程试图遍历列表时,一个原子计数器(list.entries)会递增.遍历完成后,第二个计数器(list.exits)递增.
节点分配由push处理,释放由pop处理.
推送和弹出操作与天真无锁堆栈实现非常相似,但必须遍历标记为删除的节点才能到达未标记的条目.因此推送基本上就像链表插入一样.
pop操作类似地遍历列表,但它使用atomic_fetch_or将节点标记为在遍历时被删除,直到它到达未标记的节点.
遍历0个或更多标记节点的列表后,弹出的线程将尝试CAS堆栈的头部.至少有一个并发弹出的线程将成功,在此之后,进入堆栈的所有读者将不再看到以前标记的节点.
成功更新列表的线程然后加载原子list.entries,并基本上自旋加载atomic.exits,直到该计数器最终超过list.entries.这应该意味着列表的"旧"版本的所有读者都已完成.然后,该线程只是释放它从列表顶部交换的标记节点列表.
因此,弹出操作的含义应该(我认为)可能没有ABA问题,因为释放的节点不会返回到可用的指针池,直到所有使用它们的并发读取器完成,显然内存回收问题由于同样的原因,处理也是如此.
所以,无论如何,这是理论,但我仍然在实现上摸不着头脑,因为它目前无法正常工作(在多线程情况下).似乎我在免费问题之后得到了一些写作,但是我在查找问题时遇到了麻烦,或者我的假设可能存在缺陷而且无法正常工作.
无论是概念还是调试代码的方法,都会非常感谢任何见解.
这是我当前(损坏的)代码(使用gcc -D_GNU_SOURCE -std = c11 -Wall -O0 -g -pthread -o list list.c编译):
#include <pthread.h>
#include <stdatomic.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>
#include <sys/resource.h>
#include <stdio.h>
#include <unistd.h>
#define NUM_THREADS 8
#define NUM_OPS (1024 * 1024)
typedef uint64_t list_data_t;
typedef struct list_node_t {
struct list_node_t * _Atomic next;
list_data_t data;
} list_node_t;
typedef struct {
list_node_t * _Atomic head;
int64_t _Atomic size;
uint64_t _Atomic entries;
uint64_t _Atomic exits;
} list_t; …Run Code Online (Sandbox Code Playgroud) 我想从原子 uint32s 拼凑一个 uint64 原子计数器。计数器有一个写入器和多个读取器。编写器是一个信号处理程序,所以它不能阻塞。
我的想法是使用低位的代数作为读锁。读取器重试,直到整个读取过程中生成计数稳定,并且低位未设置。
以下代码在内存排序的设计和使用中是否正确?有没有更好的办法?
using namespace std;
class counter {
atomic<uint32_t> lo_{};
atomic<uint32_t> hi_{};
atomic<uint32_t> gen_{};
uint64_t read() const {
auto acquire = memory_order_acquire;
uint32_t lo, hi, gen1, gen2;
do {
gen1 = gen_.load(acquire);
lo = lo_.load(acquire);
hi = hi_.load(acquire);
gen2 = gen_.load(acquire);
} while (gen1 != gen2 || (gen1 & 1));
return (uint64_t(hi) << 32) | lo;
}
void increment() {
auto release = memory_order_release;
gen_.fetch_add(1, release);
uint32_t newlo = 1 + lo_.fetch_add(1, release);
if (newlo …Run Code Online (Sandbox Code Playgroud) 我想知道无锁编程相对于自旋锁有哪些优点?我认为当我们在一个线程(称为A)中使用CAS机制进行无锁编程时,如果其他线程更改了CAS中的值,A线程仍然需要再次循环。我认为这就像我们使用自旋锁一样!
我对此很困惑。虽然我知道CAS和spin-lock适合在锁争用不激烈的情况下使用,但是谁能解释一下在哪些场景下应该使用lock free,应该使用spin lock?