C++并发编程实战:基于锁的并发数据结构

设计基于锁的并发数据结构的奥义就是,要确保先锁定合适的互斥,再访问数据,并尽可能缩短持锁时间。即使仅凭一个互斥来保护整个数据结构,其难度也不容忽视。我们在第3章已经分析过,需要保证不得访问在互斥锁保护范围以外的数据,且成员函数接口上不c#酒店客房管理系统得存在固有的条件竞争。若针对数据结构中的各部分分别采用独立互斥,这两个问题就会互相混杂而恶化。另外,假使并发数据结构上的操作需要锁住多个互斥,则可能会引发死锁。所以,相比只用一个互斥的数据结构,如果数据结构c语言版第二版课后答案我们考虑采用多个互斥,就需要更加谨慎。

我们将遵从上一节的指引,在这一节设计数据透视表几种简单的并发数据结构,使用互斥和锁数据结构有哪些来保护数据。我们要为各种并发数据结构提高并发程度,增加并发操作的实现机会,同时保证其线程安全。

第3章曾实现过并发的栈容器。它是我们手上最简单的并发数据结构,而且只用了一个互斥。它是线程安全的数据结构吗?以真正的并发作为衡量,它其他和其它的区别实现的并发程度算高吗?

6.2.1 采用锁实现线程安全的栈容器

第3章曾介绍c#过线程安全的栈容器,代码系统/运维清单6.1再度列出其代码。我们意在编写类似stdc#为什么用的人很少::stack&l数据结构教程第5版李春葆答案t;>的线程安全的栈容器,以支持数据的压入和弹出。

代码清单6.1 线程安全的栈容器的类定义

#include <exception>
struct empty_stack: std::exception
{
const char* what() const throw();
};
template<typename T>
class threadsafe_stack
{
private:
std::stack<T> data;
mutable std::mutex m;
public:
threadsafe_stack(){}
threadsafe_stack(const threadsafe_stack& other)
{
std::lock_guard<std::mutex> lock(other.m);
data=other.data;
}
threadsafe_stack& operator=(const threadsafe_stack&) = delete;
void push(T new_value)
{
std::lock_guard<std::mutex> lock(m);
data.push(std::move(new_value)); ⇽--- ①
}
std::shared_ptr<T> pop()
{
std::lock_guard<std::mutex> lock(m);
if(data.empty()) throw empty_stack(); ⇽--- ②
std::shared_ptr<T> const res(
std::make_shared<T>(std::move(data.top()))); ⇽--- ③
data.pop(); ⇽--- ④
return res;
}
void pop(T& value)
{
std::lock_guard<std::mutex> lock(m);
if(data.empty()) throw empty_stack();
value=std::move(data.top()); ⇽--- ⑤
data.pop(); ⇽--- ⑥
}
bool empty() const
{
std::lock_guard<std::mutex> lock(m);
return data.empty();
}
};

我们逐一核对每条指引,看看它们该怎样用于本例。

首先,我们看到每个成员函数都在内部互斥m之上数据结构实验报告加锁,因此这保障了基本的线程安全。这种方前端开发语言式保证了任何时刻都仅有唯一线程访问数据。因此,只要每个成员函数都维持不变量,就没有线程能见到不变量被破坏。

其次,在empty()和每个pop()重载之间,都潜藏着数据竞争的隐患。然而,pop()函数不仅可以加锁,其还可以明文判别内部的栈容器是否为空,所以这不属于恶性数据竞争。以上设计并未沿用std::stack<c#软件>的既有模式,即提供两个分离c#反射的成员函数top()和pop(),而是让pop()直接返回弹出的数据,因此避开了原本可能存在的数据透视表数据竞争。

接着,有几个操作可能产生异常。c#反射给互斥加锁可能产生其他应付款异常,但这极其罕见,因数据结构为只有互斥本身存在问题或数据结构严蔚敏系统资其他应收款是什么科目源耗尽,才可能出现这种状况。再者,每个成员函数的第一项内部操作就是加锁,所以栈容器所存储的并发编程数据尚未发生改动,即便抛出异常也是安全行为。互斥的解锁不可能失败,故它肯定安全,而st数据漫游是什么意思d::lock_guard<>则保证了绝不遗漏互斥的解锁操作。

data.push()的调用①有可能抛出异常,其诱因可能是复制/移动数据的过程抛出异常,也可能是底层的std栈数据结构c语言版容器在扩展容量时,不巧遇上内存分配不足。无论遇到哪种异其他应收款是什么科目常,内部的std::stack<>均能保证自身的安全,所以并不数据分析师成问题。

在p数据废土op()的第一个重载中,代码可能抛出empty_stack异常②,但任何改动都尚未发生,因此它是安全的抛出行为。共享指针res的创建③有机会抛出异常,原因可能是内存不足而无c#是什么法为新对象分配空间,也无法为引用计数而设的内部其他综合收益属于什么科目数据分配空间;也可能是虽然分数据结构教程第5版李春葆答案配了内存空间,但在数据的移动/复制过程中,其复制构造c#函数或移动构c#是什么造函数抛出异常。

针对这两c#委托种情形,C++运行库和标准库都保证不会出现内存泄漏,若存在创建失网页开发语言败残留的新对象,则会正确地销毁。因为底层栈容器依旧还没有改动,所以所存的数据还是安全的。data数据结构.pop()的调用④的实质操作是返回结果,它绝不会抛出异常,所以这一pop()重载是异常安全的重载。

pop()的第二个重系统运维工作内容载与第一个重载类似,不同之处在于,拷贝赋值操作符或移动赋值操作符会抛出异游戏开发语言常⑤,而非创建新对象和s前端开发语言td::shared_ptr实例。在调用data.pop()⑥之前,数据结构同样不发生改动,而按定义pop()不数据漫游是什么意思会抛出异常,所以这一pop()重载也是异常安全的重载。

最后,empty()不改动任何数据,是异常安全的函数。

这段代码有可能引起死锁,原因是我们在持锁期间执行用户代码:栈容器所含的数据中,有用户自定义的复制构造函数(①和③处的res构造)、移动构造函数(③处的make_shared)、拷贝赋值操作符和移动赋值操作符⑤,用户也有可能自行重载new操作数据结构实验报告符[1]。假使栈容器要插入或移除数据,在操作过程游戏开发语言中数据自身调用了上述函数,则可能再进一步调用栈容器的成员函数,因而需要获取锁,但相关的互斥却已被锁住,最后导致死锁。向栈容器添加/移除数系统运维工作内容据,却不涉及复制行为或内存分配,这是不切实际的空想。合理的解决方式是对栈容器的c#使用者提出要求,由他们负责保证避免以上死锁场景。

栈容器的所有成员函数都使用std::lock_guard<>保护数据,因此,同时调用各成员函数的线程没有数量限制。数据结构实验报告仅有构造函数和析构函数不是安全的成员函数,但这不成问题:每个对象都只能分别构造一次和销毁一次。若对象未完成构造或销毁到一半,转去调微信开发语言用成员函数,那么无论是否按并发方其他垃圾式执行,这都绝非正确之举系统运维工作内容。所以,必须由使用者自己保证:若栈容数据结构知识点总结器还未构建完成,则数据结构严蔚敏其他线程不得访问数据,并且,只有当全部线程都停止访问之后,才可销毁栈容器。

尽管在本例的栈容器上,由多线程并发调用成员函数是安全行为,但不论具体执行什么操作,锁的排他性仅容许一次只有一个线程访问数据。这将令多线程激烈争夺栈容器,迫系统运维工程师使它们串行化,应用程序的性能很可能因此而受限:线程一旦为了获取锁而等待,就变得无所事事。另外,该栈容器并未提供任何等待/添加数据的操作,因此,假如栈c#面试题容器满载数据而线程又等着添加数据,它就必须定期反c#多线程复调用empty(),或通过调用pop()而捕获empty_stack异常,从而查验c#多线程栈容器是否为空。万一真的出现这种情况,本例的栈容器实现就绝非最佳选择,因为等待的线程只有耗费宝贵的算力查验数据,或者栈容器使用者不得不另行编写代码数据结构实验报告,以在外部实现“等待-通知”的功能(如利用条件变量),令内部锁操作变得多余且浪数据结构c语言版第二版课后答案费。第4章的队列容器在内部采用条件变量,将等待行为融合到其他综合收益属于什么科目数据结构中。接下来,我们来分析队列容器。

6.2.2系统运维工程师 采用锁和条件变量实现线程安全的队列容器

第4章实现了线程安全的队列容器,如代码系统运维工作内容清单6.2所示,我们把数据结构c语言版严蔚敏第二版答案代码重新列出。并发栈容器的实现基于std::stack<>,与之类似,并发队列容器的实现则以std::queue<>为蓝本。本例的数据结构要顾及多线程并发访问的安全,因此接口与标准库的版本其他业务收入同样有所不同。

代码清单6.2 完整的类定义:采用条件变量实现的线程安全的队列容器

template<typename T>
class threadsafe_queue
{
private:
mutable std::mutex mut;
std::queue<T> data_queue;
std::condition_variable data_cond;
public:
threadsafe_queue()
{}
void push(T new_value)
{
std::lock_guard<std::mutex> lk(mut);
data_queue.push(std::move(new_value));
data_cond.notify_one(); ⇽--- ①
}
void wait_and_pop(T& value) ⇽--- ②
{
std::unique_lock<std::mutex> lk(mut);
data_cond.wait(lk,[this]{return !data_queue.empty();});
value=std::move(data_queue.front());
data_queue.pop();
}
std::shared_ptr<T> wait_and_pop() ⇽--- ③
{
std::unique_lock<std::mutex> lk(mut);
data_cond.wait(lk,[this]{return !data_queue.empty();}); ⇽--- ④
std::shared_ptr<T> res(
std::make_shared<T>(std::move(data_queue.front())));
data_queue.pop();
return res;
}
bool try_pop(T& value)
{
std::lock_guard<std::mutex> lk(mut);
if(data_queue.empty())
return false;
value=std::move(data_queue.front());
data_queue.pop();
return true;
}
std::shared_ptr<T> try_pop()
{
std::lock_guard<std::mutex> lk(mut);
if(data_queue.empty())
return std::shared_ptr<T>(); ⇽--- ⑤
std::shared_ptr<T> res(
std::make_shared<T>(std::move(data_queue.front())));
data_queue.pop();
return res;
}
bool empty() const
{
std::lock_guard<std::mutex> lk(mut);
return data_queue.empty();
}
};

代码清单6.2是并发队列数据结构c语言版容器其他应付款的实现,它与代码清单6.1的栈容器相似,不同之处在于push()中的data_cond.notify_其他应收款one()调用①,另外还增加了两个成员函数wait_and_pop()②③。try_pop()具有两个重载,与代码清单6.1的pop()几乎毫无二致。区别是即使队列容器全空,它们也不抛出异常。

其中一个重载返回布尔值,指明是系统/运维否通过传入的引用成功获取了值;另一个重载则返回一个NULL指针,表示容器内不存在数据,因而无法通过指针返回⑤。并发栈容器的pop()同样可以数据透视表采用以上模式。除了两个wait_and_并发编程三要素pop()函数之外,前文针对栈容器的并发设计和分析在这里也成立系统运维工作内容

并发栈容器的插入操作并不支持等待-通知功能,这里新增的两个wait_and_pop()函c#面试题数意在解决该问题。等待弹出的线程再也不必连续调用empty(),它可以改为调用wait_and_pop(),队列容器会通过条件变量处理其等待。data_cond.wait(开发语言有哪几种)的调用会被阻塞,直到底系统运维主要做什么层的队列容器中出现最少一个数据才返回,所以我们不必忧虑队列为空的状况;又因为互斥已经加锁,在等待期间数据仍然受到保护,所以这两个函数不会引入任何数据结构知识点总结数据竞争,死锁也不可能出现,不变量保持成立。

本例对线程安全的处理与栈容器稍有不同:假定在数据压入队列的过程中,有多个线程同时在等待,那么data数据分析师_cond.notify_one()的调用只会唤醒其中一个。然而,若该觉醒的线程在执行wait_and_pop()时抛出异常(譬如新指针std::shared_pt其他综合收益属于什么科目r<>在构建时就有可能产生异常④),就不会有任何其他线程被唤醒。如果我们不能接受这种行为方式,则将data_cond.notify_one()改为data_cond.notc#为什么用的人很少ify_c#委托all(),这轻而易举。这样就会唤醒全体线程,但要大大增加开销:它们绝大多数还是会发现队列依然为空[2],只好重新休眠。第二种处理方式是,倘若有异常抛数据结构出,则在wait_and_其他业务收入pop(其他应付款)中再次调用notify_one(),从c#怎么读而再唤醒另一线程,让它去获取存储的值。第三种处理方式是,将std::shared_ptr<>的初始化语句移动到push()的调用处,令队列容器改为存储std::shared_ptr<>,而不再直接存储数据的值。从内部std::queue<>复制std::shared_ptr<>数据结构知识点总结实例的操作不会抛出异常,所以wait_and_pop()也是异常安全的。我们采用最后一种处理方式改进并发队列容器,如代码清单6.3所示。

代码清单6.3 存储s系统运维主要做什么td::shared_ptr<>实例的线程安全的队列容器

template<typename T>
class threadsafe_queue
{
private:
mutable std::mutex mut;
std::queue<std::shared_ptr<T>> data_queue;
std::condition_variable data_cond;
public:
threadsafe_queue()
{}
void wait_and_pop(T& value)
{
std::unique_lock<std::mutex> lk(mut);
data_cond.wait(lk,[this]{return !data_queue.empty();});
value=std::move(*data_queue.front()); ⇽--- ①
data_queue.pop();
}
bool try_pop(T& value)
{
std::lock_guard<std::mutex> lk(mut);
if(data_queue.empty())
return false;
value=std::move(*data_queue.front()); ⇽--- ②
data_queue.pop();
return true;
}
std::shared_ptr<T> wait_and_pop()
{
std::unique_lock<std::mutex> lk(mut);
data_cond.wait(lk,[this]{return !data_queue.empty();});
std::shared_ptr<T> res=data_queue.front(); ⇽--- ③
data_queue.pop();
return res;
}
std::shared_ptr<T> try_pop()
{
std::lock_guard<std::mutex> lk(mut);
if(data_queue.empty())
return std::shared_ptr<T>();
std::shared_ptr<T> res=data_queue.front(); ⇽--- ④
data_queue.pop();
return res;
}
void push(T new_value)
{
std::shared_ptr<T> data(
std::make_shared<T>(std::move(new_value))); ⇽--- ⑤
std::lock_guard<std::mutex> lk(mut);
data_queue.push(data);
data_cond.notify_one();
}
bool empty() const
{
std::lock_guard<std::mutex> lk(mut);
return data_queue.empty();
}
};

队列数据所属的类型从值变成了共享指针,因而需要连带改动相关代码:其中两个pop()函数接收外部变量的引用作为参数,其功能是保存结果。原来的代码直接向它存入从底层队列容器获取的值,这里则需先根据指针提取出值①②,再将其作为结果存入。而另外两个pop()函数则返回std::shared_ptr<>实例,它们先从底层队列容器取出结果③④,再返回给外部使用者。数据分析师

如果数据通过s数据透视表td::shared_ptr<>间接存储,还会产生额外的好处:在push()中,我们依然要为新的std::shared_ptr<>c#软件实例分配内存⑤,这样可以脱离锁保护,但是按代码清单6.2的处理并发编程三要素方式,内存操作必须在持锁状态下进行。内存分配往往是成本相当高的操作,而新的队列以安全方式为其免除了锁保护,遂缩短了互斥的持锁时长,在分配内存的时候,还容许其他线程在队列容器上执行操作,因此非常有利于增强性能。

这个并发队列容器与前文的栈容器相似,缺点都是由唯一的互斥保护整个数据结构,它所系统运维工作内容支持的并发程度因此受限。虽然多个线程上的阻塞可能在不同成数据结构c语言版员函数中发生系统/运维,但是事实上每次只容许一个线程操作队列数据。该限制的部分原因是,这个实现基于标准库的开发语言std::queue<>容器,我们实际上将它视为一项大数据,或施加整体保护,或完全不保护。若能掌控数据结构的实现细节,我们就能提供粒度更精细的锁,以提高并发程度。

6.2其他垃圾.3 采用精细粒度的锁和条件变量实现线程安全的队列容器

在代码清单 6.2 和代码清单6.c#是什么3中,我们其实仅保护了一项数据,即整个内部队列data_queue,遂只用到一个互斥。为了采取精细粒度的锁操作,我们需要深入队列的实现,分数据透视表析其组成,为不同的数据单独使用互斥。

单向链表是可以充当队列的最简单的数据结构[3],如图6.1所示。队列含有一个“头指针head”,它指向头节点,每个节点再依次指向后继节点。队列弹出数据的方法是更改head指针:将数据结构有哪些指向目标改为其后继节点,并返回原来的第一项数据。

图6.1 单向链表形式的队列

新数据从队列末端系统/运维加入,其实现方式是,队列另外维护一个“尾指针tail”,指向尾节点。假如有新节点加入游戏开发语言,则将尾节点的next指针指向新节点,并更新tail指针,令其指向新节点。如果队列为空,则将hea其他综合收益属于什么科目d指针和tail指针都设置前端开发语言为NULL。

代码清单6.4是数据结构c语言版第二版课后答案这种队列的简单实现,它以代码清单6.2为基础,接口有所裁减。这个版本仅支持单线程,它只有一个try_pop()函数,尚不具备wc#面试题ait_and_pop()函数。

代码清单6.4 单线程队列的简单实现

template<typename T>
class queue
{
private:
struct node
{
T data;
std::unique_ptr<node> next;
node(T data_):
data(std::move(data_))
{}
};
std::unique_ptr<node> head; ⇽--- ①
node* tail; ⇽--- ②
public:
queue(): tail(nullptr)
{}
queue(const queue& other)=delete;
queue& operator=(const queue& other)=delete;
std::shared_ptr<T> try_pop()
{
if(!head)
{
return std::shared_ptr<T>();
}
std::shared_ptr<T> const res(
std::make_shared<T>(std::move(head->data)));
std::unique_ptr<node> const old_head=std::move(head);
head=std::move(old_head->next); ⇽--- ③
if(!head)
tail=nullptr;
return res;
}
void push(T new_value)
{
std::unique_ptr<node> p(new node(std::move(new_value)));
node* const new_tail=p.get();
if(tail)
{
tail->next=std::move(p); ⇽--- ④
}
else
{
head=std::move(p); ⇽--- ⑤
}
tail=new_tail; ⇽--- ⑥
}
};

首先,请注意代码清单6.4采用st其他d::unique_ptr<node>管控节点,通过其自身特性确保,当我们不再需要某个节点时,它和所包数据废土含的数据即被自动删除,我们不必明文编写相关操作的代码。从队列的头节点网页开发语言开始一直到队列末端,相邻节点之间都按前后顺序形成归属关系。末端节点已划归前方节数据分析师点的std::unique_ptr<noc#为什么用的人很少de>指针所有,但我们仍须对其进行直接操控c#是什么,所以通过一个原生指针(前文提及的“tail指针”)指向它。

虽然这种实现在数据废土单线程模式下工作良好,但是若我们换成多线程模式,并试图配合精细粒度的锁,其中几个细节就会引发问题。假设队列含有两项数据——head指针①和tail指针②,原则上我们可以使用两个互斥分别保数据结构知识点总结护head指针和tail指针,但问题随之而来。

最明显的问题是,push()可以同时改动head指针⑤和ta系统/运维il指针⑥,所以该函数就需要将两个互斥都锁住。尽管这并不合适,但同时锁住数据废土两个互斥的做法还算可行,问题不严重。严重的问题在于,push()和try_pop()有可能并发访问安卓开发语言同一数据恢复节点的next指其他综合收益针:push()更新tail→next④,而tr数据废土y_pop()则读取head→next③。如果队列仅含有一项数据,即head==tail,那么head软件开发语言→next和tail→next两个指针的目标节点重合,而它需要保护。假定我们数据恢复没有读取头节点和尾节点c#多线程的内部其他和其它的区别数据,无从辨别它们是否为同一个节点,就会在同时执行push()和try_pop系统运维主要做什么()的过程中,无意中试图锁定同一互斥,相比以前并无改进。如何突破困局?

1.通过分离数据而实现并发

我们可以预先游戏开发语言设立一个不含数据的虚位节点(dummy nod系统运维主要做什么e),从而确保至少存在一个节点,以区别头尾两个节点的访问。如果队列为空,head 和tail两个指针都其他垃圾不再是NUL软件开发语言L值,而是同时指向虚位节点c#多线程。这很不错,因为空队列的try_pop()不会访问head→next。若我们向队列添加数据(则会出现一个真c#委托实节点),则head安卓开发语言和tail指针会分别指向不同的节点,在head→next和tail→next上不会出现竞争。但其缺点是,为了容纳虚位节点,我们需要通过指数据恢复针间接存系统运维工程师储数据,额外增加了一个访问层级,如代码清单6.5所示。

代码清c#教程单6.5 带有虚位数据结构c语言版节点的简单队列

template<typename T>
class queue
{
private:
struct node
{
std::shared_ptr<T> data; ⇽--- ①
std::unique_ptr<node> next;
};
std::unique_ptr<node> head;
node* tail;
public:
queue():
head(new node),tail(head.get()) ⇽--- ②
{}
queue(const queue& other)=delete;
queue& operator=(const queue& other)=delete;
std::shared_ptr<T> try_pop()
{
if(head.get()==tail) ⇽--- ③
{
return std::shared_ptr<T>();
}
std::shared_ptr<T> const res(head->data); ⇽--- ④
std::unique_ptr<node> old_head=std::move(head);
head=std::move(old_head->next); ⇽--- ⑤
return res; ⇽--- ⑥
}
void push(T new_value)
{
std::shared_ptr<T> new_data(
std::make_shared<T>(std::move(new_value))); ⇽--- ⑦
std::unique_ptr<node> p(new node); ⇽--- ⑧
tail->data=new_data; ⇽--- ⑨
node* const new_tail=p.get();
tail->next=std::move(p);
tail=new_tail;
}
};

try_pop()的改动相当小。首先,由于引入了虚位节点c#面试题,head指针不再取值NULL,因此我们不再判别它是否为NULL,而改为比较指针head和tail是否重叠③。因为h系统/运维ead指针的类型是std::unique_ptr<node>,所以我们调用head.get()来进行比较运算。其次,节点现已改为通过指针存储数据①,所以在弹出操作中,我们直接获取指针④,而不再构建T类型的实例。最大的变化是pus数据库h(),我们必须先在堆数据段上创数据结构教程第5版李春葆答案建T类数据漫游是什么意思型的新实例,通过std::shared_ptr<>管控其归属权⑦(请注意我们采用了std::make_shared(),以避免因引数据结构并发编程计数而出现数据结构严蔚敏重复内存分配)。新创建的节点即为虚位节点,故无须向构造函数提供new_valu数据结构e值⑧。为了代替原来的增加数据的行为,我们将前面的共享其他应收款是什么科目指针⑦存入原来的虚位节点⑨,则该节点的数据变为新近创建的new_va并发编程三要素lue副本c#为什么用的人很少。最后,我们在队列的构造函数中创建虚位节点②。

行文至此,相信读者会问,这些改动带来了什么好处?它们对队列的线程安全有何帮助数据漫游

回答是,push()只访问tail指针而不再触及head指针,这就是一个好处。虽然try_pop()既访问head指针又访问tail指针,但tail指针只用于函数中最开始c#反射的比较运算,所以只需短暂持锁。最c#反射大的好处来自虚位节点,它存在的意义是:t数据库ry_pop()和pus其他业务收入h()不再同时操系统运维是干嘛的作相同的节点,所以我们不再需c#为什么用的人很少要由一个互斥c#多线程统领全局。换言之,指针head和tail其他可以各开发语言有哪几种用一互斥保护。但是,具体应该在哪一处加锁呢?

我们的目标是最大程度实现真正的并发功能,让尽可能多的操作有机会并安卓开发语言发进行,所以希望持锁时长最短。push()不难处理。tail指针的全部访问都需要对互斥加锁,即新节点一旦创建完成,我们就马上锁网页开发语言住互斥⑧,在将数据赋予当前的尾节点之前⑨,也要锁开发语言住互斥。该锁需要一直持有,等到函数结束才释放。

try_pop()的处理则不太简单数据结构题库。首先,我们数据需要为hea其他应收款d指针锁住互斥并一直持锁,等到它使用完成才解锁。互斥会被多个线程争抢,这将决定哪个线程弹出数据,故我们在最开始就要锁定互斥。一旦head指针的改动完成⑤,互斥即可解锁,结果的返其他垃圾有哪些东西回操作⑥无须互斥保护安卓开发语言

c#反射数据结构实验报告的只有tail指针的访问,它需要在对应的互斥上加锁。因为我们只需在try_pop()内部访数据漫游问tail指针一次,所以在临近读取指针之前再对互斥加锁数据结构c语言版严蔚敏第二版答案。最好将加锁和访问包装成同一个函数。实际上,因为仅有try_pop成员函数中的部分语句需锁住head_mutex,数据标注所以将它们包其他垃圾有哪些东西装成一个函数会显得更清数据结构有哪些晰,如代码清单6.6所示。

代码清单6.6 带有精细粒度锁的线程安全前端开发语言队列

template<typename T>
class threadsafe_queue
{
private:
struct node
{
std::shared_ptr<T> data;
std::unique_ptr<node> next;
};
std::mutex head_mutex;
std::unique_ptr<node> head;
std::mutex tail_mutex;
node* tail;
node* get_tail()
{
std::lock_guard<std::mutex> tail_lock(tail_mutex);
return tail;
}
std::unique_ptr<node> pop_head()
{
std::lock_guard<std::mutex> head_lock(head_mutex);

if(head.get()==get_tail())
{
return nullptr;
}
std::unique_ptr<node> old_head=std::move(head);
head=std::move(old_head->next);
return old_head;
}
public:
threadsafe_queue():
head(new node),tail(head.get())
{}
threadsafe_queue(const threadsafe_queue& other)=delete;
threadsafe_queue& operator=(const threadsafe_queue& other)=delete;
std::shared_ptr<T> try_pop()
{
std::unique_ptr<node> old_head=pop_head();
return old_head?old_head->data:std::shared_ptr<T>();
}
void push(T new_value)
{
std::shared_ptr<T> new_data(
std::make_shared<T>(std::move(new_value)));
std::unique_ptr<node> p(new node);
node* const new_tail=p.get();
std::lock_guard<std::mutex> tail_lock(tail_mutex);
tail->data=new_data;
tail->next=std::move(p);
tail=new_tail;
}
};

我们回想6.1.1节的指引,按严格的标准评判这段代码。我们先来明确程序含有哪些不变量,接着再查证它们是否数据结构c语言版第二版课后答案被破坏。

  • tail→next==nullptr。
  • tail→data==nu数据结构实验报告llptr。
  • hea其他垃圾有哪些东西d==tail说明队列为空。
  • 在单元素队列中,head→next==tail。
  • 对于每个节点x,只要x!=tail,则x→data指向一个T类型的实例,且x→next指向后续节点。
  • x→next==tail说明x是最后一个节点。从head指针指向的节点出发,我数据漫游是什么意思们沿着next指针反复访问后继节点,最终会到达tail指针指向的节点。

pu其他应收款sh()本身是清晰简单的操作,其仅有的数据结构的改动行为c#是什么受到互斥tac#软件il_mutex保护,这些改动行为维持不其他货币资金变量成立。因为新的尾其他综合收益属于什么科目节点是空节点,而且旧的尾节点的dat安卓开发语言a成员和next指针都设置正确,所以该尾节点现在成了队列中的最后一个真实节点数据漫游是什么意思

try_pop()则稍微复杂。分析表明,在互斥tail_mutex上加锁,不仅对读取并发编程三要素tail指针是必要的保护,当我们从头节点开始读取数据时,该加锁操作也必不可少,它保证了数据竞争不会出现。若缺少这个互斥,try_pop()和push()就很可能由不同线程并发调用,无法确定这两项操作的先后次序。虽然每个成员函数都在互斥数据结构与算法上持锁,但是它们锁住的互斥各不相同,所以它们有可能访问相同的数据其他业务收入。毕竟队列里的全部数据都来自push()的调用,数据都由其增加后端开发语言。多个线程系统运维工程师可能会访问同一项数据,而不c#多线程服从一定的内存次序。根据第5章的分析,这有可能构成数据竞争,并出现未定义行为。数据结构严蔚敏万幸,在get_tail()函数中互斥tail_mutex的锁定解决了这一切。由于get_taic#软件l()和push()两个其他综合收益属于什么科目调用都会锁住该互斥,因此两个调用网页开发语言之间会服从确定的内存次序。get_tail数据结构严蔚敏()其他应付款的调用或在push()开始前发生,或在其完成后发生。如果是前者,g数据分析师et_tail()只会见到tail指针的旧值;如果是后者,get_tail()就会见到tail指针已被赋予新值,还会见到原来的尾节点c#委托存入了新增的其他和其它的区别数据。

get_tail()的调用在head_mutex保护范围之内,这点也很重要。如若不然,pop_head()会在内部先调用get_tail(),再对互斥head_mutex加锁,代码如下。数据结构有哪些在这种情况中,可能其他线程已经调用了try_pop(),进而调用数据标注pop_数据结构c语言版严蔚敏第二版答案head(),锁住了互斥head_mutex,令pop_head()受阻而停滞不前,导致更严重的问题。

std::unique_ptr<node> pop_head()    ⇽---  ①这个实现有缺陷
{
node* const old_tail=get_tail(); ⇽--- ②在互斥head_mutex的保护范围以外取得tail指针的旧值
std::lock_guard<std::mutex> head_lock(head_mutex);

if(head.get()==old_tail) ⇽--- ③
{
return nullptr;
}
std::unique_ptr<node> old_head=std::move(head);
head=std::move(old_head->next); ⇽--- ④
return old_head;
}

上面的代码中,get_tail()的调用②在锁的作用域以外发生,导致暗藏隐患:等到数据结构严蔚敏当前线程可以在互斥head_mutex上加锁的时候数据分析师,指针head和tail有可能都发生了更改,get_tail()返回的节点可数据结构知识点总结能不再是尾节点,甚至可能不再是队列的组成节点。即便指针hea数据结构实验报告d确实指向了最后一个节点③,它和指针old_tail的比较也有可能不成立。结果,在更并发编程新head指针时④,可能令它外移,越过队列的尾节点,破坏整个数据结构。在代码清单6.6中,get_tail()的调用处于互斥head_mutex的保护范围之内,因而该实现方式正确、可行。这首先保证了其他线程都无法改变head指针,还保证了在调用push()加入新节点时,tail指针只能从队列末尾向外移动,该行为绝对安全。head指针不可能越过get_tailc#多线程()所返回的位置c#面试题,不变量遂保持成立。

一旦pop_开发语言head()将头节点c#委托从队列移除(方式是更新head指针),互斥随即解锁。接着,假如头结点是真实节点,try_pop()就提取出数据并销毁节点[4];假如是虚拟节点,则try_pop()返回一个含有NULL值的std::shared_ptr&l数据透视表t;>实例。我们清楚,执行的并发编程三要素线程是头节点的唯一访问者,因此try_pop()是安全操作。

下一个设计是队列的对外接口,它们是代码清单6.2的一部分。所c#委托以这里的分析与前文相同,结论同样是接口中不存在固有的条件竞争。

异常的处理就更复杂数据了。因为我们改变了数据的内存分配模式,所以异常可能c#是什么由不同的代码抛出。try_pop()中仅有一项操作会抛出异常,即互斥加锁,在获取锁之后,数据结构有哪些数据才会发生改动。因此,try_pop()是异数据结构教程第5版李春葆答案常安全的函数。另一方面,push()在堆上分配内存以创建两个实例,它们分别属于T类型和node类开发语言型,两次内存分配都有可能抛出异常。但这两个新创建的对象都被赋予智能指针,万一有异常抛出,它其他应收款们所占用的内存会被自动释放。在获取锁之后,push()余下的任何操其他应收款作都不会抛出异常,所以任务圆满完成,数据恢复push()是异常安全的函数。

我们没有改变接口的外在形式,所以死锁无法乘虚而入。成员函数内部同样无懈可击,唯一需要获取两其他业务收入个锁数据透视表数据结构c语言版严蔚敏第二版答案操作位于pop_head()内c#酒店客房管理系统,而它总是先锁住互斥head_mutex,然后对tail_mutex加锁苹果开发语言,故死锁不会出现游戏开发语言

我们关注的终极问题是并发是否真正可行。相比代码清单6.2的实现,这份数据结构threadsafe_queue的并发潜能数据标注要大得多,因为这里开发语言采用的锁粒度更精细,更多的操作在锁保护以外微信开发语言完成。例如,push()函数在没有持锁的状态下,为新节点和新数据完成了内存分配。其意义是,多个线c#为什么用的人很少程能为新节点和新数据并发分配内存,而不产生任何问题。每次只有数据结构c语言版第二版课后答案一个线程可将生成的新节点加入队列,只涉及几个简单的指针赋值操作,所以这里的代码持锁时长很短。相比之下,基于std::queu数据结构题库e<>微信开发语言;的实现则不然,因为它要为std::queue<>的一切内存分配操作加锁。

同样,try_pop()只在互斥tail_mutex上短暂持锁,数据结构c语言版严蔚敏第二版答案以保护tail指针的读取。因此,try_pop()的整个调用过程几乎都可以与push()并发执行。队列节点通数据漫游是什么意思过unique_ptr<node>的析构函数删除,该操作开销高,所以在互斥head_mutex的保护范围以外执行,因而在其锁定期间执行的操作也被缩减至最少。这样就增加了try_pop()的并发调用数数据漫游目,虽然每次只容许一数据结构个线程调用pop_head(),游戏开发语言但多个线程可以并数据恢复发执行try_pop()的数据结构c语言版第二版课后答案其他部分,安全地删除各自旧有的头节点并返回数据。

2.等待数据弹出

代码清单6.6实现了线程安全的队列,其中运用了精细粒度的锁其他应收款操作,但它只支持try_pop()(也只存在唯一一个重载其他垃圾)。然而代码清单 6.2 还提供了使用c#反射数据恢复便的wait_and_pop()函数,我们能否借助精细粒度的锁操作实现相同功能的函数?

当然能。问题是具体要怎么做c#多线程?修改push()似乎并不困难:在函其他应收款数末尾加上对data_cond.notify_one()的调用即可,与代码清单6.2一样。事情其实没那么简单微信开发语言,我们之所以采用精细粒度的锁,目的是尽可能提其他应付款高并发操作的数量。如果在notify_one()调用期间,互斥依然被锁住,形式与代码清单6.2一样,而等待通知的线程却在互斥解锁前觉醒,它就需要继续等待互斥解锁。矛盾的是,若在数据解锁互斥之后调用notify_one(),那么互斥已经可以再次获取,并且超前一步,等c#是什么着接受通知的线程对其加锁(前提是其他线程没有抢先数据库将其重新锁住)。这点改进看似细微,但对某些情况却有重要作用。

wait_and_pop()就复杂得多,因为我们需要确定在哪里等待、根据什么断言唤醒等待、需要锁住什么互斥等。等待唤醒的条件是“队列非空”,用代码表示为head!=tail。按这种写法,要求两个互斥head_mutex和tail_mutex都被锁住,我们分析代码清单6.6的时候就已经确定,只有在c#教程读取tail指针时才有必要锁住互微信开发语言斥tail_mutex,而比较运算无须保护,本例同理数据结构有哪些。若我们将断言设定为hea数据分析师d!=get_tail(),则只需持有互斥head_mute微信开发语言x,所以在调用苹果开发语言data_cond.wait()时,就可以重新锁住head_mutex[5]。只要我们加入了等待的逻辑,这种实现就与try_pop()一样。

对于try_pop()的另一个重载和对应的wait_and_pop()的重载,我们也要谨慎思考和设计。在代码清单6.6中,try_pop()函数的结果通过共享指c#委托针std::shared_ptr<>的实例返回,其指向目标由old_head间接从pop_head()取得。如果模仿代码清单6.2,数据结构严蔚敏将以上方法改为try_pop()的第一个重载的模式,令函数接收名为value的引用参数,再由拷贝赋值操作赋予它old_head的值,就可数据结构有哪些能会出现与异常有关的问题。根据这种改动,在拷贝赋值操作执开发语言有哪几种行时,数据已经从队列中移除,且互斥已经解锁,剩下的全部动作就是将数据返回给调用者。但是,如果拷贝赋值操作抛出了异常(完全有可能),则该项数据丢失,因为它无法回到队列本来的位置上。

若队列模板在具现化时,模板参数采用了实际类型T,而该类型支持不抛出异常的移动赋值操作,或不抛出数据结构c语言版第二版课后答案异常的交换操作,我们即可使用类型T。然而,我们还是更希望实现通用的解决方法,对任何类型T都有效。在上述场景中,我们需要在队列移除节点以前,将可能抛出异常的操作移动到锁的保护范围之内。换言之,我们还需要pop_head()的另一个重功,在改动队列之前就获取其存储的值。

相比而言,empty()就很简单了:只需锁住互斥head_mutex,然后检查head==get_tail()(见代码清单6数据结构与算法.10,该系统运维工作内容处head节点的指针由head.get()获得)。队列实现的最终代码由代码清单6.7~代码清单6.10给出。

代码清单6.7 采用锁操作并支持等待功能的线其他应付款程安全的数据标注队列:内部数据和对外接口

template<typename T>
class threadsafe_queue
{
private:
struct node
{
std::shared_ptr<T> data;
std::unique_ptr<node> next;
};
std::mutex head_mutex;
std::unique_ptr<node> head;
std::mutex tail_mutex;
node* tail;
std::condition_variable data_cond;
public:
threadsafe_queue():
head(new node),tail(head.get())
{}
threadsafe_queue(const threadsafe_queue& other)=delete;
threadsafe_queue& operator=(const threadsafe_queue& other)=delete;
std::shared_ptr<T> try_pop();
bool try_pop(T& value);
std::shared_ptr<T> wait_and_pop();
void wait_and_pop(T& value);
void push(T new_value);
bool empty();
};

数据码清单6.8实现了向队列压入新节点的操作,其过程相当直观系统运维主要做什么,这个实现与前文所示版本十分接近。

代码清单6.8 采用锁操作并支持等待功开发语言能的线程安全的队列:压入新数据

template<typename T>
void threadsafe_queue<T>::push(T new_value)
{
std::shared_ptr<T> new_data(
std::make_shared<T>(std::move(new_value)));
std::unique_ptr<node> p(new node);
{
std::lock_guard<std::mutex> tail_lock(tail_mutex);
tail->data=new_data;
node* const new_tail=p.get();
tail->next=std::move(p);
tail=new_tail;
}
data_cond.notify_one();
}

我们曾经提过,复杂之处全在于pop()上,它运用几个辅助前端开发语言函数简化操作。代码清单6数据结构与算法.9展示了wait_and_pop()及其辅助函数的实现。

代码清单6.9 采用锁操作并支持等待功能的线程安全的队列:wait_and_pop()c#反射

template<typename T>
class threadsafe_queue
{
private:
node* get_tail()
{
std::lock_guard<std::mutex> tail_lock(tail_mutex);
return tail;
}
std::unique_ptr<node> pop_head() ⇽--- ①
{
std::unique_ptr<node> old_head=std::move(head);
head=std::move(old_head->next);
return old_head;
}
std::unique_lock<std::mutex> wait_for_data() ⇽--- ②
{
std::unique_lock<std::mutex> head_lock(head_mutex);
data_cond.wait(head_lock,[&]{return head.get()!=get_tail();});
return std::move(head_lock); ⇽--- ③
}
std::unique_ptr<node> wait_pop_head()
{
std::unique_lock<std::mutex> head_lock(wait_for_data()); ⇽--- ④
return pop_head();
}
std::unique_ptr<node> wait_pop_head(T& value)
{
std::unique_lock<std::mutex> head_lock(wait_for_data()); ⇽--- ⑤
value=std::move(*head->data);
return pop_head();
}
public:
std::shared_ptr<T> wait_and_pop()
{
std::unique_ptr<node> const old_head=wait_pop_head();
return old_head->data;
}
void wait_and_pop(T& value)
{
std::unique_ptr<node> const old_head=wait_pop_head(value);
}
};

代码清单 6.9 展示出wait_and数据结构题库_pop()实现代码,它含有几个辅助函数,用以简化代码和减少重复,如pop_head()①和wai数据结构c语言版第二版课后答案t_for_data()②。前者移除头节点而改动队列,后者开发语言则等待数据加入空队列,以将其弹出。wait_for_data()特别值得注意,它在条件变量上等待,以lambda函数作为断言,并且向调用者返回锁的实例③。因为waitc#为什么用的人很少_pop_head()的两个重载都会改动队列数据,并且都依赖微信开发语言wait_for_微信开发语言data()函数,而后者将锁并发编程返回则保证了头节点弹出的全过程都持有同一个锁④⑤。这里的pop_head()也为try_pop()复用,如代码清单6.10所示。

代码清单6.10 采用锁操作并支持等待功能的线程安c#酒店客房管理系统全的队列:try_pop()和empty()

template<typename T>
class threadsafe_queue
{
private:
std::unique_ptr<node> try_pop_head()
{
std::lock_guard<std::mutex> head_lock(head_mutex);
if(head.get()==get_tail())
{
return std::unique_ptr<node>();
}
return pop_head();
}
std::unique_ptr<node> try_pop_head(T& value)
{
std::lock_guard<std::mutex> head_lock(head_mutex);
if(head.get()==get_tail())
{
return std::unique_ptr<node>();
}
value=std::move(*head->data);
return pop_head();
}
public:
std::shared_ptr<T> try_pop()
{
std::unique_ptr<node> old_head=try_pop_head();
return old_head?old_head->data:std::shared_ptr<T>();
}
bool try_pop(T& value)
{
std::unique_ptr<node> const old_head=try_pop_head(value);
return old_head;
}
bool empty()
{
std::lock_guard<std::mutex> head_lock(head_mutex);
return (head.get()==get_tail());
}
};

第7章将数据分析讲解无锁队列,它以这个队列的实现作为蓝本。这个队列数据漫游是什么意思是无限队列。只要存在空闲内存,即便已存入的数据没有被移除,各个线程还是能持续往队列添加新数据。与之c#面试题对应的是有限队列,其最大长度在创建之际就已固定。一旦有限队列容量已满,再试图向其压入数据就会失败,或者发生阻塞,直到有数c#软件据弹出而c#软件产生容纳空间为止。有限队列可用于多线程的工作分配,它能够依据待执行的任务的数量,确保工作在各线程中均匀分配。它能防止以下情形发生:某些线程向队列添加任务的速度过快,远超线程从队列领取任务的速度。

要实现这个功能,仅需简单地扩展本节讲解的无限队安卓开发语言列代码:只需限制push()中数据结构c语言版的条件变量上的等待数量。我们需要等待队列中的数据被弹其他应收款出(由pop()执行),所含数据数目小数据库于其最大容量,而不是等着有数据被压入而使队列非空。关于有限队列的进一步讨论已经超出本书范围。现在,我们来研究更加复杂的数据结构。


                                            C++并发编程实战:基于锁的并发数据结构

这是一本介绍c#是什么C++并发和多线程编程的深度指南。本书从C++标准程序库的各种工具讲起其他应付款,介绍线程管控、在线程间共享数据、并发操作的同步、C++内存模型和原子操作等内容。同时,本书还介绍基于锁的并发数据结构、无锁数据结构、并发代码,以及高级线程管理、并行算法函数、其他应付款多线程应用的测试和除错。本数据分析师书还通过附录及线上资源提供丰富的补充资料,以帮助读者更完整、细致地掌握数据结构C++并发编程的知识脉络。

本书适合需要深入了解C++多线程开发的读者,以及系统运维工程师使用C++进行各类软件开发的开发人员、测试人员,还可以作为C++线程库的参考工具书。