如何通过网站建设吸引客户并有效管理网站建设费用?
摘要:网站建设怎么找到客户,网站建设费无形资产,腾讯云学生怎么做网站的,天津网站优化公司哪家专业用于共享数据保护的替代工具 虽然互斥元是最通用的机制,但提到保护共享数据时,它们并不是
网站建设怎么找到客户,网站建设费 无形资产,腾讯云学生怎么做网站的,天津网站优化公司哪家专业用于共享数据保护的替代工具
虽然互斥元是最通用的机制#xff0c;但提到保护共享数据时#xff0c;它们并不是唯一的选择#xff1b;还有别的替代品#xff0c;可以在特定情况下提供更恰当的保护。
一个特别极端#xff08;但却相当常见#xff09;的情况#xff0c;…用于共享数据保护的替代工具
虽然互斥元是最通用的机制但提到保护共享数据时它们并不是唯一的选择还有别的替代品可以在特定情况下提供更恰当的保护。
一个特别极端但却相当常见的情况就是共享数据只在初始化时才需要并发访问的保护但在那之后却不需要显式同步。这可能是因为数据是一经创建就是只读的所以就不存在可能的同步问题或者是因为必要的保护作为数据上操作的一部分被隐式地执行。在任一情况中在数据被初始化之后锁定互斥元纯粹是为了保护初始化这是不必要的并且对性能会产生的不必要的打击。为了这个原因C标准提供了一种机制纯粹为了在初始化过程中保护共享数据。
在初始化时保护共享数据
假设你有一个构造起来非常昂贵的共享资源只有当实际需要时你才会要这样做。也许它会打开一个数据库连接或分配大量的内存。像这样的延迟初始化lazyinitialization在单线程代码中是很常见的——每个请求资源的操作首先检查它是否已经初始化如果没有就在使用之前初始化之。
std::shared_ptrsome_resource resource_ptr;
void foo()
{if(!resource_ptr){resource_ptr.reset(new some_resource); //❶}resource_ptr-do_something();
}如果共享资源本身对于并发访问是安全的当将其转换为多线程代码时唯一需要保护的部分就是初始化❶但是像清单3.11中这样的朴素的转换会引起使用该资源的线程产生不必要的序列化。这是因为每个线程都必须等待互斥元以检查资源是否已经被初始化。
//清单3.11 使用互斥元进行线程安全的延迟初始化
#include memory
#include mutexstruct some_resource
{void do_something(){}};std::shared_ptrsome_resource resource_ptr;
std::mutex resource_mutex;
void foo()
{std::unique_lockstd::mutex lk(resource_mutex); //所有的线程在这里被序列化if(!resource_ptr){resource_ptr.reset(new some_resource); //只有初始化需要被保护}lk.unlock();resource_ptr-do_something();
}int main()
{foo();
}
这段代码是很常见的不必要的序列化问题已足够大以至于许多人都试图想出一个更好的方法来实现包括臭名昭著的二次检查锁定(Double-Checked Locking)模式在不获取锁❶(在下面的代码中的情况下首次读取指针并仅当此指针为NULL时获得该锁。一旦已经获取了锁该指针要被再次检查❷(这就是二次检查的部分)以防止在首次检查和这个线程获取锁之间另一个线程就已经完成了初始化。
void undefined_behaviour_with_double_checked_locking()
{if(!resource_ptr) //❶{ std::lock_guardstd::mutex lk(resource_mutex);if(!resource_ptr) //❷{resource_ptr.reset(new some_resource); //❸}}resource_ptr-do_something(); //❹
}不幸的是这种模式因某个原因而臭名昭著。它有可能产生恶劣的竞争条件因为在锁外部的读取❶与锁内部由另一线程完成的写入不同步❸。这就因此创建了一个竞争条件不仅涵盖了指针本身还涵盖了指向的对象。就算一个线程看到另一个线程写入的指针它也可能无法看到新创建的 some_resource 实例从而导致do_something()❹的调用在不正确的值上运行。这是一个竞争条件的例子该类型的竞争条件被C标准定义为数据竞争(data race)因此被定为未定义行为。因此这是肯定需要避免的。
C标准委员会也发现这是一个重要的场景所以C标准库提供了std::once_flag和 std::call_once 来处理这种情况。与其锁定互斥元并且显式地检查指针还不如每个线程都可以使用std::call_once到 std::call_once返回时指针将会被某个线程初始化以完全同步的方式这样就安全了。使用std::call_once比显式使用互斥元通常会有更低的开销特别是初始化已经完成的时候所以在std::call_once符合所要求的功能时应优先使用之。下面的例子展示了与清单3.11相同的操作改写为使用std::call_once。在这种情况下通过调用函数来完成初始化但是通过一个带有函数调用操作符的类实例也可以很容易地完成初始化。与标准库中接受函数或者断言作为参数的大部分函数类似std::call_once可以与任意函数或可调用对象合作。
std::shared_ptrsome_resource resource_ptr;
std::once_flag resource_flag; //❶void int_resource()
{resource_ptr.reset(new some_resource);
}
void foo()
{std::call_once(resource_flag, init_resource); //初始化会被正好调用一次resource_ptr-do_something();
}在这个例子中std::once_flag❶和被初始化的数据都是命名空间作用域的对象但是std::call_once()可以容易地用于类成员的延迟初始化如清单3.12所示。
//清单3.12 使用std::call_one的线程安全的类成员延迟初始化
#include mutexstruct connection_info
{};struct data_packet
{};struct connection_handle
{void send_data(data_packet const){}data_packet receive_data(){return data_packet();}
};struct remote_connection_manager
{connection_handle open(connection_info const){return connection_handle();}
} connection_manager;class X
{
private:connection_info connection_details;connection_handle connection;std::once_flag connection_init_flag;void open_connection(){connectionconnection_manager.open(connection_details);}
public:X(connection_info const connection_details_):connection_details(connection_details_){}void send_data(data_packet const data) //❶{std::call_once(connection_init_flag,X::open_connection,this); //❷connection.send_data(data);}data_packet receive_data() //❸{std::call_once(connection_init_flag,X::open_connection,this);return connection.receive_data();}
};int main()
{}
在这个例子中初始化由首次调用 send_data()❶或是由首次调用receive_data()来完成。使用成员函数open_connection()来初始化数据同样需要将this指针传入函数。和标准库中其他接受可调用对象的函数一样比如std::thread 和std::bind()的构造函数这是通过传递一个额外的参数给std::call_once()来完成的❷。
值得注意的是像std::mutex、std::once_flag的实例是不能被复制或移动的所以如果想要像这样把它们作为类成员来使用就必须显式定义这些你所需要的特殊成员函数。
一个在初始化过程中可能会有竞争条件的场景是将局部变量声明为static的。这种变量的初始化被定义为在时间控制首次经过其声明时发生。对于多个调用该函数的线程这意味着可能会有针对定义“首次”的竞争条件。在许多C11之前的编译器上这个竞争条件在实践中是有问题的因为多个线程可能都认为它们是第一个并试图去初始化该变量又或者线程可能会在初始化已在另一个线程上启动但尚未完成之时试图使用它。在C11中这个问题得到了解决。初始化被定义为只发生在一个线程上并且其他线程不可以继续直到初始化完成所以竞争条件仅仅在于哪个线程会执行初始化而不会有更多别的问题。对于需要单一全局实例的场合这可以用作std::call_once的替代品。
class my_class;
my_class get_my_class_instance()
{static my_class instance; //❶初始化保证线程是安全的return instance;
}多个线程可以继续安全地调用get_my_class_instance()❶而不必担心初始化时的竞争条件。
保护仅用于初始化的数据是更普遍的场景下的一个特例那些很少更新的数据结构。对于大多数时间而言这样的数据结构是只读的因而可以毫无顾忌地被多个线程同时读取但是数据结构偶尔可能需要更新。这里我们所需要的是一种承认这一事实的保护机制。
保护很少更新的数据结构
假设有一个用于存储DNS条目缓存的表它用来将域名解析为相应的P地址。通常一个给定的DNS条目将在很长一段时间里保持不变——在许多情况下DNS条目会保持数年不变。虽然随着用户访问不同的网站新的条目可能会被不时地添加到表中但这一数据却将在其整个生命中基本保持不变。定期检查缓存条目的有效性是很重要的但是只有在细节已有实际改变的时候才会需要更新。
虽然更新是罕见的但它们仍然会发生并且如果这个缓存可以从多个线程访问它就需要在更新过程中进行适当的保护以确保所有线程在读取缓存时都不会看到损坏的数据结构。
在缺乏完全符合预期用法并且为并发更新与读取专门设计的专用数据结构的情况下这种更新要求线程在进行更新时独占访问数据结构直到它完成了操作。一旦更新完成该数据结构对于多线程并发访问又是安全的了。使用std::mutex来保护数据结构就因而显得过于悲观因为这会在数据结构没有进行修改时消除并发读取数据结构的可能我们需要的是另一种互斥元。这种新的互斥元通常称为读写reader-writer互斥元因为它考虑到了两种不同的用法由单个“写”线程独占访问或共享由多个“读”线程并发访问。
新的C标准库并没有直接提供这样的互斥元尽管已向标准委员会提议。由于这个建议未被接纳本节中的例子使用由Boost库提供的实现它是基于这个建议的。在后面你会看到使用这样的互斥元并不是万能药性能依赖于处理器的数量以及读线程和更新线程的相对工作负载。因此分析代码在目标系统上的性能是很重要的以确保额外的复杂度会有实际的收益。
你可以使用 boost::shared_mutex的实例来实现同步而不是使用std::mutex的实例。对于更新操作std::lock_guardboost::shared_mutex和std::unique_lockboost::shared_mutex可用于锁定以取代相应的std::mutex特化。这确保了独占访问就像std::mutex那样。那些不需要更新数据结构的线程能够转而使用boost::shared_lockboost::shared_mutex来获得共享访问。这与std::unique_lock用起来正是相同的除了多个线程在同一时间、同一boost::share_mutex上可能会具有共享锁。唯一的限制是如果任意一个线程拥有一个共享锁试图获取独占锁的线程会被阻塞直到其他线程全都撤回它们的锁同样地如果任意一个线程具有独占锁其他线程都不能获取共享锁或独占锁直到第一个线程撤回了它的锁。
清单3.13展示了一个简单的如前面所描述的DNS缓存使用std::map来保存缓存数据用boost::share_mutex进行保护。
//清单3.13 使用boost::share_mutex保护数据结构
#include map
#include string
#include mutex
#include boost/thread/shared_mutex.hppclass dns_entry
{};class dns_cache
{std::mapstd::string,dns_entry entries;boost::shared_mutex entry_mutex;
public:dns_entry find_entry(std::string const domain){boost::shared_lockboost::shared_mutex lk(entry_mutex); //❶std::mapstd::string,dns_entry::const_iterator const itentries.find(domain);return (itentries.end())?dns_entry():it-second;}void update_or_add_entry(std::string const domain,dns_entry const dns_details){std::lock_guardboost::shared_mutex lk(entry_mutex); //❷entries[domain]dns_details;}
};int main()
{}
在清单3.13中find_entry()使用一个boost::share_lock实例来保护它以供共享、只读的访问❶多个线程因而可以毫无问题地同时调用find_entry()。另一方面update_or_add_entry()使用一个 std::lock_guard实例在表被更新时提供独占访问❷不仅在调用update_or_add_entry()中其他线程被阻止进行更新调用find_entry()的线程也会被阻塞。
递归锁
在使用std::mutex的情况下一个线程试图锁定其已经拥有的互斥元是错误的并且试图这么做将导致未定义行为undefined behavior)。然而在某些情况下线程多次重新获取同一个互斥元却无需事先释放它是可取的。为了这个目的C标准库提供了std::recursive_mutex。它就像std::mutex一样区别在于你可以在同一个线程中的单个实例上获取多个锁。在互斥元能够被另一个线程锁定之前你必须释放所有的锁因此如果你调用lock()三次你必须也调用unlock()三次。正确使用std::lock_guardstd::recursive_mutex和std::unique_lockstd::recursive_mutex将会为你处理。
大多数时间如果你觉得需要一个递归互斥元你可能反而需要改变你的设计。递归互斥元常用在一个类被设计成多个线程并发访问的情况中因此它具有一个互斥元来保护成员数据。每个公共成员函数锁定互斥元进行工作然后解锁互斥元。然而有时一个公共成员函数调用另一个函数作为其操作的一部分是可取的。在这种情况下第二个成员函数也将尝试锁定该互斥元从而导致未定义行为。粗制滥造的解决方案就是将互斥元改为递归互斥元。这将允许在第二个成员函数中对互斥元的锁定成功进行并且函数继续。
然而这样的用法是不推荐的因为它可能导致草率的想法和糟糕的设计。特别地类的不变量在锁被持有时通常是损坏的这意味着第二个成员函数需要工作即便在被调用时使用的是损坏的不变量。通常最好是提取一个新的私有成员函数该函数是从这两个成员函数中调用的它不锁定互斥元它认为互斥元已经被锁定。然后你可以仔细想想在什么情况下可以调用这个新函数以及在那些情况下数据的状态。
