假设我有一些代码给我一个竞争条件,比如说
class foo
{
some_data data;
public:
void bar(some_type arg)
{
// may change data
}
// ...
};
在没有保护的情况下使用foo::bar()
不会线程安全,因为另一个线程可能同时调用bar()
。因此,可以说,更好的选择是
class foo
{
some_data data;
std::mutex my_mutex;
void unsafe_bar(some_type arg);
public
void bar(some_type arg)
{
std::lock_guard<std::mutex> lock(my_mutex);
unsafe_bar(arg);
}
};
但是,假设有许多类似的类,其成员函数具有同样的问题
class foo1 { public: void bar(some_type arg); /*...*/ };
class foo2 { public: void bar(some_type arg); /*...*/ };
class foo3 { public: void bar(some_type arg); /*...*/ };
class foo4 { public: void bar(some_type arg); /*...*/ };
class foo5 { public: void bar(some_type arg); /*...*/ };
每个在一个代码块内调用:
void work(some_type arg, foo1&f1, foo2&f2, foo3&f3, foo4&f4, foo5&f5)
{
f1.bar(arg);
f2.bar(arg);
f3.bar(arg);
f4.bar(arg);
f5.bar(arg);
}
现在,这些调用中的每一个都锁定并解锁另一个互斥锁,这似乎效率低下。最好只使用一个互斥锁。
问有推荐/最好的方法吗?
我在考虑以下设计:
class foo
{
std::mutex my_mutex;
void unsafe_bar(some_type arg);
public:
template<typename Mutex>
void bar(some_type arg, Mutex&m)
{
std::lock_guard<Mutex> lock(m);
unsafe_bar(arg);
}
void bar(some_type arg)
{
bar(arg,my_mutex);
}
};
现在,用户可以依赖foo::my_mutex
(foo::bar
的第二版),或者
提供互斥锁,可以是std::recursive_mutex
或null_mutex
(满足互斥锁概念,但不执行任何操作)。 问这是一个明智/有用的想法吗?
编辑我注意到我的种族实际上并不依赖于任何特定的迭代器(以及它可能的失效等)。我删除了对迭代器的任何引用。
答案 0 :(得分:2)
tl; dr 最终,您在对象上设置线程同步的方式取决于您,并且从单独的对象调用互斥锁上的lock
可能效率不高,您可以考虑到你的整体设计并弄清楚&#39;哪里有&#39;您希望您的互斥锁/信号量和其他同步对象能够最好地处理与使代码“线程安全”相关的开销。如果您正在编写将在外部使用的代码(如库代码),那么您需要确保记录某个特定功能实际上是线程安全的&#39;否则我(作为用户)会自己保护我自己的代码。
void work(some_iterator const&i, foo1&f1, foo2&f2, foo3&f3, foo4&f4, foo5&f5)
{
f1.bar(i);
f2.bar(i);
f3.bar(i);
f4.bar(i);
f5.bar(i);
}
问有没有推荐/最好的方法呢?
是的,从foo
对象中取出互斥锁并将其放在其他地方;
// defined somewhere
std::mutex _mtx;
void work(some_iterator const&i, foo1&f1, foo2&f2, foo3&f3, foo4&f4, foo5&f5)
{
_mtx.lock();
f1.bar(i);
f2.bar(i);
f3.bar(i);
f4.bar(i);
f5.bar(i);
_mtx.unlock();
}
问template<typename Mutex> void bar(some_iterator const&i, Mutex&m)
是一个明智/有用的想法吗?
不,正如上面的例子所示,呼叫全球&#39;更简单(也更清洁)。互斥/ semphore对象;如果我想使用你的模板化功能做同样的事情,我会有额外的打字,额外的打电话,很可能不是我想要的效果,例如:
// defined somewhere
std::mutex _mtx;
void work(some_iterator const&i, foo1&f1, foo2&f2, foo3&f3, foo4&f4, foo5&f5)
{
f1<std::mutex>.bar(i, _mtx);
f2<std::mutex>.bar(i, _mtx);
f3<std::mutex>.bar(i, _mtx);
f4<std::mutex>.bar(i, _mtx);
f5<std::mutex>.bar(i, _mtx);
}
我必须特别了解work
函数中的互斥锁,因为我的some_iterator
可能会在工作函数之外被修改(即使每个foo
void work(some_iterator const&i, foo1&f1, foo2&f2, foo3&f3, foo4&f4, foo5&f5)
{
// i could be modified here
f1.bar(i); // here if mutex handle != others
// here
f2.bar(i); // here if mutex handle != others
// here
f3.bar(i); // here if mutex handle != others
// here
f4.bar(i); // here if mutex handle != others
// here
f5.bar(i); // here if mutex handle != others
// here
}
1}}对象有一个互斥锁),例如:
work
当然,这可能是你想要的?如果work
函数本质上不是原子的,那么我(作为函数的用户)可能会在std::mutex _mtx2;
void some_thread_fn1()
{
_mtx2.lock();
// some other code
work(i, f1, f2, f3, f4, f5);
_mtx2.unlock();
}
void some_thread_fn2()
{
_mtx2.lock();
// some other code
work(i, f1, f2, f3, f4, f5);
_mtx2.unlock();
}
函数周围放置一个互斥锁,例如(假设上面的代码): / p>
work
谁曾使用foo
函数需要(有些)意识到在其中调用互斥锁,否则它们可能会加倍保护(击败你的原始目的)。
另请注意,您的所有代码都不会实际锁定相同的互斥锁(假设您的std::mutex
类中也包含class foo1
{
some_data data;
std::mutex my_mutex;
void unsafe_bar(some_iterator const&i);
public:
void bar(some_iterator const&i)
{
std::lock_guard<std::mutex> lock(my_mutex);
unsafe_bar(i);
}
};
class foo2
{
some_data data;
std::mutex my_mutex;
void unsafe_bar(some_iterator const&i);
public:
void bar(some_iterator const&i)
{
std::lock_guard<std::mutex> lock(my_mutex);
unsafe_bar(i);
}
};
foo1 f1;
foo2 f2;
some_iterator x;
void thread1()
{
f1.bar(x);
}
void thread2()
{
f2.bar(x);
}
个),例如:
{{1}}
上面的代码会导致竞争条件,因为您要锁定2个单独的互斥锁(因为您想要锁定同一个互斥锁,所以会出现逻辑错误)。
这个问题似乎更多地是关于多线程设计和最佳实践,正如另一个答案所指出的那样。
希望可以提供帮助。
答案 1 :(得分:1)
有推荐/最好的方法吗?
我认为你的方法很常见,是避免竞争条件的好方法。
这是一个明智/有用的想法吗?
我认为第二种方法不仅值得怀疑。您将把同步的负担放在您班级的客户/用户身上。至少每当使用模板化函数时。但是如果你这样做,你可以声明你的类不是线程安全的,并且调用者必须在所有情况下同步它。哪个至少是一致的。
通常,您应该同步数据,同时访问的数据成员。您的互斥锁的数量更多地是关于粒度。例如,总是锁定一个完整的块或仅锁定读/写操作。但这取决于用例。
修改强>:
因为你现在发布了一些代码
这仍然取决于你想如何使用你的课程
f1到f5是否仅用于
void work(some_iterator const&i, foo1&f1, foo2&f2, foo3&f3, foo4&f4, foo5&f5)
方法。然后根本不要使用互斥锁。类的用户很容易进行同步。 work
中的互斥锁。
这些类是否与工作方法分开使用?
同样,答案将是保护与其自己的互斥锁一起使用的每个类成员,因为对于客户端来说,它将更加困难。