我收到的代码不适用于多线程应用,现在我必须修改代码才能支持多线程。
我有一个基于以下指令的Singleton类(MyCenterSigltonClass
):
http://en.wikipedia.org/wiki/Singleton_pattern
我把它做成了线程安全的
现在我在类中看到包含10-12个成员,其中一些使用getter / setter方法。 一些成员被声明为static,并且是类指针,如:
static Class_A* f_static_member_a;
static Class_B* f_static_member_b;
对于这些成员,我在类(mutex_a
)中定义了一个互斥锁(如Class_A
),我没有在我的MyCenterSigltonClass
中直接添加互斥锁,原因是它们与我的MyCenterSigltonClass
是一对一的关联,我想我可以选择在班级(MyCenterSigltonClass
)或Class_A
中为f_static_member_a
定义互斥。
1)我是对的吗?
此外,我的Singleton类(MyCenterSigltonClass
)包含一些其他成员,如
Class_C f_classC;
对于这些成员变量,我应该在MyCenterSigltonClass
中为每个变量定义一个互斥锁,以使它们具有线程安全性吗?什么是处理这些案件的好方法?
感谢任何建议。
-Nima
答案 0 :(得分:1)
成员是否是静态的并不重要。如何保护成员变量实际上取决于如何从公共方法访问它们。
您应该将互斥锁视为锁,以保护某些资源免受并发读/写访问。您不必考虑必须保护内部类对象,而是考虑其中的资源。您还需要考虑将要使用的锁的范围,特别是如果代码最初设计为多线程的话。让我举几个简单的例子。
class A { private: int mValuesCount; int* mValues; public: A(int count, int* values) { mValuesCount = count; mValues = (count > 0) ? new int[count] : NULL; if (mValues) { memcpy(mValues, values, count * sizeof(int)); } } int getValues(int count, int* values) const { if (mValues && values) { memcpy(values, mValues, (count < mValuesCount) ? count : mValuesCount); } return mValuesCount; } }; class B { private: A* mA; public: B() { int values[5] = { 1, 2, 3, 4, 5 }; mA = new A(5, values); } const A* getA() const { return mA; } };
在此代码中,不需要保护mA,因为跨多个线程不存在冲突访问的可能性。没有线程可以修改mA的状态,因此所有并发访问只能从mA读取。但是,如果我们修改A类:
class A { private: int mValuesCount; int* mValues; public: A(int count, int* values) { mValuesCount = 0; mValues = NULL; setValues(count, values); } int getValues(int count, int* values) const { if (mValues && values) { memcpy(values, mValues, (count < mValuesCount) ? count : mValuesCount); } return mValuesCount; } void setValues(int count, int* values) { delete [] mValues; mValuesCount = count; mValues = (count > 0) ? new int[count] : NULL; if (mValues) { memcpy(mValues, values, count * sizeof(int)); } } };
我们现在可以有多个线程调用B :: getA(),一个线程可以从mA读取,而另一个线程写入mA。考虑以下线程交互:
Thread A: a->getValues(maxCount, values); Thread B: a->setValues(newCount, newValues);
当线程A处于复制过程中时,线程B可能会删除mValues。在这种情况下,您需要A类中的互斥锁来保护对mValues和mValuesCount的访问:
int getValues(int count, int* values) const { // TODO: Lock mutex. if (mValues && values) { memcpy(values, mValues, (count < mValuesCount) ? count : mValuesCount); } int returnCount = mValuesCount; // TODO: Unlock mutex. return returnCount; } void setValues(int count, int* values) { // TODO: Lock mutex. delete [] mValues; mValuesCount = count; mValues = (count > 0) ? new int[count] : NULL; if (mValues) { memcpy(mValues, values, count * sizeof(int)); } // TODO: Unlock mutex. }
这将阻止对mValues和mValuesCount的并发读/写。根据环境中可用的锁定机制,您可以在getValues()中使用只读锁定机制,以防止多个线程阻止并发读取访问。
但是,如果A类更复杂,您还需要了解需要实现的锁定范围:
class A { private: int mValuesCount; int* mValues; public: A(int count, int* values) { mValuesCount = 0; mValues = NULL; setValues(count, values); } int getValueCount() const { return mValuesCount; } int getValues(int count, int* values) const { if (mValues && values) { memcpy(values, mValues, (count < mValuesCount) ? count : mValuesCount); } return mValuesCount; } void setValues(int count, int* values) { delete [] mValues; mValuesCount = count; mValues = (count > 0) ? new int[count] : NULL; if (mValues) { memcpy(mValues, values, count * sizeof(int)); } } };
在这种情况下,您可以进行以下线程交互:
Thread A: int maxCount = a->getValueCount(); Thread A: // allocate memory for "maxCount" int values Thread B: a->setValues(newCount, newValues); Thread A: a->getValues(maxCount, values);
线程A已被编写,好像对getValueCount()和getValues()的调用将是一个不间断的操作,但是线程B可能在线程A的操作中间更改了计数。根据新计数是大于还是小于原始计数,可能需要一段时间才能发现此问题。在这种情况下,需要重新设计A类,或者需要提供某种事务支持,因此使用A类的线程可以阻止/取消阻塞其他线程:
Thread A: a->lockValues(); Thread A: int maxCount = a->getValueCount(); Thread A: // allocate memory for "maxCount" int values Thread B: a->setValues(newCount, newValues); // Blocks until Thread A calls unlockValues() Thread A: a->getValues(maxCount, values); Thread A: a->unlockValues(); Thread B: // Completes call to setValues()
由于代码最初并非设计为多线程,因此很可能会遇到这样的问题,其中一个方法调用使用来自早期调用的信息,但是从来没有关注对象的状态在这些电话之间切换。
现在,开始想象如果单例中的对象之间存在复杂的状态依赖关系,并且多个线程可以修改这些内部对象的状态,会发生什么。大量的线程都会变得非常非常混乱,调试变得非常困难。
因此,当您尝试使单例线程安全时,您需要查看几个对象交互层。一些好问题要问:
如果您只是从内部对象读取状态(第一个示例),则可能不需要任何锁定。您可能需要提供简单的锁定以防止并发读/写访问(第二个示例)。您可能需要重新设计类或为客户端提供锁定对象状态的能力(第三个示例)。或者您可能需要实现更复杂的锁定,其中内部对象跨线程共享状态信息(例如,类Foo中的资源上的锁需要锁定类Bar中的资源,但是在类Bar中锁定该资源并不一定需要锁定Foo类中的资源。
根据所有对象的交互方式,实现线程安全代码可能会变成一项复杂的任务。它可能比我在这里给出的例子复杂得多。请确保您清楚地了解您的课程的使用方式以及它们之间的互动方式(并准备花一些时间追踪难以重现的错误)。
答案 1 :(得分:0)
如果这是您第一次进行线程处理,请考虑不从后台线程访问单例。你可以做对,但你可能不会在第一次就做对。
要意识到如果你的单例暴露了指向其他对象的指针,那么这些对象也应该是线程安全的。
答案 2 :(得分:0)
您不必为每个成员定义互斥锁。例如,您可以使用单个互斥锁来同步每个成员的访问权限,例如:
class foo
{
public:
...
void some_op()
{
// acquire "lock_" and release using RAII ...
Lock(lock_);
a++;
}
void set_b(bar * b)
{
// acquire "lock_" and release using RAII ...
Lock(lock_);
b_ = b;
}
private:
int a_;
bar * b_;
mutex lock_;
}
当然,“一锁”解决方案可能不适合您的情况。这取决于你自己决定。无论如何,简单地引入锁并不会使代码成为线程安全的。你必须以正确的方式在正确的位置使用它们以避免竞争条件,死锁等。你可以运行很多并发问题。
此外,您并不总是需要互斥锁或其他线程机制(如TSS)来使代码具有线程安全性。例如,以下函数“func
”是线程安全的:
class Foo;
void func (Foo & f)
{
f.some_op(); // Foo::some_op() of course needs to be thread-safe.
}
// Thread 1
Foo a;
func(&a);
// Thread 2
Foo b;
func(&b);
虽然上面的func
函数是线程安全的,但它调用的操作可能不是线程安全的。关键是你并不总是需要用互斥体和其他线程机制来编写代码,以使代码线程安全。有时重组代码就足够了。
关于多线程编程的文献很多。这样做绝对不容易,因此请花时间了解细微差别,并利用Boost.Thread等现有框架来缓解低级多线程API中存在的一些inherent and accidental complexities。
答案 3 :(得分:0)
我真的建议使用Interlocked ....在使用需要多线程感知的代码时增加,减少和CompareAndSwap值的方法。我没有单手C ++经验,但快速搜索http://www.bing.com/search?q=c%2B%2B+interlocked会发现许多确认建议。如果你需要性能,这些可能比锁定更快。
答案 4 :(得分:0)
如@Void所述,仅互斥锁并不总是解决并发问题的方法:
无论如何,仅仅引入锁并不能使代码生效 线程安全的。您必须以正确的方式在正确的位置使用它们 为了避免比赛条件,僵局等。 您可能会遇到的并发问题。
我想添加另一个示例:
class MyClass
{
mutex m_mutex;
AnotherClass m_anotherClass;
void setObject(AnotherClass& anotherClass)
{
m_mutex.lock();
m_anotherClass = anotherClass;
m_mutex.unlock();
}
AnotherClass getObject()
{
AnotherClass anotherClass;
m_mutex.lock();
anotherClass = m_anotherClass;
m_mutex.unlock();
return anotherClass;
}
}
在这种情况下, getObject()方法始终是安全的,因为该方法受互斥锁保护,并且您拥有该对象的副本,该对象的副本可以返回给调用方,该调用方可以是其他类和线程。这意味着您正在处理一个可能是旧的副本(与此同时,另一个线程可能通过调用 setObject()来更改m_anotherClass)。现在,如果将m_anotherClass变成指针而不是对象,该怎么办?变量?
class MyClass
{
mutex m_mutex;
AnotherClass *m_anotherClass;
void setObject(AnotherClass *anotherClass)
{
m_mutex.lock();
m_anotherClass = anotherClass;
m_mutex.unlock();
}
AnotherClass * getObject()
{
AnotherClass *anotherClass;
m_mutex.lock();
anotherClass = m_anotherClass;
m_mutex.unlock();
return anotherClass;
}
}
这是一个互斥体不足以解决所有问题的示例。 使用指针时,您只能拥有一个指针的副本,但是在调用者和方法中,指向的对象是相同的。因此,即使在调用 getObject()时指针是有效的,您也无法保证在执行指针操作期间指针值将存在。这仅仅是因为您无法控制对象的生存期。 这就是为什么您应尽可能使用对象变量并避免使用指针的原因(如果可以)。