C++ 中的锁定和等待同步






4.70/5 (25投票s)
在 C++ 中应用锁与等待同步。
引言
Boost.Thread 库提供了系统线程原语的可移植封装,用于创建并发环境并对其进行同步。Boost.Thread 库的功能足以创建功能齐全的多线程程序,并在此中使用基于锁与等待的同步。但 Boost.Thread 库只是系统线程例程的第一层。从功能角度来看,Boost.Thread 的接口与 PTHREAD 或 Win32 Threads 等系统线程接口是相同的。这意味着 Boost.Thread 允许非安全操作,如对资源的非同步并发访问,并迫使用户进行例行且危险的工作,而这些工作完全可以使用 C++ 语言来完成。
Lwsync 库(锁与等待同步)提供了两种模式的实现,可以替代原始的系统同步机制,允许用户在并发环境中创建锁与等待同步。这两种模式一方面可以映射到任何同步机制(默认情况下,它们映射到 Boost.Thread,因此可以在任何有 Boost 库的地方使用);另一方面,它们完全依赖于 C++ 语言。因此,用户可以使用 Lwsync 库作为系统同步机制和最终并发代码之间的一层,使锁与等待同步更加简单和安全。
临界资源模式允许创建资源对象,并保证对其的所有访问都将进行同步。所有同步将在此类访问之前和之后自动完成。监视器模式是一种临界资源模式,但它不仅同步对资源对象的所有访问,还允许线程等待直到资源达到某个定义的状态。在大多数情况下,仅凭这两种模式就足以实现基于锁与等待的同步。
请注意,Lwsync 库仅允许同步线程,而不允许启动线程、控制线程属性等。Boost.Thread 库可以替代此功能。
Lwsync 库
临界资源模式
临界资源是一个模板,它将资源类型作为参数,并仅在所有同步完成后才允许访问资源对象。临界资源为用户提供了一种称为“访问器”的智能指针,它在其构造函数和析构函数中调用所有同步操作,因此用户只能在所有同步完成后通过访问器访问资源对象。访问资源对象的唯一方法是使用访问器。临界资源可以与自动访问器对象和临时访问器对象很好地配合使用。因此,资源访问可以通过代码范围或表达式进行同步。如果临界资源与允许递归获取的系统同步机制(如默认使用的 boost::recursive_mutex
)一起使用,则资源访问器甚至可以从创建它的例程的作用域内提升。这意味着访问器可以按值返回,调用者可以继续使用访问对象,而不会丢失对它的访问。
此外,用户还可以使用临界资源来同步尚未同步的现有对象,方法是使用临界资源模板的引用特化。
示例 1
typedef lwsync::critical_resource<int> int_resource_t;
int_resource_t int_res(10);
// thread 1:
// Use temporary accessor object to access int resource.
*int_res.access() = 20;
// thread 2:
{
// Use automatic accessor object to const access int resource.
int_resource_t::const_accessor int_res_access = int_res.const_access();
if ( *int_res_access == 20)
std::cout << "Thread 1 has already changed int_res value.";
else
std::cout << "Thread 1 doesn't change int_res value yet.";
}
示例 2
lwsync::critical_resource<std::ostream&> sync_cout(std::cout);
// thread N:
// It is thread-safe to use many operators << in one expression to
// out some data, because critical resource can use temporary objects
// to synchronize an expression.
*sync_cout.access() << "Hello" << " from" << " thread " << N;
示例 3
typedef lwsync::critical_resource<std::vector<int> > sync_vector_t;
sync_vector_t vec;
// some thread:
{
// Critical resource can be naturally used with STL containers.
sync_vector_t::const_accessor vec_access = vec.const_access();
for(std::vector<int>::const_iterator where = vec_access->begin();
where != vec_access->end();
++where
)
std::cout << *where << std::endl;
}
sync_vector_t::accessor some_vector_action()
{
sync_vector_t::accessor vec_access = vec.access();
vec_access->puch_back(10);
return vec_access;
// Access is escalated from within a some_vector_action() scope
// So that one can make some other action with vector before it becomes
// unlocked.
}
{
sync_vector_t::accessor vec_access = some_vector_action()
vec_access->puch_back(20);
// Elements 10 and 20 will be placed in vector sequentially.
// Any other action with vector cannot be processed between those two
// push_back's.
}
请注意,存在使用动态访问器对象(使用 operator new)或类数据成员来手动控制某些资源的访问时间的能力。但在大多数情况下,存储访问器不是一个好主意,因为在所有访问器的生命周期内,资源都将保持阻塞状态,并且除了销毁访问器对象外,没有其他方法可以解除阻塞。
在一个表达式或作用域中访问多个临界资源是可能的,但在大多数情况下,这样做很可能是不安全的死锁。可以使用表达式中的 copy()
方法来获取资源的副本,并在同一时间只阻塞一个资源。
示例 4
typedef lwsync::critical_resource<int> int_resource_t;
int_resource_t one_number(10);
int_resource_t other_number(20);
// thread 1:
// this operation is dead lock unsafe because more that one
// resource will be blocked in a one moment of time.
int result = *one_number.access() + *other_number.access()
// thread 2:
// It can be dead-locked with thread 1 because
// sequence of resources blocking is different
int result = *other_number.access() + *one_number.access()
// thread 3:
// This is dead-lock safe because only one resource
// will be blocked in a one moment of time.
int result = other_number.copy() + one_number.access()
在上面的示例中,线程 3 与线程 1 或线程 2 一起使用时不会导致死锁。但在此情况下,无法保证 one_number
和 other_number
在某个定义的时间点上具有 result
总和。在获得 other_number
的副本后,one_number
的值可能会发生变化。
可以在不显式调用 access
(或 const_access
)方法的情况下构造访问器对象。此功能可用作语法糖。
示例 5
typedef lwsync::critical_resource<int> int_resource_t;
int_resource_t critical_number;
{
// Method critical_number.access() is invoked implicitly.
int_resource_t::accessor number_access = critical_number;
}
{
// Method critical_number.const_access() is invoked implicitly.
int_resource_t::const_accessor number_access = critical_number;
}
监视器模式
监视器是一种临界资源,用户可以通过与临界资源相同的方式访问资源对象。但监视器不仅提供资源同步,还允许等待直到资源达到某个定义的状态。资源的状态由谓词定义,因此用户也可以使用 STL 谓词来监视某些资源。
监视器达到定义状态后,用户将获得资源访问器,并且重要的是,谓词应在访问器处理的同一锁内调用。因此,监视器模式保证用户在调用谓词的同一锁内获得对资源的访问。
示例 6
typedef lwsync::monitor<std::vector<int> > sync_vector_t;
bool is_not_empty(const std::vector<int>& vec)
{
return !vec.empty();
}
{
sync_vector_t::accessor vec_access = vec.wait_for(is_not_empty);
// Do something with vector. One can guarantee that
// this vector is not empty.
}
{ // Output vectors content when it becomes not empty.
sync_vector_t::const_accessor vec_access =
vec.const_wait_for(is_not_empty);
for(std::vector<int>::const_iterator where = vec_access->begin();
where != vec_access->end();
++where
)
std::cout << *where << std::endl;
}
此外,监视器可以等待资源,如果该资源是 bool
类型或可以转换为 bool
类型,而无需谓词。例如,可以等待指针不再是 null
的那一刻。
示例 7
typedef lwsync::monitor<my_data_t*> my_data_monitor_t;
my_data_monitor_t my_data_instance(0);
// thread 1:
{
// Waiting for monitor without any predicate.
my_data_monitor_t::accessor data = my_data_instance.wait();
data->invoke_something();
}
// thread 2:
*my_data_instance.access() = new my_data_t();
// Thread 1 can start working at this moment.
示例 8
typedef lwsync::monitor<bool> notifyer_t;
notifyer_t notify_object;
// thread 1:
for(;;)
{
notify_object::accessor notify_access = notify_object.wait();
// Perform some action here
*notify_access = false; // Work have done.
}
// thread 2:
// Request thread 1 to perform some action:
*notify_object.access() = true;
等待谓词通过值或 const 引用访问资源,即使在等待非 const 资源访问时也不会改变。禁止使用非 const 引用的谓词,以避免死锁,当两个线程都在等待某个监视器,并且它们不断地互相重新评估资源状态以检测可能的更改时。
监视器等待取消
所有等待某个监视器的线程都可以通过 cancel()
方法取消。在这种情况下,线程将收到 monitor::waiting_canceled
异常而不是访问器对象。监视器在此情况下也会进入已取消状态。如果某个线程尝试等待已取消的监视器,该线程也将收到 monitor::waiting_canceled
异常。监视器可以使用 renew()
方法恢复到正常状态,所有线程将能够再次等待它。
在某些情况下,有必要强制监视器重新评估所有谓词。如果某个谓词很复杂并且在不更改监视器状态的情况下更改其自身状态,则此操作是必要的。touch()
方法将强制所有线程重新评估其谓词。
同步策略
在大多数有 Boost.Thread
的情况下,可以不使用同步策略而使用 Lwsync。但它们对于某些特殊情况仍然是必需的。例如,如果需要记录对某个临界资源的访问日志。
临界资源和监视器模式使用同步策略作为模板参数,因此可以将其映射到任何同步机制。默认情况下,这两种同步策略都使用 boost::recursive_mutex
和 boost::condition
可移植同步机制。请注意,临界资源和监视器的同步策略是不同的。临界资源同步策略仅基于锁定机制,而监视器策略不仅需要锁定,还需要等待机制。用户可以使用监视器同步策略来同步临界资源(实际上,监视器就是这样做的),但反之则不然。
临界资源同步策略必须提供 locker_type
,该类型可用作临界资源的成员变量,以将资源对象与保护器关联起来。它还必须提供四个方法来执行同步。
template<typename Resource>
static void const_lock(locker_type*, const Resource*);
template<typename Resource>
static void lock(locker_type*, Resource*);
template<typename Resource>
static void const_unlock(locker_type*, const Resource*);
template<typename Resource>
static void unlock(locker_type*, Resource*);
所有这些方法都将被资源访问器调用。同步策略具有 const_lock
和 const_unlock
方法,以自然地与读写锁同步模式配合使用。监视器同步策略具有临界资源策略的所有功能,此外还必须有两个等待方法。
template<typename Resource, typename Predicate>
static void wait(locker_type*, const Resource&, Predicate);
template<typename Resource, typename Predicate>
static void const_wait(locker_type*, const Resource&, Predicate);
这使得实现等待资源成为可能,并在资源达到定义状态时获取读锁保护器,如果读写锁模式可用且等待机制不需要互斥执行。从等待机制或等待谓词中抛出异常是安全的。在这种情况下,用户将收到一个异常而不是对资源的访问。用户可以将等待机制实现为一个取消点,并在线程被取消时从 wait()
方法中抛出异常。或者,如果谓词认为对象进入了损坏状态,它也可以抛出异常。监视器模式将正确处理所有这些情况。
历史
- 2006 年 8 月 7 日 - 初始修订。
- 2006 年 10 月 22 日 - 添加了监视器等待取消。
- 2008 年 7 月 28 日 - Lwsync 现在与 boost 1.35.0 兼容。