• 忘掉天地
  • 仿佛也想不起自己
bingliaolongBingliaolong  2020-05-16 15:51 Aet 隐藏边栏 |   抢沙发  7 
文章评分 6 次,平均分 5.0

概述

如果我们在线程间共享数据,我们需要设置规则,哪个线程可以访问数据的哪一位,什么时间以及如何将更改传达给关心数据的其他线程。

关键点

问题

从整体上来看,所以线程之间共享数据的问题,都是修改数据导致的。如果所有的共享数据都只读的,就没有问题,因为一个线程所读取的数据不受另一个线程是否正在读取相同的数据而影响。

竞态条件

在并发中,竞态条件就是结果取决于两个或更多的线程上的操作执行的相对顺序的一切事物。

C++标准还定义了术语:数据竞争
表示因对单个对象的并发修改而产生的特定类型的竞争条件。(数据竞争会造成可怕的未定义行为)

避免竞态条件

  • 最简单的选择,用保护机制封装我们的数据结构,以确保只有实际执行修改的线程能够在不变量损坏的地方看到中间数据
  • 修改数据结构的设计及其不变量,从而令修改作为一系列不可分割的变更来完成,每个修改均保留其不变量。这通常被称为“无锁编程”。
  • 另一种方式,是将对数据结构的更新作为一个事务来处理,就如同在一个事务内完成数据库的更新一样。

互斥量 mutex

在访问共享数据结构之前,锁定lock()与该数据相关的互斥量,当访问数据结构完成后,解锁unlock()该互斥量。

  • 全局变量
  • some_mutex用于保护全局变量
  • add_to_list中和list_contains中都使用了lock_guard的对象,并且这个对象管理的是同一个some_mutex,这就导致在这两个函数中的访问是互斥的。也就是说,list_contains无法在add_to_list进行修改的半途中看到some_list.

死锁

通俗的讲,有两个线程,线程A中持有Mutex_b并等待Mutex_a,线程B中持有Mutex_a并等待Mutex_b

四个必要条件

  1. 互斥条件(Mutual Exclusion): 至少有一个资源必须是不能共享的,即一次只有一个进程使用
  2. 占有并等待(Hold and Wait): 进程至少持有一个资源,并等待另一个资源,这个资源被另一个进程所占有
  3. 不可剥夺条件(No Preemption): 资源只能由占有它的进程自愿释放
  4. 循环等待条件(Circular Wait): 存在一组等待进程,其中每个进程都在等待下一个进程所持有的资源

解决:

  • 通过构造lock_guard对象实例,来对应互斥量
  • 另附加一个参数std::adopt_lock告诉lock_guard对象该互斥量已被锁,并且它们只应该沿用互斥量上已有锁的所有权,而不是试图在构造函数中锁定互斥量

避免:

1.避免嵌套锁

2.在持有锁时,避免调用用户提供的代码

3.以固定顺序获取锁

4.使用锁层次

层次互斥量直接的死锁是不可能出现的,因为互斥量本身实行了锁定顺序。

5.将这些设计准则扩展到锁之外

unique_lock

unique_locklock_guard提供更多的灵活性。

一个unique_lock实例并不总是拥有与之相关联的互斥量。

第二个参数:

  • std::defer_lock
    • 表示该互斥量在构造时应保持未被锁定
    • 换句话说,这个标签会告诉std::unique_lock对象先不要锁上互斥量,需要上锁的时候,我自己回去手动锁
  • std::adopt_lock
    • 该互斥量已被锁,并且它们只应该沿用互斥量上已有锁的所有权,而不是试图在构造函数中锁定互斥量
    • 换句话说,这个标签会告诉std::unique_lock对象这个互斥量对象已经在别的地方被锁上了,所以只需要在析构里去释放就可以了
  • std::try_to_lock
    • 此标签构造std::unique_lock对象会尝试锁定其管理的互斥量,但不会阻塞等待

作用域之间转移锁的所有权

因为unique_lock实例并没有拥有与其相关的互斥量,所以通过四处移动实例,互斥量的所有权可以在实例之间进行转移。

在某些情况下这些转移是自动发生的。而在其他情况下,必须通过调用std::move()来显式实现。
从根本上来讲,这个取决于源是否是左值(实变量或对实变量的引用)或者是右值(某种临时量)。

对于右值,其所有权是自动转换的,而对于左值,所有权转移必须显式的完成。

  • lk是函数内部的变量,它可以被之间返回,无须std::move(),编译器会负责调用移动构造
  • process_data()函数里面,将所有权转移到它自己的unique_lock实例

call_once

  1. C++标准库提供了std::once_flag和std::call_once来处理条件竞争。
  2. 比起显示的检查指针,每个线程只需要使用std::call_once,在std::call_once结束时,就能安全的知道指针已经被其他线程初始化了
  3. std::call_once比显示使用互斥量消耗更少的资源

  1. foo函数中,std::call_once确保init_resource函数只被调用一次,即使在多线程环境中,多个线程同时调用foo函数时也是如此
    1. 一旦资源被初始化,之后的std::call_once调用就不会再执行init_resource函数,因为resource_flag已经被设置

本文为原创文章,版权归所有,欢迎分享本文,转载请保留出处!

bingliaolong
Bingliaolong 关注:0    粉丝:0 最后编辑于:2023-09-16
Everything will be better.

发表评论

表情 格式 链接 私密 签到
扫一扫二维码分享