0%

其实是在问哪些类型有 hash 函数和重载了 < 比较操作符。因为说的是 STL,所以不考虑内建类型。

可哈希和 == 比较

常见的可以哈希和 == 比较的集合类型有(以下省略 std 命名空间):

  • string / string_view
  • bitset
  • unique_ptr / shared_ptr

这样的标准库模板类其实非常少!通常,可以用于 hash 和相等的类型都能用于 < 比较

注意 map 和 set 通常不可哈希!包括:

--remerge-diff 选项在 git loggit show 命令中都存在。它用更容易阅读的方式显示一个合并结点相对于两个 parent 的变化。

下图是一个例子(来源在这里)。没用这个选项的时候会显示三路的差异,有三种颜色。只看绿色部分就是最终解决了冲突后的代码,但是看红色还得看红色是来自哪一个 parent,不够方便。

用了这个选项之后修改被整合在了不同的代码段中,只使用一栏标志(就不用再从左边去寻找代码来自哪个 parent,只需要看它属于哪一个代码块)。将红色的部分视为注释,只看正常颜色的字体,就可以得知冲突解决后的结果是:

继承 std::enable_shared_from_this 模板类之后就多了一个弱指针(_M_weak_this)。同时还多了一个 __enable_shared_from_this_base 方法,创建共享指针时该方法能被 ADL 找到,以关联和共享控制块。该方法是私有的,不过 __shared_ptr<typename, typename> 是友元类,因此能访问它。

共享指针模板类 __shared_ptr 有个私有方法 _M_enable_shared_from_this_with,它能通过 SFINAE 判断元素类型是否是继承自 enable_shared_from_this 的 CRTP 类,如果是则将自己的控制块关联给 enable_shared_from_this 中的弱指针。

按照这个逻辑,在创建共享指针的时候,_M_enable_shared_from_this_with 应该会被调用。这一点确实可以被验证(下面还有一些构造函数,就不展示了):

问题

发现是 mmc.exe 不工作了,“.msc 文件打开方式”的系统注册项是正常的。

sfc/dism/chkdsk 三件套,无果

sfc.exe /scannow
DISM.exe /Online /Cleanup-image /scanhealth
DISM.exe /Online /Cleanup-image /checkhealth
DISM.exe /Online /Cleanup-image /restorehealth

确实是显示发现了问题并修复,但是结果还是打不开。

chkdsk /f /r

Wait 和 notify 接口介绍

std::atomic<T>::wait 用来等待原子变量值的改变,如果原子变量值和给定的参数 old 相同则阻塞,直到被 notify_all() 或者 notify_one() 通知,或者自发地解除阻塞。因此用 wait 要在循环中使用。

// std::atomic<T>::wait
void wait( T old, std::memory_order order = std::memory_order_seq_cst ) const noexcept;
void wait( T old, std::memory_order order = std::memory_order_seq_cst ) const volatile noexcept;

Cppreference 上说这个接口通常比轮询和自旋锁更高效。看了一下 libstdc++ 的实现,实际上内部也是自旋锁,只是比我们在外面用自旋锁要高效一些。实现在 libstdc++-v3/include/bits/atomic_wait.h 文件中

wait 的实现

调用链路(同一缩进下函数的调用不区分顺序,不表示前者在后者之前调用):

__atomic_wait_address_v
    __detail::__waiter<std::true_type>::_M_do_wait_v
        __detail::__waiter_pool::_M_do_spin_v 🔍
            // do-while loop of:
            __detail::__waiter_pool::_M_do_wait 🔍
            // and
            __detail::_S_do_spin_v
                __detail::__atomic_spin 🔍

执行策略(C++17)

std::execution::seq
std::execution::par
std::execution::par_unseq
std::execution::unseq (C++20)

它们分别属于以下类型,但是使用的时候不要自己创建类型,应该直接使用标准库中提供的执行策略对象。

std::execution::sequenced_policy
std::execution::parallel_policy
std::execution::parallel_unsequenced_policy
std::execution::unsequenced_policy (C++20)

其中的并行策略仅仅是允许算法这样做,但不能强制算法按要求做。

Note that this is permission, not a requirement—the library may still execute the code on a single thread if it wishes.

Unwanted blocking

  • Deadlock
  • Livelock,和死锁的区别是在循环中积极检查条件,比如自旋锁,线程一直消耗 CPU 但始终无法前进
  • Blocking on I/O or other external input,线程在等待一个不定期的、可能永远不会到来的操作

Race conditions

  • Data races
  • Broken invariants
  • Lifetime issues

Code review

  1. 在并发访问下,哪些数据需要保护
  2. 如何确保数据受到保护?
  3. 此时其他线程可能位于代码的哪个部分?
  4. 当前线程持有哪些互斥锁?
  5. 其他线程可能持有哪些互斥锁?
  6. 在当前线程中执行的操作与在其他线程中执行的操作之间是否存在顺序要求?如何确保这些要求得到满足?
  7. 当前线程加载的数据是否仍然有效?是否可能已被其他线程修改?(比如 CAS、双重校验锁)
  8. 如果假设其他线程可能正在修改数据,这意味着什么?如何确保这种情况永远不会发生?

Testing

书 P345 给出了一些对线程安全队列进行测试的测试点。

线程池

实现可以 submit 任务并获取 future 的线程池

有了 std::future 就能对提交的任务做等待。

线程池初始化时就创建指定数量的工作线程,每个线程的任务就是在循环中从线程安全队列上获取任务并运行。每个任务的类型是 std::packaged_task<result_type()> task,每次有工作要提交都会包装到 std::packaged_task,工作的提交者因而可以获取 std::future

由于 C++23 才引入 std::move_only_function<>,书上实现了一个简单的替代。

修复 quicksort 工作线程的死锁

书上给了一个 quicksort 的例子,指出划分完成时,本线程先递归做完 new_higher 段的工作,再去等待 new_lower 段的工作的 future。这个实现中线程池中工作线程数量是固定的,如果所有工作线程都需要等待 future,那么就没有剩余的工作线程去真正推进任务了!有限线程数 + 线程等待尚未排队的任务 = 死锁。

解决方案是让线程在等待期间不要休眠,而是主动处理其他任务: