为什么以下程序在不使用互斥锁时不混合输出



我已经多次运行该程序。 即使我不使用互斥锁,我也没有看到输出不正确。我的目标是证明互斥锁的必要性。我的想法是,具有不同"num"值的不同线程将被混合。

是因为对象不同吗?

using VecI = std::vector<int>;
class UseMutexInClassMethod {
mutex m;
public:
VecI compute(int num, VecI veci)
{
VecI v;
num = 2 * num -1;
for (auto &x:veci) {
v.emplace_back(pow(x,num));
std::this_thread::sleep_for(std::chrono::seconds(1));
}
return v;
}
};  
void TestUseMutexInClassMethodUsingAsync()
{
const int nthreads = 5;
UseMutexInClassMethod useMutexInClassMethod;
VecI vec{ 1,2,3,4,5 };
std::vector<std::future<VecI>> futures(nthreads);
std::vector<VecI> outputs(nthreads);
for (decltype(futures)::size_type i = 0; i < nthreads; ++i) {
futures[i] = std::async(&UseMutexInClassMethod::compute,
&useMutexInClassMethod,
i,vec
);
}
for (decltype(futures)::size_type i = 0; i < nthreads; ++i) {
outputs[i] = futures[i].get();
for (auto& x : outputs[i])
cout << x << " ";
cout << endl;
}
}

如果你想要一个非常确定地失败的例子,你可以看看下面。它设置了一个名为accumulator的变量,通过引用所有期货来共享。这是您的示例中缺少的内容。您实际上并没有共享任何内存。确保您了解按引用传递和按值传递之间的区别。

#include <vector>
#include <memory>
#include <thread>
#include <future>
#include <iostream>
#include <cmath>
#include <mutex>

struct UseMutex{
int compute(std::mutex & m, int & num)
{
for(size_t j = 0;j<1000;j++)
{
///////////////////////
// CRITICAL SECTIION //
///////////////////////
// this code currently doesn't trigger the exception
// because of the lock on the mutex. If you comment
// out the single line below then the exception *may*
// get called.
std::scoped_lock lock{m};
num++;
std::this_thread::sleep_for(std::chrono::nanoseconds(1));
num++;
if(num%2!=0)
throw std::runtime_error("bad things happened");
}
return 0;
}
};  
template <typename T> struct F;
void TestUseMutexInClassMethodUsingAsync()
{

const int nthreads = 16;
int accumulator=0;
std::mutex m;
std::vector<UseMutex> vs{nthreads};
std::vector<std::future<int>> futures(nthreads);

for (auto i = 0; i < nthreads; ++i) {
futures[i]= std::async([&,i](){return vs[i].compute(m,accumulator);});
}
for(auto i = 0; i < nthreads; ++i){
futures[i].get(); 
}

}
int main(){
TestUseMutexInClassMethodUsingAsync();
}

您可以评论/取消注释该行

std::scoped_lock lock{m};

这保护了共享变量num的增量。这个小程序的规则是,在

if(num%2!=0)
throw std::runtime_error("bad things happened");

num应该是 2 的倍数。但是由于多个线程在没有锁的情况下访问此变量,因此您无法保证这一点。但是,如果在双倍增量和测试周围添加锁,则可以确保在增量和测试期间没有其他线程访问此内存。

失败 https://godbolt.org/z/sojcs1WK9

通过 https://godbolt.org/z/sGdx3x3q3

当然,失败的不能保证失败,但我已经设置了它,以便它很有可能失败。

笔记

[&,i](){return vs[i].compute(m,accumulator);};

是一个 lambda 或内联函数。表示法[&,i]意味着它通过引用捕获除按值捕获i之外的所有内容。这很重要,因为i每个循环迭代都会发生变化,我们希望每个未来都获得唯一的i

是因为对象不同吗?

是的。

您的代码实际上是完全线程安全的,无需在此处mutex。除了通过std::asyncvecTestUseMutexInClassMethodUsingAsync复制到compute(并且复制是线程安全的)以及将计算结果从compute的返回值移动到futures[i].get()的返回值之外,您永远不会在线程之间共享任何状态。.get()也是线程安全的:它会阻塞,直到compute()方法终止,然后返回其计算结果。

实际上很高兴看到,即使是故意尝试获得竞争条件也失败了:)

您可能需要完全重做您的示例,以演示对共享对象的同时*访问如何破坏事物。摆脱std::asyncstd::future,使用简单的std::thread按引用捕获,删除sleep_for(因此两个线程都执行大量操作而不是每秒执行一次),显着增加操作数量,您将获得可见的竞赛。不过,它可能看起来像是崩溃。

*- 是的,我知道严格来说,多线程系统中不存在"挂钟模拟访问"。但是,它有助于大致了解在何处查找可见的竞争条件以进行演示。

注释指出,仅仅不保护关键部分并不能保证风险行为确实发生。
这也适用于多次运行,因为虽然不允许你测试几次,然后依赖于重复观察到的行为,但优化机制可能会导致足够多的重复观察,以至于感知到具有可重现性。

如果您打算证明同步的必要性,则需要使用同步来使事情保持几乎保证的可观察到的缺乏保护的不当行为。

请允许我只概述一个序列,对调度机制有一些假设(这是基于我在专业使用的嵌入式环境中遇到的一个相当简单的、单核的、基于优先级的调度环境),只是为了通过一个简化的例子来提供见解:

  • 启动优先级较低的上下文。
  • 可选择在进入关键部分之前设置适当的保护
  • 启动关键部分,例如通过输出待连续输出的前半部分
  • 异步触发更高优先级的上下文,该上下文正在执行可能违反关键部分的操作,例如输出不应位于关键部分的两部分输出中间的内容
  • (在受保护的情况下,不会执行其他上下文,尽管优先级更高)
  • (在不受保护的情况下,由于优先级更高,现在将执行其他上下文)
  • 结束关键部分,例如通过输出待连续输出的后半部分
  • (可选)在离开关键部分后移除保护
  • (在受保护的情况下,现在执行另一个上下文,现在
  • 允许它)
  • (在不受保护的情况下,另一个上下文已经执行)

注意:
我使用术语"关键部分"的含义是一段代码,该代码很容易被另一段代码或同一代码的另一次执行中断/抢占/取消调度。特别是对我来说,在没有应用保护的情况下可以存在关键部分,尽管这不是一件好事。我明确说明这一点,因为我知道该术语的含义是"应用保护/同步中的代码段"。我不同意,但我接受该术语的使用方式不同,并且在发生潜在冲突时需要澄清。

最新更新