SyncUnsafeCell文档提到需要用户自己提供同步机制,我想问下我这种使用方式会出现不同步的问题吗?
之所以用SyncUnsafeCell是因为:
-
我有一个 Vec<SyncUnsafeCell>, 会被并行的修改,但业务保证绝不会出现对同一个SyncUnsafeCell进行修改,只是具体是哪些会被修改是不确定的,但一定不会出现重复,所以实际是不存在并发修改的。这样避免运行时开销,不用加锁甚至没有原子操作。
-
然后还有会只读的并行的访问这个 Vec<SyncUnsafeCell> 的业务逻辑
-
流程2一定发生在流程1结束之后才开始(两个都是在主线程用rayon创建的并行任务),像1,2,1,2这样的流程。流程(1,2)的并行是用的相同的线程池调度(例如rayon),但是每个SyncUnsafeCell在那个线程上被修改/访问是完全不确定的。
请问我需要额外提供一些同步原语之类的来保证,每次2一定能观察到1的改动吗?会出现因为核心缓存导致脏读结果不一致吗?
1
共 7 条评论, 1 页
评论区
写评论那就看你的具体代码了; 说白了, 你要凑出两个条件
调度确实有可能, 任务调度出去的时候应该会有一个写屏障, 调度进来的时候会有一个读屏障。 因此, 你要是让写任务先调度出去, 然后再发生读任务的调度进来, 问题不大。 但这个是不是如此还真不好说。
比如你怎么通知读任务有数据的? 在通知的时候, 你自然还没有调度出去, 但通知发出去, 读任务可能就已经 调度进来了, 这顺序就是先读屏障, 后写屏障, 没有作用。
但还有可能你这个通知机制说不定也带写屏障, 说不定它能保证一个先写后读的屏障顺序, 这也没问题。 但这些说白了都是依赖外部代码的, 就算成了, 也是凑巧。
本质上, 你应该就是将一个不 Sync 的 T 欺骗成 T: Sync 了。 所以, 你应该做的就是将读写 T 线程安全起来, 自己的事情自己干。
--
👇
bnyu: "一个线程写 A, 另一个线程读 A" 这个不会同时发生,所以不存在并发读写,应该就没有线程安全问题吧
"一个线程写 A, 另一个线程读 A" 这个不会同时发生,所以不存在并发读写,应该就没有线程安全问题吧
--
👇
zylthinking: 不会重叠但可能并发吧, 就是一个线程写 A, 另一个线程读 A;
你管理流程1 tasks的工具(比如rayon)
可能起不到作用, 因为就算它做了, 也是在调度的时候做, 但你未必能保证写完成之后才调度读; 否则你就不用并发了, 单纯使用一个任务先写再读就好了。--
👇
bnyu: 感谢大佬解答。不能直接分发Vec的每个元素来并行是因为业务不是针对每个T来进行修改的,而是类似另一组Vec,这里的每个U会计算得知去修改Vec的哪个下标的哪个值(业务保证不会算出相同的index),所以Vec才需要多个可变修改。 1和2不会重叠,那这样我就放心使用这个Unsafe封装了。
--
👇
aj3n: 应该不会存在因为核心脏读导致的不一致,因为你既然说2在1结束之后才会开始,你管理流程1 tasks的工具(比如rayon)应该已经做过内存屏障的工作了;
主要问题还是2和1会不会重叠,你虽然说流程2会在流程1结束后开始,但是下一轮的流程1不会在上一轮流程2没结束的时候开始吧?
其实最理想的做法是直接用rayon scope之类的方法直接分发Vec每个元素的&mut T,然后在下一轮直接用scope只读,这样完全不需要Unsafe也没有心理负担,只是可能被生命周期艹;
不会重叠但可能并发吧, 就是一个线程写 A, 另一个线程读 A;
你管理流程1 tasks的工具(比如rayon)
可能起不到作用, 因为就算它做了, 也是在调度的时候做, 但你未必能保证写完成之后才调度读; 否则你就不用并发了, 单纯使用一个任务先写再读就好了。--
👇
bnyu: 感谢大佬解答。不能直接分发Vec的每个元素来并行是因为业务不是针对每个T来进行修改的,而是类似另一组Vec,这里的每个U会计算得知去修改Vec的哪个下标的哪个值(业务保证不会算出相同的index),所以Vec才需要多个可变修改。 1和2不会重叠,那这样我就放心使用这个Unsafe封装了。
--
👇
aj3n: 应该不会存在因为核心脏读导致的不一致,因为你既然说2在1结束之后才会开始,你管理流程1 tasks的工具(比如rayon)应该已经做过内存屏障的工作了;
主要问题还是2和1会不会重叠,你虽然说流程2会在流程1结束后开始,但是下一轮的流程1不会在上一轮流程2没结束的时候开始吧?
其实最理想的做法是直接用rayon scope之类的方法直接分发Vec每个元素的&mut T,然后在下一轮直接用scope只读,这样完全不需要Unsafe也没有心理负担,只是可能被生命周期艹;
应该存在, 并且这还不到缓存一致性问题层次, 而是一个线程安全问题。
从文档可看到,
This is just an UnsafeCell, except it implements Sync if T implements Sync.
Providing proper synchronization is still the task of the user, making this type just as unsafe to use.
因此, 这个安全性应该是 T 本身应该保证的, 若 T 不 sync, 则 UnsafeCell 也不 Sync。 换句话说, 若 T 不 sync, 则根本不可能跨线程还能编译的过去。
既然编译的过去, 那要么是 T 是 sync 的, 要么你伪造了 T:sync。 若有问题, 只有在你伪造的这个情况下才会出现。
因此, 这件事其实就是你正确实现 T 的 sync 语义的问题, 若不正确实现, 则外界不知道, 也未必一定替你做同步。
感谢大佬解答。不能直接分发Vec的每个元素来并行是因为业务不是针对每个T来进行修改的,而是类似另一组Vec,这里的每个U会计算得知去修改Vec的哪个下标的哪个值(业务保证不会算出相同的index),所以Vec才需要多个可变修改。 1和2不会重叠,那这样我就放心使用这个Unsafe封装了。
--
👇
aj3n: 应该不会存在因为核心脏读导致的不一致,因为你既然说2在1结束之后才会开始,你管理流程1 tasks的工具(比如rayon)应该已经做过内存屏障的工作了;
主要问题还是2和1会不会重叠,你虽然说流程2会在流程1结束后开始,但是下一轮的流程1不会在上一轮流程2没结束的时候开始吧?
其实最理想的做法是直接用rayon scope之类的方法直接分发Vec每个元素的&mut T,然后在下一轮直接用scope只读,这样完全不需要Unsafe也没有心理负担,只是可能被生命周期艹;
用std::thread::scope写了个demo
应该不会存在因为核心脏读导致的不一致,因为你既然说2在1结束之后才会开始,你管理流程1 tasks的工具(比如rayon)应该已经做过内存屏障的工作了;
主要问题还是2和1会不会重叠,你虽然说流程2会在流程1结束后开始,但是下一轮的流程1不会在上一轮流程2没结束的时候开始吧?
其实最理想的做法是直接用rayon scope之类的方法直接分发Vec每个元素的&mut T,然后在下一轮直接用scope只读,这样完全不需要Unsafe也没有心理负担,只是可能被生命周期艹;