管理线程

  1. 当指定的入口函数返回时,该线程就会退出,std::thread可以与任何可调用(callable)类型一起工作,包括函数、lambda表达式、函数指针、带有函数调用操作的类的实例等
  2. 我们所提供的函数对象被复制到属于新创建的执行线程的存储器中,并从那里调用
  3. 我们需要在std::thread对象被销毁之前决定线程是结合还是分离,线程本身可能在结合或分离它之前早就已经结束了,否则程序就会被终止(std::thread的析构函数调用std::terminate())
  4. 如果我们不等待线程完成,那么我们需要确保通过该线程访问的数据是有效的,直到该线程完成为止,特别是当线程函数持有局部变量的指针或引用,且函数退出时候线程尚未完成时。我们可以通过结合(join)线程确保在函数退出前该线程执行完毕
  5. 如果一个线程成为分离的,获取一个引用它的std::thread对象也是不可能的,所以它也不再能够被结合,被分离的线程通常被称为守护线程,无需任何显式的用户界面,而运行在后台
  6. std::thread构造函数中的参数会以默认的方式被复制到内部存储空间,即使函数中的相应参数为引用,此时如果我们需要引用可以用std::ref来包装确实需要被引用的参数,这样就能正确传入参数的引用
  7. 当我们传递一个成员函数的指针作为参数时,前提是提供一个合适的对象指针作为第一个参数,构造函数的第三个参数会成为成员函数的第一个参数。
  8. std::thread对象是可移动的,但是不可复制,这确保了在任意时刻只有一个对象与特定的执行线程相关联
  9. 当一个std::thread对象已经有了一个相关联的线程的情况下,我们不能仅仅通过向管理线程的std::thread对象赋值一个新的值来“舍弃”一个线程
  10. std::thread::hardware_currency()返回一个对于给定程序执行时能够真正并发运行的线程数量的指示,如果该信息不可用可能会返回0.当我们运行比硬件所能支持的更多的线程时,上下文的切换将意味着更多的线程会降低性能
  11. 线程标识符是std::thread::id类型的,我们可以通过指定的std::thread对象调用get_id()成员函数来获得,也可以在当前线程通过调用std::this_thread::get_id()来获得当前线程的线程标识符。如果两个线程标识符相等,则它们代表着同一个线程或两者都具有“没有线程”的值

在线程间共享数据

  1. 将线程用作并行的关键优点之一,是在它们之间简单、直接地共享数据的潜力,而共享数据的不正确使用是与并发有关的错误的最大诱因之一
  2. 线程间共享数据的问题都是修改数据导致的,如果所有共享数据都是只读的,就没有问题,因为一个线程所读取的数据不受另一个线程是否正在读取相同的数据而影响
  3. 编写使用并发的软件中大量的复杂性来源于避免有问题的竞争条件,由C++标准提供的保护共享数据的最基本机制是互斥元,在访问共享的数据结构之前,锁定与该数据相关的互斥元,当访问数据结构完成后,解锁该互斥元
  4. 互斥元也有死锁和保护过多或过少数据的问题,并且如果一个函数返回对受保护数据的指针或引用,那么能够访问(并可能修改)该指针或引用的任意代码现在可以访问受保护的数据而无需锁定该互斥元
  5. 除了检查函数没有向其调用者传出指针和引用,检查它们没有向其调用的不在我们掌控之下的函数传入这种指针或引用,也是很重要的,特别是当函数通过函数参数或其他方式在运行时提供的
  6. 不要将受保护数据的指针和引用传递到锁的范围之外,无论是通过从函数中返回它们、将其存放在外部可见的内存还是作为参数传递给用户提供的函数
  7. 每个线程都在等待另一个释放其互斥元的情景称为死锁,它是需要锁定两个或更多互斥元以执行操作时的最大问题。std::lock()函数可以同时锁定两个或更多的互斥元,而没有死锁的风险,而且试图在已锁定的std::mutex上获取锁是未定义行为
  8. std::lock()提供了关于锁定给定互斥元的全或无定义的语义,当锁定一个互斥元后如果发生异常,已锁定的互斥元会自动释放
  9. 避免死锁的方法:
    (1)避免锁嵌套,如果持有一个锁,那就别获取锁
    (2)在持有锁时,避免调用用户提供的代码
    (3)以固定顺序获取锁
    (4)使用锁层次
    (5)讲这些设计准则扩展到锁之外
  10. std::unique_lockstd::lock_guard提供了更多的灵活性,一个std::unique_lock实例并不总是拥有与之相关的互斥元,可以将std::defer_lock作为构造函数第二参数传递,来表示该互斥元在构造时应保持未被锁定,可以通过调用lock()或者将std::unique_lock对象本身传递给std::lock()来获取。如果该实例拥有互斥元则构造函数必须调用unlock(),否则一定不能调用unlock(),可以通过owns_lock()成员函数来查询这个是否拥有互斥元的标识
  11. std::unique_lock可移动但是不可复制
  12. 锁定在恰当的粒度不仅关乎锁定的数据量,这也是关系到持有多长时间,以及在持有锁时执行哪些操作,一般情况下,只应该以执行要求的操作所需的最小可能时间而去持有锁
  13. 如果不能在操作的整个持续时间中持有所需的锁,我们就应该暴露在竞争条件之中
  14. 在初始化时保护共享数据需要避免二次检查锁定问题,这个模式在不获取锁的情况下首次读取指针,并仅当此指针为nullptr时获得该锁,一旦获得了锁,该指针要被再次检查,以防在首次检查和这个线程获取锁之间,另一个线程就已完成了初始化,但是这个模式可能造成在锁外部的读取与锁内部由另一线程完成的写入不同步,可能只能看到另一线程写入的指针而不能看到新创建的实例,从而在不正确的值上运行
  15. 我们可以使用C++标准库中所提供的std::call_oncestd::once_flag来处理初始化保护共享数据的问题,每个线程都使用std::call_once,到std::call_once返回时共享数据将会被某个线程初始化(以完全同步的方式),并且使用std::call_once比显式使用互斥元通常会有更低的开销,特别是初始化已完成的时候
  16. 对于需要单一全局实例的场合,static关键字可以用作std::call_once的替代品,因为只初始化一次
  17. 对于保护很少更新的数据结构,读操作比写操作更为频繁,我们可以使用读写锁std::shared_mutex,由单个写进程独占访问或共享,由多个“读”线程并发访问,对于更新操作,可以使用std::lock_guard或者std::unique_lock来配合std::shared_mutex,确保独占访问,而不需要更新的线程则使用std::shared_lock配合std::shared_mutex来获得共享访问
  18. 如果任一线程拥有共享锁,试图获取独占锁的线程会被阻塞,知道其他所有线程全都撤回他们的锁,同样地,如果任意一个线程具有独占锁,其他线程都不能获取共享锁或独占锁,知道第一个线程撤回了它的锁
  19. 在某些情况下,线程多次重新获取同一个互斥元却无需事先释放它是可取的,此时我们可以使用标准库提供的递归锁std::recursive_mutex,它与std::mutex的区别在于可以同时在同一个线程中的单个实例上获取多个锁,在互斥元被其他线程锁定之前需要释放所有的锁,lock()几次就需要unlock()几次。不过不推荐使用递归锁,因为可能导致草率的想法和糟糕的设计