需要一些建议来使代码多线程

时间:2010-02-18 22:56:37

标签: c++ multithreading

我收到的代码不适用于多线程应用,现在我必须修改代码才能支持多线程。

我有一个基于以下指令的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

5 个答案:

答案 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()

由于代码最初并非设计为多线程,因此很可能会遇到这样的问题,其中一个方法调用使用来自早期调用的信息,但是从来没有关注对象的状态在这些电话之间切换。

现在,开始想象如果单例中的对象之间存在复杂的状态依赖关系,并且多个线程可以修改这些内部对象的状态,会发生什么。大量的线程都会变得非常非常混乱,调试变得非常困难。

因此,当您尝试使单例线程安全时,您需要查看几个对象交互层。一些好问题要问:

  1. 单例中的任何方法是否显示可能在方法调用之间发生变化的内部状态(如我在上一个示例中提到的那样)?
  2. 是否向单身人士的客户透露了任何内部对象?
  3. 如果是这样,那些内部对象上的任何方法都会显示可能在方法调用之间发生变化的内部状态吗?
  4. 如果显示内部对象,它们是否共享任何资源或状态依赖项?
  5. 如果您只是从内部对象读取状态(第一个示例),则可能不需要任何锁定。您可能需要提供简单的锁定以防止并发读/写访问(第二个示例)。您可能需要重新设计类或为客户端提供锁定对象状态的能力(第三个示例)。或者您可能需要实现更复杂的锁定,其中内部对象跨线程共享状态信息(例如,类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()时指针是有效的,您也无法保证在执行指针操作期间指针值将存在。这仅仅是因为您无法控制对象的生存期。 这就是为什么您应尽可能使用对象变量并避免使用指针的原因(如果可以)