快速排序-FP模式版
函数化编程(functional programming)是一种编程方式,函数结果只依赖于传入函数的参数。使用相同的参数调用函数,不管多少次都会获得相同的结果。
函数化编程的好处并不限于将“纯粹”作为默认方式(范型)的语言。
在PF(函数化)中使用future。我们实现一个简单的快排算法
快排算法思想:给一个数据列表,每次选取其中一个数为中间值,然后将数据列表中的其他数值分为2组,比他小的在前 大的在后
下面代码中的是FP-模式的串行实现,需要传入列表,并且返回一个列表,与 std::sort() 做同样的事情不
同。 (译者: std::sort() 是无返回值的,因为参数接收的是迭代器,所以其可以对原始列表直进行修改与排序。可参考sort())
// 快速排序——串行版
#include <list>
#include <algorithm>
template <typename T>
std::list<T> sequential_quick_sort(std::list<T> input)
{
if (input.empty())
{
return input;
}
std::list<T> result;
result.splice(result.begin(), input, input.begin());
T const &pivot = *result.begin();
auto divide_point = std::partition(input.begin(), input.end(),
[&](T const &t)
{ return t < pivot; });
std::list<T> lower_part;
lower_part.splice(lower_part.end(), input, input.begin(),
divide_point);
auto new_lower(
sequential_quick_sort(std::move(lower_part)));
auto new_higher(
sequential_quick_sort(std::move(input)));
result.splice(result.end(), new_higher);
result.splice(result.begin(), new_lower);
return result;
}
选择了FP模式的接口,要使用递归对两部分排序,所以需要创建两个列表。可以用splice()来完成,将
input列表小于divided_point的值移动到新列表lower_part中,其他数继续留在input列表中。而后,可以递归调用自己,对两个列表进行排序。显式使用 std::move() 将列表传递到函数中后,可以再次使用splice(),将result中的结果以正确的顺序进行拼接。new_higher指向的值放在“中间”值的后面,new_lower指向的值放在“中间”值的前面。
快速排序——FP模式线程强化
使用纯函数模式容易转化为并行版本。
快速排序——并行版
#include <list>
#include <algorithm>
#include <future>
template <typename T>
std::list<T> parallel_quick_sort(std::list<T> input)
{
if (input.empty())
{
return input;
}
std::list<T> result;
result.splice(result.begin(), input, input.begin());
T const &pivot = *result.begin();
auto divide_point = std::partition(input.begin(), input.end(),
[&](T const &t)
{ return t < pivot; });
std::list<T> lower_part;
lower_part.splice(lower_part.end(), input, input.begin(),
divide_point);
std::future<std::list<T>> new_lower(
std::async(¶llel_quick_sort<T>, std::move(lower_part)));
auto new_higher(
parallel_quick_sort(std::move(input)));
result.splice(result.end(), new_higher);
result.splice(result.begin(), new_lower.get());
return result;
}
当前线程不对小于“中间”值部分的列表进行排序, std::async() 会使用另一线程对列表进行排序。大于部分如同之前一样,使用递归进行排序。通过递归调用parallel_quick_sort(),可以使用硬件并发。 std::async() 会启动一个新线程,这样当递归三次时,就会有八个线程在运行了。当递归十次(对于大约
有1000个元素的列表),如果硬件能处理这十次递归调用,将会创建1024个执行线程。当运行库认为产生了太多的任务时(也许是因为数量超过了硬件并发的最大值),可能会同步的切换新产生的任务。当任务过多时(已影响性能),为了避免任务传递的开销,这些任务应该在使用get()获取结果的线程上运行,而不是在新线程上运行。这也符合 std::async 的行为,为每一个任务启动一个线程(甚至是在任务超额时,也就是在 std::launch::deferred 没有明确规定的情况下),或为了同步执行所有任务( std::launch::async 有明确规定的情况下)。当运行库自动裁剪线程时,建议去查看一下运行库的实现文档,了解一下将会有怎样的行为表现。
比起使用 std::async() ,可以写一个spawn_task()函数对 std::packaged_task 和 std::thread 做一下包装。需要为函数结果创建一个 std::packaged_task 对象, 并从这个对象中获取future,或在线程中返回future。其本身并没有太多优势(事实上会造成大规模的超额任务),但可为转型成一个更复杂的实现进行铺垫,实现会向队列添加任务,而后使用线程池的方式来运行。 std::async 更适合于已知所有任务的情况,并且要能完全控制线程池中构建或执行过任务的线程