比特币多久上交易所的:Rust如何保证多线程应用程序中的安全性

  Rust的大部分开发人员都有C/C++背景,这让开发者很容易过渡到Rust并行性,因为它是非常相似。但是对于许多来自其他开发语言的人来说,这是一个挑战。在本文中,我们将逐步介绍标准的Rust并行性工具及其背后的动机。在一开始,这将需要对硬件进行深入研究,然后是对诸如原子之类的低级工具的解释,最后是对诸如Mutex之类的高级工具的解释。最后,我们将说明Rust如何保证多线程应用程序中的安全性。

  在Rust中,当您听到人们谈论并行性和并发性时,它主要是关于框架的,因为Rust作为一种语言不支持任何特定的并行性或并发性抽象,而是提供了最低限度的要求,例如标准线程和多个同步原语。这是我们将在本文中探讨的最低限度的内容。

  为什么并行性很难

  首先,我们需要了解为什么并行性很难,原因是硬件,操作系统和编译器过于复杂。 自1970年以来,处理器核心不再直接与内存配合使用,而是使用复杂的缓存和写缓冲区层次结构。

  

  我们甚至不需要查看整个层次结构就可以理解为什么如此困难。让我们删除所有缓存并仅考虑写缓冲区。写缓冲区对于处理器的性能绝对必不可少,因为对内存的写操作非常昂贵,我们希望尽可能多地对其进行批处理。

  

  考虑下我们在两个core上运行的程序。该程序有一个关键部分,我们不想在两个core上同时执行。确保这一点的方法之一是使用目的flag。core使用flag来声明其进入关键部分的flag。从逻辑上讲,如果其中一个core进入了关键部分,则它们的flag为非零,而另一个core也就不会进入。但是,如果两个core都写了它们的flag而不刷新缓冲区,那么它们都将进入临界区,因为它们将从内存中读取flag的0值。

  考虑该问题的另一种方法是认为写缓冲区通过在flag1=1之前执行flag2!=0来对操作顺序进行了重新排序。类似地,我们可以认为缓存也对操作进行了重新排序。

  执行优化(如子表达式消除)的编译器以及执行预取和推测等操作的处理器也会重新排序操作, 结果源代码中的操作顺序将与特定core执行的顺序不同。实际上,当在两个单独的cord上并行执行时,相同的代码可以具有不同的操作顺序。

  如果我们不使用运行在不同core上的线程相互协作,那么操作顺序将不会成为问题。协作线程要求我们像上面的示例一样,认为操作X在线程B上的操作Y之前发生在线程A上。多线程要求我们能够讨论跨线程的操作之间的因果关系。没有特殊工具帮助是不可能的。

  低级原语(Low-Level Primitives)

  原子性是低级同步原语,它通过限制操作顺序使我们具有因果关系。这些原语必须是由处理器级,因为除了限制编译器之外,我们还希望从高速缓存级的重排序和其他方面限制处理器。 原子性提供两个保证:

  · 我们可以对它们执行读/写操作,而不必担心读或写被破坏。

  · 原子操作即使在线程之间也可以保证它们相对于彼此执行的顺序。 实际上,原子甚至会在非原子操作上强制执行该顺序,我们将在后面看到。

  要求对原子变量进行的每个操作都必须具有排序类型:

  Ordering::Relaxed

  Ordering::Acquire 和 Ordering::Release(或其联合替代Ordering :: AcqRel)

  ordering::seqcst-序列一致性的缩写

  您几乎总是会使用SeqCst,它应用最强的约束并且最容易推论。relaxed应用了最弱的约束,并且非常不直观,因此除非您正在开发低级别的高性能代码,否则应该远离它。就认知复杂性而言,Acquire/Release是中间点,但在SEQCST上,你几乎永远不会喜欢它。然而,理解Acquire/Release对于理解高级同步原语(如Mutex和RwLock)非常有帮助。

  获取/释放(Acquire/Release)

  如前所述,,Acquire/Release是硬件级操作,因为它们为硬件生成特殊指令。可以通过以下方式使用,Acquire/Release:

  let x=AtomicUsize::new(0);

  let mut result=x.load(Ordering::Acquire);

  result +=1;

  x.store(result, Ordering::Release); // The value is now 1.

  acquire只能用于加载操作,而release只能用于存储操作。Acquire/Release具有以下规则:

  > acquire-在代码中发生的所有内存访问均在所有线程可见之后保留(记住线程B和C可以不同地感知线程A对内存的操作顺序);

  release-在代码中发生在它之前的所有内存访问在它之前,所有线程都可以看到它;

  因此,在以下情况下,如果线程A执行左侧代码,则线程B和C可以在内部进行所有操作并进行交换,如下所示。然而,在acquire之前,他们看不到a=“bye”的发生。

  

  通过Acquire/Release,我们可以在线程之间建立因果关系。 例如在下面的代码中,我们可以假设如果b为true,则a也必须也设置为true。

  let x=Arc::new(AtomicBool::new(false));

  let y=Arc::new(AtomicBool::new(false));

  {

  let x=x.clone();

  let y=y.clone();

  thread::spawn(move || {

  x.store(true, Ordering::Release);

  y.store(true, Ordering::Release);

  });

  }

  {

  let x=x.clone();

  let y=y.clone();

  thread::spawn(move || {

  let b=y.load(Ordering::Acquire);

  let a=x.load(Ordering::Acquire);

  if b { assert!(a); }

  });

  }

  使用Atomic Acquire/Release,我们可以实现一个全功能的spinlock,它保护代码的某个区域不受多个线程的并发访问:

  while(locked.compare_exchange(false, true, Ordering::Acquire,

  Ordering::Acquire)) {}

  // Do important stuff that only one thread can execute at a time.

  locked.store(false, Ordering::Release);

  请注意,除了上述限制之外,“Acquire/Release”操作还具有另外一个模糊规则,可以防止自旋锁(如上面的锁)受到干扰。 Rust从C11内存模型继承了它。

  顺序一致性

  不幸的是,在许多情况下,Acquire/Release仍然存在很难的争论。请看以下代码:

  

  在这段代码中,两条消息都有可能被打印,这意味着线程c和线程d对最先发生的事件有不一致的视图。换句话说,使用acquire/release没有全局操作顺序。acquire/release只是创建传递的因果关系,seqcst建立了一个全局操作顺序。如果我们将上述获取/发布替换为seqcst,则最多将打印一条消息。更正式地说,seqcst遵循以下规则:

  在SeqCst操作之前/之后发生的所有原子操作在所有线程上都保持在其之前/之后。普通的非原子读和写操作可能会在原子读取中向下移动,或者在原子写入中向上移动。

  在rustseqcst中,它会发出一个防止不需要的重新排序的内存屏障。 不幸的是,SeqCst比纯Acquire / Rease更昂贵,然而,在全局范围内仍然可以忽略不计,因此强烈建议尽可能使用seqcst。

  高级原语(High-Level Primitives)

  在上面的代码中,我们使用了线程,而没有解释它们是什么。 通常,人们将线程称为三件事:

  硬件线程,又名超线程;

  操作系统线程;

  绿色线程;

  超线程是指处理器将每个物理内核虚拟地拆分为两个虚拟内核时,可以实现更好的负载分配。操作系统线程由操作系统内部创建和管理,其中每个线程执行自己的代码,并轮流在虚拟内核上运行。大多数操作系统实际上使线程数不受限制,但是不幸的是,启动它们是昂贵的,因为它需要分配堆栈。绿色线程由用户软件实现,它们在OS线程之上运行。 绿色线程的优点是:即使在没有OS线程支持的环境中,它们也可以工作;它们比常规线程旋转起来快得多。

  不幸的是,锈已经去除了绿色的线程,现在只允许裸操作系统线程。 这样做是因为绿色线程不是零成本的抽象,这是Rust区别于其他语言的基本规则。

  绿色线程将需要繁重的运行时,即使每个程序不使用它们,也必须为此付费。

  但是,如果人们知道如何正确使用它们,则OS线程并不昂贵。 考虑带有自旋锁的上一个示例。 循环将在等待释放锁的同时燃烧CPU。 我们可以使用yield_now修复它:

  while(locked.compare_exchange(false, true, Ordering::Acquire,

  Ordering::Acquire) {

  std::thread::yield_now();

  }

  // Do important stuff.

  locked.store(false, Ordering::Release);

  由于操作系统可能比虚拟核心具有更多的线程,因此可能还有另一个线程正在等待调度。 yield_now告诉操作系统,它可以尝试在该虚拟内核上运行另一个线程,而第一个线程等待其锁定。

  Mutex和RwLock

  在上一节中,我们讨论了原子,它们是在硬件级别运行的低级同步原语。 Mutex和RwLock是高级同步原语,它们在OS级别上运行。

  Mutex和RwLock与我们之前看过的自旋锁相似,但有一个主要区别-自旋锁(spinlock)消耗CPU等待该锁的时间,而Mutex和RwLock释放当前的OS线程并且不燃烧CPU。 因此它们必须在操作系统级别而不是在纯硬件级别上进行操作,类似于我们对产生线程的自旋锁的修改。 但是产生自旋锁(spinlock)和互斥锁(Mutex)之间的主要区别在于,使用互斥锁(Mutex),操作系统知道一旦释放锁后何时唤醒等待线程,而使用产生自旋锁(spinlock),操作系统将偶尔地唤醒等待线程,希望释放锁此外,互斥锁的实现是特定于平台的。

  RwLock与Mutexes相似,因为它们保护某些区域的代码不被并发访问,但需要权衡取舍:

  Mutex将代码从读和写两方面锁定,而RwLock允许在没有写的情况下并发读取,类似于借用检查器;

  Mutex是一个同步生成器。在下一部分中,我们将看到什么是发送和同步特征,并将再次访问Mutex和RwLock;

  通过发送和同步(Send and Sync)实现安全性

  在上一篇文章中,我们了解了Rust如何通过借用规则和生存期来提供单线程安全性。 发送和同步特征将这种安全性扩展到多线程应用程序中。

  关于Rust安全性,最重要的了解是它只能防止数据争用,而不能阻止其他任何事情。当一个线程写入内存区域,而另一个线程从该区域读取或写入该区域时,就会发生数据争用,这会导致读写操作中断。数据争用尤其令人讨厌,因为它们可能导致未定义的行为。它们的原因是非常明确的,因此可以像Rust一样自动检测到或在语言级别上阻止它。

  另一方面,竞争条件是语义错误。例如我们可以错误地假设一个事件总是在另一个事件之前发生。竞争条件打破了域逻辑不变性,通常是不正确同步或缺少同步的标志。Rust无法使我们免于犯语义级别的错误。实际上,设计这样的语言是不可行的。死锁和活锁也是语义错误,是域逻辑不变的结果,例如我们假设锁A总是在持有锁B的同时发生,但是我们在代码中的某处实现了导致死锁的另一种方式。因此我们唯一要谈论的是Rust如何防止数据竞争。

  发送(send)和同步(Sync)旨在防止数据争用。 发送(send)和同步(Sync)是自动派生的,不安全和标记特征。

  · 工程师没有明确实现自动特征。相反编译器会自动派生它们。发送特征标记结构可在线程之间安全发送,同步特征标记结构可在线程之间安全共享。如果结构的所有字段均为“发送/同步”,则编译器将其确定为“发送/同步”。

  · 不安全的特征需要使用不安全的关键字才能实现。

  · 标记特征没有特别的方法,仅用于表达实现它们的结构的某些属性。例如eq-trait是标记特性的另一个例子。EQ告诉我们,一个已经实现相等操作的结构可以被使用,好像这个操作是自反的、对称的和传递的。

  大多数原语都是发送/同步(Send/Sync),因此几乎所有类型都是发送/同步(Send/Sync),除了Rc,Cell和RefCell。 Rc,Cell和RefCell不同步,因为它们实现内部可变性,这意味着对它们的操作(如果同时执行)可能导致数据争用。同样,Rc不是“发送”,因为它会将指针复制到相同的数据,因此线程不需要共享相同的副本即可引起数据竞争。因此Rust完全禁止跨线程发送Rc。 有趣的是,Cell和RefCell告诉编译器它们的不安全性的方式是通过用UnsafeCell包裹它们的内部字段,该对象的全部目的是防止Sync特性的自动派生。 Rc不使用UnsafeCell,而是显式声明自身!Send和!Sync。

  实现sync的对象的引用是send,反之亦然。 换句话说,&t:send表示t:sync,t:sync表示&t:send。在线程之间发送对象非常常见,而共享则不太常见。通常当我们希望线程访问同一对象时,我们将其包装到一个智能指针(如Arc)中,这导致我们发送其副本而不是实际共享它。要共享一个对象,我们需要共享其引用,如下所示:

  fn main() {

  let x=42;

  thread::spawn(|| {

  println!("{}", x);

  }).join.unwrap();

  }

  由于std :: thread :: spawn仅采用具有静态生存期的闭包,因此在大多数情况下不起作用。从堆栈中实际借用变量的唯一方法是使用第三方库(如crossbeam)中的作用域线程。

  现在让我们再谈谈Mutex与RwLock。 形式上,它们的实现如下所示:

  impl Send for Mutex

  impl Sync for Mutex

  和

  impl Send for RwLock

  impl Sync for RwLock

  这意味着我们可以将仅实现Send而不实现Sync的对象T包装到Mutex中,并且Mutex 将同时成为Send和Sync。 RwLock虽然不是同步生成器。 由于多个线程可以同时访问基础对象,因此它应该是Sync。 互斥锁阻止任何形式的同时访问,因此可以想到该对象被发送到持有锁的线程。

文章内容系本站作者个人观点,不代表本站对其观点赞同或支持,文章的版权归该作者所有。如需转载,请注明文章来源。本文地址:http://www.cis.net.cn/kejikuaixun/44246.html
留言与评论(共有 条评论)
验证码:

最新文章

Rust如何保证多线程应用程序中的安全性

科技快讯
Rust的大部分开发人员都有C/C++背景,这让开发者很容易过渡到Rust并行性,因为它是非常相似。但是对于许多来自其他开发语言的人来说,这是一个挑战。在本文中,我们将逐步介绍标准的Rust并行性工具及其背后的动机。在一开始,这将需要对硬件进行深入研究,然后是对诸如原子之类的低级工具的解释,最后是对诸如Mutex之类的高级工具的解释。

波场TRON创始人孙宇晨接受首尔经济Decenter专访

科技快讯
场CEO孙宇晨接受专访,韩国著名媒体《首尔经济》的分部decenter的专访,并对于收购一事及日后的规化,进行了详细的回答。▼波场于2018年7月完成对BitTorrent的收购,目前,BitTorrent已经成为波场的存储事业部(StorageBusinessUnit),在

关爱失业狗人士:伏地魔操作法,一台稳定的提款机。

科技快讯
币安的投票上币结束了,市面上的BNB又将少两万的流通,开心。前两天写《钱就在那里......》,很多朋友表示有些懵逼,为什么投个票都能玩儿出套利逻辑?好吧,好吧,算我输。伏地魔操作法早在上个世纪(去年)就已经讲过的最简单实用的套利方法,居然没有人在意。事实上伏地魔操作法从最早期的1.0到现在迭代到3

欧盟议会称区块链可为企业与公民“赋能”

科技快讯
欧洲议会成员认为,小企业可以从区块链技术整合中受益。行业、研究与能源委员会上周三投票推荐小企业研究区块链支付系统,旨在削减中介支付服务商所带来的一些成本。另外,据一份新闻稿显示,在下个月的全体会议期间,将向欧盟委员会提出有关该技术的口头问题。委员会特别提出了该技术的非货币用途,尤其是在数据控制、供应链管理、

算力集中孰好孰坏

科技快讯
TokenAnalyst最近的一份报告(称,一家矿场能够控制大约50%的比特币哈希率。这一观察基于以这个事实:五个大矿池成立了合资企业来启动一项新的云挖矿服务。到了2020年,比特币已成为高度中心化的系统;人们对系统的信任越来越集中在少数几个大矿池。加密货币研究公司TokenAnalyst表示,比特币网络哈希算力的集中化应引起关注,因为它会破

无政府状态是比特币的关键所在而不是问题

科技快讯
上周,在SXSW大会上有一个小组就被授权区块链与像比特币这样的非授权系统熟优孰弱进行了辩论。几天前,比特币编程员JimmySong在推特上发布了关于比特币的所有讨论的音频,我认为他在指出比特币的关键价值主张,以及比特币为何不值得与被许可的系统进行比较方面说得非常好。比特币有什么创新之处?正如宋在研讨会上的发言中所说,比特币

盈链9.14数字货币行情分析:币市反弹如温水煮青蛙,短期整体趋势偏弱

科技快讯
有那么一部恶搞的美国西部电影《死在西部的一百万种方式》,里面的人物角色的死法真是五花八门,脑洞大开。人们可能会被手枪射死、被野牛撞死、被冰块砸死、被照相机炸死、被毒蛇咬死、被自己的屁嘣死、被乌龟撞死、被蘑菇碰死、被食人花咬死、掉到坑里摔死等等,那些波谲云诡的年代有一百万种扑街的方式,总能找到一款适合自己的。

CRUZEO协议概述了其公平的链上治理过程

科技快讯
服务经济框架CRUZEO协议最近发布了其拟议的“非财阀式链上治理”结构的大纲。CRUZEO的目标是成为一个“拖放式”的平台,为所有技术背景的企业家建立去中心化的市场。在CRUZEO市场中,利益相关者包括构建者、消费者和代币持有者。CRUZEO强调用户可以同时代表多个身份。在生

币须2018.3.14行情分析:昨晚防洪预案,现在启动

科技快讯
今晚6点以后,主流币打破近两日,窄幅震啊震的,短时僵局,目前BTC处于破位状态,昨晚推文的防洪策略注意一下,眼下可能用得到。至于本轮在破位区间内真实意图,现在仅有6天数据,得出的结论还很模糊,这里不表述,先当作破位区间内蓄势处理即可。另外明天适当注意一下,毕竟

王团长日记第352篇:你怎么知道是韩国人?

科技快讯
昨天下午在深圳活动结束后,就从深圳连夜飞回来,是因为提前约好了,今天有韩国的一群人,要来公司喝茶嗑瓜子聊天谈合作。中午同事先带他们,去公司附近饭店吃饭,我后赶到饭店,对老板娘说:“有人定了位置。”老板娘说:“是不是一群韩国人。”我说:“是的。”然后我就反问:“你怎么知道是韩国人?”老板娘笑着说