并发程序编写时,对单读单写的数据,通常认为是可以不用加锁的,最多注意下内存屏障;
问题描述
常见的单独单写并发时脏读的问题:
- PA 同一地址的并发读写 POD类型(C8 / U8 / I16 / U16 / I32 / U32 / I64 / U64 / B8 / F32 / F64)
- PB 同一地址的并发读写 SIMD 类型 (I128, U128)
- PC 同一地址的并发读写 数组与结构体
分析
此部分代码是架构相关的,有Arm / X86 上迁移问题
- A1 问题PA是基础,x86与Arm提供了不同的保证,以上基本类型内存对齐时,并发是可以保证的;
- A2 问题P2,P3视为复杂类型写入和读取是多次操作,一定会脏读;
violate
C/C++ 中的 volatile - 知乎 (zhihu.com)
volatile
不能解决多线程中的问题。
按照 Hans Boehm & Nick Maclaren 的总结,volatile
只在三种场合下是合适的。
- 和信号处理(signal handler)相关的场合;
- 和内存映射硬件(memory mapped hardware)相关的场合;
- 和非本地跳转(
setjmp
和longjmp
)相关的场合。
Intel spec
Intel® 64 and IA-32 Architectures Software Developer Manuals 卷3 - 8.1.1
PA问题中,地址对齐的访问都是原子的,同一缓存行内和不触发 Page Fault 和 Cache Miss 的不对齐操作也是原子的。
Arm spec
Arm Architecture Reference Manual for A-profile architecture B2.2.1 & B2.5.2
PA问题中,地址对齐的访问是single - copy atomic,非地址对齐的访问在允许 SCTLR_ELx 且芯片支持 LSE,且数据在同一个16b以内是原子的。
内存对齐
- 结构体中字段的偏移地址和结构体大小与编译器相关;
- 栈变量和全局变量、静态变量的首地址与编译器相关;
- 内存申请的首地址与运行时库和操作系统API相关;
gcc & msvc
(23条消息) 结构体字节对齐和位域对齐——VC、gcc_bytxl的博客-CSDN博客_gcc 结构体 起始地址
对齐应该确实与编译器相关,帖子中相当于 packed 属性默认值在 vs 和 gcc 中不一致,但是实际测试应该是都是 8。
linux malloc
malloc(3) - Linux manual page (man7.org)
The malloc() and calloc() functions return a pointer to the allocated memory, which is suitably aligned for any built-in type.
malloc.c - malloc/malloc.c - Glibc source code (glibc-2.37) - Bootlin 行 97
Alignment: 2 * sizeof(size_t) (default) (i.e., 8 byte alignment with 4byte size_t). This suffices for nearly all current machines and C compilers. However, you can define MALLOC_ALIGNMENT to be wider than this if necessary.
linux shmat & mmap
shmat(2): shared memory operations - Linux man page (die.net)
If shmaddr isn't NULL and SHM_RND is specified in shmflg, the attach occurs at the address equal to shmaddr rounded down to the nearest multiple of SHMLBA. Otherwise shmaddr must be a page-aligned address at which the attach occurs
自定义内存分配
自己做内存管理时,比如申请一大片内存,逐个切分时,会导致内存的起始地址不是对齐的,后续的字段也不对齐,从而潜在有并发问题;
PC 问题一些思考
写者持续更新 <a, b, c...>, 产生 <a1, b1, c1...> <a2, b2, c2....>多个版本的数据,读者持续读,预期是读到同一个版本的数据,不产生脏读,即读到 <a_i, b_i, c_i+1, ...>
加锁,如 spinlock
仿写 exanic-software/rwlock.h at master · cisco/exanic-software · GitHub
typedef uint32_t spinlock_t;
void spin_lock(spinlock_t* lock)
{
uint16_t v2o = __sync_fetch_and_add((violate uint16_t*)lock+1, 1);
while (*(violate uint16_t*)lock != v2o)
__ia32_pause();
}
void spin_unlock(spinlock_t* lock)
{
uint32_t v = *(violate uint32_t*)lock;
if ((U16)v != (U16)(v >> 16))
__sync_fetch_and_add((violate uint16_t*)lock, 1);
}
记录每个版本,更新版本号
typedef struct data_t {
char a;
char b;
char c;
char d;
} data_t
data_t datas[DATA_MAX];
uint32_t dataver;
Writer : | Reader:
write datas[i] | read dataver
write_barrier() | read_barrier()
write dataver | write datas[i]
double check
typedef struct data_t {
char a;
char b;
char c;
char d;
} data_t
data_t data;
uint32_t dataver;
Writer : | Reader:
write dataver | 1 read dataver as dataver0
write_barrier() | 2 read_barrier()
write data | 3 write data
| 4 if (daraver != dataver0) goto 1
总结
- 地址对齐是认为,基本类型的读写是原子的,可跨平台的
-
violate
不是必须的 - 更推荐用 CAS, FAA等原子操作显式地进行原子更新