【发布时间】:2022-01-19 20:59:50
【问题描述】:
这是给我的一个测试任务,我显然失败了:
1.使用两个线程递增一个整数。线程 A 在偶数时递增,线程 B 在奇数时递增(对于整数问题,我们可以将其指定为命令行中提供的最大数字)
1a。添加更多线程有哪些困难?请说明代码的困难。
1b。额外的功劳——为上述问题设计一个改进的解决方案,可以通过多个线程进行扩展
第一次尝试后的反馈是“没有解决原子修改和错误共享”。我试图解决它们,但第二次尝试没有反馈。我想用这个测试来学习,所以我想我会问最顶尖的专家——你。
以下是第一次尝试的头部:
#include <iostream>
#include <mutex>
#include <atomic>
class CIntToInc
{
private:
int m_nVal; //std::atomic<int> m_nVal;
int m_nMaxVal;
public:
CIntToInc(int p_nVal, int p_nMaxVal) : m_nVal(p_nVal), m_nMaxVal(p_nMaxVal) { }
const int GetVal() const { return m_nVal; }
const int GetMaxVal() const { return m_nMaxVal; }
void operator ++() { ++m_nVal; }
};
struct COper
{
enum class eOper { None = 0, Mutex = 1, NoMutex = 2 };
eOper m_Oper;
public:
friend std::istream& operator>> (std::istream &in, COper &Oper);
bool operator == (const eOper &p_eOper) { return(m_Oper == p_eOper); }
};
以下是第一次尝试的来源。它包括我对解决方案为何有效的想法。我在MSVS2012中编译了代码。
// Notes:
// 1a.
// Since an integer cannot be an odd number and an even number at the same time, thread separation happens naturally when each thread checks the value.
// This way no additional synchronization is necessary and both threads can run at will, provided that it's all they are doing.
// It's probably not even necessary to declare the target value atomic because it changes (and thus lets the other thread increment itself) only at the last moment.
// I would still opt for making it atomic.
// Adding more threads to this setup immediately creates a problem with threads of equal condition (even or odd) stepping on each other.
// 1b.
// By using a mutex threads can cleanly separate. Many threads with the same condition can run concurrently.
// Note: there is no guarantee that each individual thread from a pool of equally conditioned threads will get to increment the number.
// For this method reading has to be inside the mutext lock to prevent a situation where a thread may see the value as incrementable, yet when it gets to it, the value has already
// been changed by another thread and no longer qualifies.
// cout message output is separated in this approach.
//
// The speed of the "raw" approach is 10 times faster than that of the mutex approach on an equal number of threads (two) with the mutex time increasing further as you add threads.
// Use 10000000 for the max to feel the difference, watch the CPU graph
//
// If the operation is complex and time consuming, the approach needs to be different still. The "increment" functionality can be wrapped up in a pimpl class, a copy can be made
// and "incremented". When ready, the thread will check for whether the value has changed while the operation was being performed on the copy and, if not, a fast swap under the mutex
// could be attempted. This approach is resource-intensive, but it mininuzes lock time.
//
// The approach above will work if the operation does not involve resources that cannot be easily copied (like a file to the end of which we are writing)
// When such resources are present, the algorithm probably has to implement a thread safe queue.
// END
#include "test.h"
#include <thread>
int main_test();
int main(int argc, char* argv[])
{
main_test();
return(0);
}
void IncrementInt2(CIntToInc &p_rIi, bool p_bIfEven, const char *p_ThreadName, std::mutex *p_pMu)
// the version that uses a mutex
// enable cout output to see thread messages
{
int nVal(0);
while(true) {
p_pMu->lock();
bool DoWork = (nVal = p_rIi.GetVal() < p_rIi.GetMaxVal());
if(DoWork) {
//std::cout << "Thread " << p_ThreadName << ": nVal=" << nVal << std::endl;
if((!(nVal % 2) && p_bIfEven) || (nVal % 2 && !p_bIfEven)) {
//std::cout << "incrementing" << std::endl;
++p_rIi; } }
p_pMu->unlock();
if(!DoWork) break;
//if(p_bIfEven) // uncomment to force threads to execute differently
// std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
}
void IncrementInt3(CIntToInc &p_rIi, bool p_bIfEven, const char *p_ThreadName)
// the version that does not use a mutex
// enable cout output to see thread messages. Message text output is not synchronized
{
int nVal(0);
while((nVal = p_rIi.GetVal()) < p_rIi.GetMaxVal()) {
//std::cout << "Thread " << p_ThreadName << ": nVal=" << nVal << std::endl;
if((!(nVal % 2) && p_bIfEven) || (nVal % 2 && !p_bIfEven)) {
//std::cout << "Thread " << p_ThreadName << " incrementing" << std::endl;
++p_rIi; }
}
}
std::istream& operator>> (std::istream &in, COper &Oper)
// to read operation types from cin
{
int nVal;
std::cin >> nVal;
switch(nVal) {
case 1: Oper.m_Oper = COper::eOper::Mutex; break;
case 2: Oper.m_Oper = COper::eOper::NoMutex; break;
default: Oper.m_Oper = COper::eOper::None; }
return in;
}
int main_test()
{
int MaxValue, FinalValue;
COper Oper;
std::cout << "Please enter the number to increment to: ";
std::cin >> MaxValue;
std::cout << "Please enter the method (1 - mutex, 2 - no mutex): ";
std::cin >> Oper;
auto StartTime(std::chrono::high_resolution_clock::now());
if(Oper == COper::eOper::Mutex) {
std::mutex Mu;
CIntToInc ii(0, MaxValue);
std::thread teven(IncrementInt2, std::ref(ii), true, "Even", &Mu);
std::thread todd(IncrementInt2, std::ref(ii), false, "Odd", &Mu);
// add more threads at will, should be safe
//std::thread teven2(IncrementInt2, std::ref(ii), true, "Even2", &Mu);
//std::thread teven3(IncrementInt2, std::ref(ii), true, "Even3", &Mu);
teven.join();
todd.join();
//teven2.join();
//teven3.join();
FinalValue = ii.GetVal();
}
else if(Oper == COper::eOper::NoMutex) {
CIntToInc ii(0, MaxValue);
std::thread teven(IncrementInt3, std::ref(ii), true, "Even");
std::thread todd(IncrementInt3, std::ref(ii), false, "Odd");
teven.join();
todd.join();
FinalValue = ii.GetVal(); }
std::chrono::duration<double>elapsed_seconds = (std::chrono::high_resolution_clock::now() - StartTime);
std::cout << "main_mutex completed with nVal=" << FinalValue << " in " << elapsed_seconds.count() << " seconds" << std::endl;
return(0);
}
对于第二次尝试,我对标题进行了以下更改:
制作 m_nVal std::atomic
使用原子方法来增加和检索 m_nVal
用填充符将 m_nVal 与只读 m_nMaxVal 分开
源文件没有改变。新标题如下。
#include <iostream>
#include <mutex>
#include <atomic>
class CIntToInc
{
private:
int m_nMaxVal;
char m_Filler[64 - sizeof(int)]; // false sharing prevention, assuming a 64 byte cache line
std::atomic<int> m_nVal;
public:
CIntToInc(int p_nVal, int p_nMaxVal) : m_nVal(p_nVal), m_nMaxVal(p_nMaxVal) { }
const int GetVal() const {
//return m_nVal;
return m_nVal.load(); // std::memory_order_relaxed);
}
const int GetMaxVal() const { return m_nMaxVal; }
void operator ++() {
//++m_nVal;
m_nVal.fetch_add(1); //, std::memory_order_relaxed); // relaxed is enough since we check this very variable
}
};
struct COper
{
enum class eOper { None = 0, Mutex = 1, NoMutex = 2 };
eOper m_Oper;
public:
friend std::istream& operator>> (std::istream &in, COper &Oper);
bool operator == (const eOper &p_eOper) { return(m_Oper == p_eOper); }
};
我不知道这种方法是否从根本上是错误的,或者是否存在一个或多个较小的错误。
【问题讨论】:
-
您的代码看起来设计过度。你不增加
int,你使用了一些类——我认为这违反了你的要求。 -
char m_Filler[64 - sizeof(int)];为什么不只是alignas(64) std::atomic<int> m_nVal;?或者使用更便携的 C++17:alignas(std::hardware_destructive_interference_size) std::atomic<int> m_nVal;。另外为什么将第一个参数作为引用传递,而是指向互斥体的指针?顺便说一句:在等待值变为奇数甚至偶数时保持线程运行可能不是最好的主意......也许使用 2std::condition_variables 可能是个好主意? (一个用于“奇数可用”,一个用于“可用偶数”,每次递增后,在右侧使用notify_one...) -
“它会改变......仅在最后一刻”并不意味着您可以跳过同步。如果不止一个线程访问一个对象,并且这些线程中至少有一个线程写入该对象并且您不同步访问,则行为未定义。
标签: c++ multithreading concurrency stl