决定使用多少个线程,并且这些线程应该去做什么。还需要决定是使用“全能”的线程去完成所有的任务,还是使用“专业”线程只去完成一件事情,或将两种方法混合。使用并发的时候,需要作出诸多选择来驱动并发,这里的选择会决定代码的性能和清晰度。因此,这里的选择至关重要,所以在你设计应用程序的结构时,再作出适当的决定。
使用过MPI(Message Passing Interface)和OpenMP的人对这个结构一定很熟悉:一项任务被分割成多个,放入一个并行任务集中,执行线程独立的执行这些任务,结果在会有主线程中合并。一种方式是使用递归算法(递减操作)。
使用栈的并行快速排序算法——等待数据块排序:
#include "threadsafe_stack.h"//省略部分头文件
template<typename T>
struct sorter // 1
{
struct chunk_to_sort
{
std::list data;
std::promiselist > promise;
};
threadsafe_stack chunks; // 2 支持在栈上简单的存储无序数据块
std::vector<std::thread> threads; // 3 对线程进行设置
unsigned const max_thread_count;
std::atomic<bool> end_of_data;
sorter() :
max_thread_count(std::thread::hardware_concurrency() - 1),
end_of_data(false)
{}
~sorter() // 4
{
end_of_data = true; // 5 设置标志
for (unsigned i = 0;i// 6 等待所有线程完成工作
}
}
void try_sort_chunk()
{
// 7 从栈上弹出一个数据块
std::shared_ptr chunk = chunks.pop();
if (chunk)
{
sort_chunk(chunk); // 8 排序
}
}
std::list do_sort(std::list & chunk_data) // 9
{
if (chunk_data.empty())
{
return chunk_data;
}
std::list result;
result.splice(result.begin(), chunk_data, chunk_data.begin());
T const& partition_val = *result.begin();
typename std::list ::iterator divide_point = // 10 对数据进行划分
std::partition(chunk_data.begin(), chunk_data.end(),
[&](T const& val) {return val//此函数操作是移动元素
std::futurelist > new_lower =
new_lower_chunk.promise.get_future();
chunks.push(std::move(new_lower_chunk)); // 11 将这些数据块的指针推到栈上
if (threads.size()// 12 有备用处理器的时候,产生新线程
{
threads.push_back(std::thread(&sorter::sort_thread, this));
//参数传入方式
}
//只剩高值部分,排序由当前线程完成
std::list new_higher(do_sort(chunk_data));
result.splice(result.end(), new_higher);
// 13 因为小于部分的数据块可能由其他线程进行处理,那么就得等待这个线程完成
while (new_lower.wait_for(std::chrono::seconds(0)) !=
std::future_status::ready) //等待低值完成
{
// 14 当线程处于等待状态时,就让当前线程尝试处理栈上的数据
try_sort_chunk();
}
result.splice(result.begin(), new_lower.get());
//从期望中得到数据list
return result;
}
void sort_chunk(std::shared_ptr const& chunk)
{
// 15 结果存在promise中,让线程对已经存在于栈上的数据块进行提取
chunk->promise.set_value(do_sort(chunk->data));
}
void sort_thread()
{
while (!end_of_data) // 16 未被设置时
{
try_sort_chunk();
// 17 新生成的线程还在尝试从栈上获取需要排序的数据块
std::this_thread::yield();
/*18 在循环检查中,也要给其他线程机会,
*可以从栈上取下数据块进行更多的操作。
*Provides a hint to the implementation to
*reschedule the execution of threads,
*allowing other threads to run.
*/
}
}
};
template<typename T>
std::list parallel_quick_sort(std::list input) // 19
{
if (input.empty())
{
return input;
}
sorter s;
// 20 当所有数据都已经排序完成,do_sort将会返回
return s.do_sort(input);
}
int main()
{
std::list<int> l{ 35,3,4,44,66,22,11,222,333,55,1,0,9,6,
35,3,4,44,66,22,11,222,333,55,1,0,9,6 };
auto r = parallel_quick_sort(l);//18ms
for (auto &im : r)
{
std::cout << im << std::endl;
}
system("pause");
return 0;
}
上面程序使用的线程安全栈(无锁栈的使用还在摸索中):
#ifndef _THREADSAFE_STACK_H
#define _THREADSAFE_STACK_H
template<typename T>
class threadsafe_stack//读书笔记5
{
private:
std::stack data;
mutable std::mutex m;
public:
threadsafe_stack() {}
threadsafe_stack(const threadsafe_stack& other)
{
std::lock_guard lock(other.m);
data = other.data;
}
threadsafe_stack& operator=(const threadsafe_stack&) = delete;
void push(T new_value)
{
std::lock_guard lock(m);
data.push(std::move(new_value)); // 1
}
std::shared_ptr pop()
{
std::lock_guard lock(m);
if (data.empty()) return std::make_shared();
// 2 修改了异常处理做法,返回空指针代替抛出异常
std::shared_ptr const res(
std::make_shared(std::move(data.top()))); // 3
data.pop(); // 4
return res;
}
void pop(T& value)
{
std::lock_guard lock(m);
if (data.empty()) throw empty_stack();
value = std::move(data.top()); // 5
data.pop(); // 6
}
bool empty() const
{
std::lock_guard lock(m);
return data.empty();
}
};
#endif // !_THREADSAFE_STACK_H
该方案制约线程数量的值就是std::thread::hardware_concurrency()
的值,这样就能避免任务过于频繁的切换。不过,这里还有两个问题:线程管理和线程通讯。要解决这两个问题就要增加代码的复杂程度。虽然,线程对数据项是分开处理的,不过所有对栈的访问,都可以向栈添加新的数据块,并且移出数据块以作处理。这里重度的竞争会降低性能(即使使用无锁(无阻塞)栈)。
这个方案使用到了一个特殊的线程池——所有线程的任务都来源于一个等待链表,然后线程会去完成任务,完成任务后会再来链表提取任务。这个线程池很有问题(包括对工作链表的竞争)。几种划分方法:1,处理前划分;2,递归划分(都需要事先知道数据的长度固定),还有上面的那种划分方式。事情并非总是这样好解决;当数据是动态生成,或是通过外部输入,那么这里的办法就不适用了。在这种情况下,基于任务类型的划分方式,就要好于基于数据的划分方式。
每个线程只需要关注自己所要做的事情即可。其本身就是基本良好的设计,每一段代码只对自己的部分负责。
分离关注:多线程下有两个危险需要分离关注。一个是对错误担忧的分离,主要表现为线程间共享着很多的数据,或者是不同的线程要相互等待。这两种情况都是因为线程间很密切的交互。当这种情况发生,就需要看一下为什么需要这么多交互。当所有交互都有关于同样的问题,就应该使用单线程来解决,并将引用同一原因的线程提取出来。或者,当有两个线程需要频繁的交流,且没有其他线程时,那么就可以将这两个线程合为一个线程。当通过任务类型对线程间的任务进行划分时,不应该让线程处于完全隔离的状态。当多个输入数据集需要使用同样的操作序列,可以将序列中的操作分成多个阶段,来让每个线程执行。
划分任务序列:当任务会应用到相同操作序列,去处理独立的数据项时,就可以使用流水线(pipeline)系统进行并发。这样可以为流水线中的每一阶段操作创建一个独立线程。
在多线程代码中有很多因素会影响性能——对线程处理的数据做一些简单的改动(其他不变),都可能对性能产生戏剧性的效果。
处理器个数是影响多线程应用的首要因素。当多于机器支持线程数在运行的时候(都没有阻塞或等待),应用将会浪费处理器的运算时间在线程间进行切换。这种情况发生时,我们称其为超额认购(oversubscription)。根据前面,我们可以知道使用std::thread::hardware_concurrency()
函数就能知道在给定硬件上可以扩展的线程数量了。但是要谨慎使用,因为它不会考虑其它运行在系统上的线程(除非已经将系统信息进行共享)。最坏的情况就是,多线程同时调用 std::thread::hardware_concurrency()
函数来对线程数量进行扩展,这样将导致庞大的超额认购。std::async()
就能避免这个问题,因为标准库会对所有的调用进行适当的安排。同样,谨慎的使用线程池也可以避免这个问题。
不过,即使你已经考虑到所有在应用中运行的线程,程序还要被同时运行的其他程序所影响。这里的一种选择是使用与std::async()
类似的工具,来为所有执行异步任务的线程的数量做考虑;另一种选择就是,限制每个应用使用的处理芯个数。理想算法可能会取决于问题规模与处理单元的比值。大规模并行系统中有很多的处理单元,算法可能就会同时执行很多操作,让应用更快的结束;这就要快于执行较少操作的平台,因为该平台上的每一个处理器只能执行很少的操作。随着处理器数量的增加,另一个问题就会来影响性能:多个处理器尝试访问同一个数据。
当两个线程并发的在不同处理器上执行,并且对同一数据进行读取,通常不会出现问题;因为数据将会拷贝到每个线程的缓存中,并且可以让两个处理器同时进行处理。不过,当有线程对数据进行修改的时候,这个修改需要更新到其他核芯的缓存中去,就要耗费一定的时间。根据线程的操作性质,以及使用到的内存序,这样的修改可能会让第二个处理器停下来,等待硬件内存更新缓存中的数据。即便是精确的时间取决于硬件的物理结构,不过根据CPU指令,这是一个特别特别慢的操作,相当于执行成百上千个独立指令。如果处理器很少需要互相等待,那么这种情况就是低竞争(low contention)。反之称为高竞争(high contention)。
数据在每个缓存中传递若干次叫做乒乓缓存(cache pingpong),当一个处理器因为等待缓存转移而停止运行时,这个处理器就不能做任何事情,所以对于整个应用来说,这就是一个坏消息。
随着处理器对数据的访问次数增加,对于互斥量的竞争就会增加,并且持有互斥量的缓存行将会在核芯中进行转移,因此会增加不良的锁获取和释放次数。有一些方法可以改善这个问题,其本质就是让互斥量对多行缓存进行保护,不过这样的互斥量需要自己去实现。如果乒乓缓存是一个糟糕的现象,那么该怎么避免它呢?在后面,答案会与提高并发潜能的指导意见相结合:减少两个线程对同一个内存位置的竞争。虽然,要实现起来并不简单。即使给定内存位置被一个线程所访问,可能还是会有乒乓缓存的存在,是因为另一种叫做伪共享(false sharing)的效应。
处理器缓存通常不会用来处理在单个存储位置,但其会用来处理称为缓存行(cache lines)的内存块。假设你有一个int类型的数组,并且有一组线程可以访问数组中的元素,且对数组的访问很频繁(包括更新)。通常int类型的大小要小于一个缓存行,同一个缓存行中可以存储多个数据项。因此,即使每个线程都能对数据中的成员进行访问,硬件缓存还是会产生乒乓缓存。每当线程访问0号数据项,并对其值进行更新时,缓存行的所有权就需要转移给执行该线程的处理器,这仅是为了让更新1号数据项的线程获取1号线程的所有权。缓存行是共享的(即使没有数据存在),因此使用伪共享来称呼这种方式。这个问题的解决办法就是对数据进行构造,让同一线程访问的数据项存在临近的内存中(就像是放在同一缓存行中),这样那些能被独立线程访问的数据将分布在相距很远的地方,并且可能是存储在不同的缓存行中。在本章接下来的内容中看到,这种思路对代码和数据设计的影响。
伪共享发生的原因:某个线程所要访问的数据过于接近另一线程的数据,另一个是与数据布局相关的陷阱会直接影响单线程的性能。
任务切换(task switching)。如果系统中的线程数量要比核芯多,每个核上都要运行多个线程。这就会增加缓存的压力,为了避免伪共享,努力让不同线程访问不同缓存行。因此,当处理器切换线程的时候,就要对不同内存行上的数据进行重新加载(当不同线程使用的数据跨越了多个缓存行时),而非对缓存中的数据保持原样(当线程中的数据都在同一缓存行时)。
当有超级多的线程准备运行时(非等待状态),任务切换问题就会频繁发生。这个问题我们之前也接触过:超额认购(oversubscription)。
如果有很多额外线程,就会有很多线程准备执行,而且数量远远大于可用处理器的数量,不过操作系统就会忙于在任务间切换,以确保每个任务都有时间运行。
当为多线程性能而设计数据结构的时候,需要考虑竞争(contention),伪共享(false sharing)和数据距离(data proximity)。这三个因素对于性能都有着重大的影响,并且你通常可以改善的是数据布局,或者将赋予其他线程的数据元素进行修改。
对于一个数组来说,访问连续的元素是最好的方式,因为这将会减少缓存的使用,并且降低伪共享的概率。当然,如果空间已经被N个元素所占有,且N个元素也就是每个缓存行上具体的存储元素数量,就会让伪共享的情况消失,因为线程将会对独立缓存行上的数据进行操作。
另一方面,因为矩阵是以行连续的方式存储,那么现在可以以N行的方式访问所有的元素。如果再次选择相邻行,这就意味着线程现在只能写入N行,这里就有不能被其他线程所访问的连续内存块。那么让线程对每组列进行处理就是一个改进,因为伪共享只可能有在一个内存块的最后几个元素和下一个元素的开始几个上发生,
第三个选择——将矩阵分成小矩阵块?这可以看作先对列进行划分,再对行进行划分。因此,划分列的时候,同样有伪共享的问题存在。如果你可以选择内存块所拥有行的数量,就可以有效的避免伪共享;将大矩阵划分为小块,对于读取来说是有好处的:就不再需要读取整个源矩阵了。
根据文中计算分析,将矩阵分成小块或正方形的块,要比使用单线程来处理少量的列好的多。当然,可以根据源矩阵的大小和处理器的数量,在运行时对块的大小进行调整。和之前一样,当性能是很重要的指标,就需要对目标架构上的各项指标进行测量。
最后,同样的原理可以应用于任何情况,这种情况就是有很大的数据块需要在线程间进行划分;仔细观察所有数据访问的各个方面,以及确定性能问题产生的原因。各种领域中,出现问题的情况都很相似:改变划分方式就能够提高性能,而不需要对基本算法进行任何修改。
根据上面对数组的探讨,优化方式总结如下:
//1
struct protected_data
{
std::mutex m;
char padding[65536]; // 65536字节已经超过一个缓存行的数量级
my_data data_to_protect;
};
//2
struct my_data
{
data_item1 d1;
data_item2 d2;
char padding[65536];
};
my_data some_array[256];
如果这样能够提高性能,你就能知道伪共享在这里的确存在。 parallel_accumulate
函数进行了剖析。std::accumulate
的原始并行版本(读书笔记1): template<typename Iterator, typename T>
struct accumulate_block
{
void operator()(Iterator first, Iterator last, T& result)
{
result = std::accumulate(first, last, result); // 1
}
};
template<typename Iterator, typename T>
T parallel_accumulate(Iterator first, Iterator last, T init)
{
unsigned long const length = std::distance(first, last); // 2
if (!length)
return init;
unsigned long const min_per_thread = 25;
unsigned long const max_threads =
(length + min_per_thread - 1) / min_per_thread;
unsigned long const hardware_threads =
std::thread::hardware_concurrency();
unsigned long const num_threads =
std::min(hardware_threads != 0 ? hardware_threads : 2, max_threads);
unsigned long const block_size = length / num_threads;
std::vector results(num_threads); // 3
std::vector<std::thread> threads(num_threads - 1); // 4
Iterator block_start = first; // 5
for (unsigned long i = 0;i<(num_threads - 1);++i)
{
Iterator block_end = block_start; // 6
std::advance(block_end, block_size);
threads[i] = std::thread( // 7 异常可能发生点
accumulate_block(),
block_start, block_end, std::ref(results[i]));
block_start = block_end; // 8
}
accumulate_block()(block_start, last, results[num_threads - 1]); // 9
std::for_each(threads.begin(), threads.end(),
std::mem_fn(&std::thread::join));
return std::accumulate(results.begin(), results.end(), init); // 10
}
当在(7)处创建了第一个线程,如果再抛出异常,就会出问题的;对于新的std::thread
对象将会销毁,程序将调用 std::terminate
来中断程序的运行。使用 std::terminate
的地方,可不是什么好地方。accumulate_block(9)的调用就可能抛出异常,就会产生和上面类似的结果;线程对象将会被销毁,并且调用 std::terminate
。另一方面,最终调用 std::accumulate
(10)可能会抛出异常,不过处理起来没什么难度,因为所有的线程在这里已经汇聚回主线程了。上面只是对于主线程来说的,不过还有很多地方会抛出异常:对于调用accumulate_block的新线程来说就会抛出异常(1)。没有任何catch块,所以这个异常不会被处理,并且当异常发生的时候会调用 std::terminater()
来终止应用的运行。也许这里的异常问题并不明显,不过这段代码是非异常安全的。std::packaged_task
和std::future
相结合,来解决这个问题。如果使用 std::packaged_task
重新构造代码,代码可能会是如下模样。 std::packaged_task
的并行 std::accumulate
: template<typename Iterator, typename T>
struct accumulate_block
{
T operator()(Iterator first, Iterator last) // 1
{
return std::accumulate(first, last, T()); // 2 显示传入默认构造函数
}
};
template<typename Iterator, typename T>
T parallel_accumulate(Iterator first, Iterator last, T init)
{
unsigned long const length = std::distance(first, last);
if (!length)
return init;
unsigned long const min_per_thread = 25;
unsigned long const max_threads =
(length + min_per_thread - 1) / min_per_thread;
unsigned long const hardware_threads =
std::thread::hardware_concurrency();
unsigned long const num_threads =
std::min(hardware_threads != 0 ? hardware_threads : 2, max_threads);
unsigned long const block_size = length / num_threads;
std::vector<std::future > futures(num_threads - 1); // 3 使用futures向量为每个新生线程存储
std::vector<std::thread> threads(num_threads - 1);
Iterator block_start = first;
for (unsigned long i = 0;i<(num_threads - 1);++i)
{
Iterator block_end = block_start;
std::advance(block_end, block_size);
/*std::packaged_task<> 对一个函数或可调用对象,绑定一个期望。
当 std::packaged_task<>对象被调用,它就会调用相关函数或可调用对象,
将期望状态置为就绪,返回值也会被存储为相关数据。*/
std::packaged_task task( // 4
accumulate_block());
//<>填写传入传出参数,task初始化为一个函数
futures[i] = task.get_future(); // 5 从任务中获取future
threads[i] = std::thread(std::move(task), block_start, block_end);
// 6 将需要处理的数据块的开始和结束信息传入,让新线程去执行这个任务
block_start = block_end;
}
/*当任务执行时,future将会获取对应的结果,以及任何抛出的异常。
如果相关任务抛出一个异常,那么异常就会被future捕捉到,
并且使用get()的时候获取数据时,这个异常会再次抛出。*/
T last_result = accumulate_block()(block_start, last);
// 7 将最终数据块的结果赋给一个变量进行保存
std::for_each(threads.begin(), threads.end(),
std::mem_fn(&std::thread::join));
T result = init; // 8 初始值
for (unsigned long i = 0;i<(num_threads - 1);++i)
{
result += futures[i].get(); // 9 累加每个future上的值
}
result += last_result; // 10 加上最后一块数据
return result;
}
这样,一个问题就已经解决:在工作线程上抛出的异常,可以在主线程上抛出。如果不止一个工作线程抛出异常,那么只有一个能在主线程中抛出,不过这不会有产生太大的问题。如果这个问题很重要,你可以使用类似std::nested_exception
(嵌套异常) 来对所有抛出的异常进行捕捉。剩下的问题就是,当生成第一个新线程和当所有线程都汇入主线程时,抛出异常;这样会让线程产生泄露。最简单的方法就是捕获所有抛出的线程,汇入的线程依旧是joinable()
的,并且会再次抛出异常: try
{
for (unsigned long i = 0;i<(num_threads - 1);++i)
{
// ... as before
}
T last_result = accumulate_block()(block_start, last);
std::for_each(threads.begin(), threads.end(),
std::mem_fn(&std::thread::join));
}
catch (...)
{
for (unsigned long i = 0;i<(num_thread - 1);++i)
{
if (threads[i].joinable())
thread[i].join();
}
throw;//再次抛出,解决异常,确保正确填充result
}
现在好了,无论线程如何离开这段代码,所有线程都可以被汇入。出现重复代码:std::for_each(threads.begin(),threads.end(),std::mem_fn(&std::thread::join));
class join_threads
{
std::vector<std::thread>& threads;
public:
explicit join_threads(std::vector<std::thread>& threads_) :
threads(threads_)
{}
~join_threads()
{
for (unsigned long i = 0;iif (threads[i].joinable())
threads[i].join();
}
}
};
异常安全版 std::accumulate: template<typename Iterator, typename T>
T parallel_accumulate(Iterator first, Iterator last, T init)
{
unsigned long const length = std::distance(first, last);
if (!length)
return init;
unsigned long const min_per_thread = 25;
unsigned long const max_threads =
(length + min_per_thread - 1) / min_per_thread;
unsigned long const hardware_threads =
std::thread::hardware_concurrency();
unsigned long const num_threads =
std::min(hardware_threads != 0 ? hardware_threads : 2, max_threads);
unsigned long const block_size = length / num_threads;
std::vector<std::future > futures(num_threads - 1);
std::vector<std::thread> threads(num_threads - 1);
join_threads joiner(threads);
// 1 当创建了线程容器,就对新类型创建了一个实例,可让退出线程进行汇入
Iterator block_start = first;
for (unsigned long i = 0;i<(num_threads - 1);++i)
{
Iterator block_end = block_start;
std::advance(block_end, block_size);
std::packaged_task task(
accumulate_block());
futures[i] = task.get_future();
threads[i] = std::thread(std::move(task), block_start, block_end);
block_start = block_end;
}
T last_result = accumulate_block()(block_start, last);
T result = init;
for (unsigned long i = 0;i<(num_threads - 1);++i)
{
result += futures[i].get(); // 2 将会阻塞线程,直到结果准备就绪
}
result += last_result;
return result;
}
不再需要显式的将线程进汇入。std::async()
的异常安全。当需要显式管理线程的时候,需要代码是异常安全的。 std::accumulate
——使用 std::async()
: template<typename Iterator, typename T>
T parallel_accumulate(Iterator first, Iterator last, T init)
{
unsigned long const length = std::distance(first, last); // 1 序列长度
unsigned long const max_chunk_size = 25;
if (length <= max_chunk_size)
{
return std::accumulate(first, last, init); // 2
}
else
{
Iterator mid_point = first;
std::advance(mid_point, length / 2); // 3 寻找数量中点
//这个数据块分成两部分,然后再生成一个异步任务对另一半数据进行处理
std::future first_half_result =
std::async(parallel_accumulate, // 4 异步任务
first, mid_point, init);
T second_half_result = parallel_accumulate(mid_point, last, T());
// 5 第二半的数据是通过直接的递归调用来处理
return first_half_result.get() + second_half_result; // 6 和相加
}
}
标准库能保证std::async
的调用能够充分的利用硬件线程,并且不会产生线程的超额认购,一些“异步”调用是在调用get()
(6)后同步执行的。优雅的地方,不仅在于利用硬件并发的优势,并且还能保证异常安全。如果有异常在递归调用(5)中抛出,通过调用std::async
(4)所产生的“期望”,将会在异常传播时被销毁。这就需要依次等待异步任务的完成,因此也能避免悬空线程的出现。另外,当异步任务抛出异常,且被future所捕获,在对get()
(6)调用的时候,future中存储的异常,会再次抛出。 while (true)
{
event_data event = get_event();
if (event.type == quit)
break;
process(event);
}
为了确保用户输入被及时的处理,无论应时在做些什么,get_event()
和process()
必须以合理的频率调用。这就意味着任务要被周期性的悬挂,并且返回到事件循环中,或get_event()
/process()
必须在一个合适地方进行调用。每个选项的复杂程度取决于任务的实现方式。下面,使用并发分离关注(将GUI线程和任务线程进行分离): std::thread task_thread;
std::atomic<bool> task_cancelled(false);
void gui_thread()
{
while (true)
{
event_data event = get_event();
if (event.type == quit)
break;
process(event);
}
}
void task()
{
while (!task_complete() && !task_cancelled)
{
do_next_operation();
}
if (task_cancelled)
{
perform_cleanup();
}
else
{
post_gui_event(task_complete);
}
}
void process(event_data const& event)
{
switch (event.type)
{
case start_task:
task_cancelled = false;
task_thread = std::thread(task);
break;
case stop_task:
task_cancelled = true;
task_thread.join();
break;
case task_complete:
task_thread.join();
display_results();
break;
default:
//...
}
}
std::for_each
std::accumulate
很相似。 template<typename Iterator, typename Func>
void parallel_for_each(Iterator first, Iterator last, Func f)
{
unsigned long const length = std::distance(first, last);
if (!length)
return;
unsigned long const min_per_thread = 25;
unsigned long const max_threads =
(length + min_per_thread - 1) / min_per_thread;
unsigned long const hardware_threads =
std::thread::hardware_concurrency();
unsigned long const num_threads =
std::min(hardware_threads != 0 ? hardware_threads : 2, max_threads);
unsigned long const block_size = length / num_threads;
std::vector<std::future<void> > futures(num_threads - 1); // 1 void表示无返回值
std::vector<std::thread> threads(num_threads - 1);
join_threads joiner(threads);
Iterator block_start = first;
for (unsigned long i = 0;i<(num_threads - 1);++i)
{
Iterator block_end = block_start;
std::advance(block_end, block_size);
std::packaged_task<void(void)> task( // 2 传入lambda函数,执行for_each()
[=]()
{
std::for_each(block_start, block_end, f);
});
futures[i] = task.get_future();
threads[i] = std::thread(std::move(task));
// 3 避免了传入线程的构造函数,无其它参数
block_start = block_end;
}
std::for_each(block_start, last, f);
for (unsigned long i = 0;i<(num_threads - 1);++i)
{
futures[i].get(); // 4 提供检索工作线程异常的方法
//如果不想把异常传递出去,就可以省略这一步。
}
}
使用 std::async
实现 std::for_each
(会简化代码): template<typename Iterator, typename Func>
void parallel_for_each(Iterator first, Iterator last, Func f)
{
/*运行时对数据进行迭代划分的,
而非在执行前划分好,
这是因为你不知道你的库需要使用多少个线程。*/
unsigned long const length = std::distance(first, last);
if (!length)
return;
unsigned long const min_per_thread = 25;
if (length<(2 * min_per_thread))
{
std::for_each(first, last, f); // 1
}
else
{
Iterator const mid_point = first + length / 2;
// 2 将每一级的数据分成两部分,异步执行另外一部分
std::future<void> first_half =
std::async(¶llel_for_each,
first, mid_point, f);
parallel_for_each(mid_point, last, f); // 3 剩余部分递归
first_half.get(); // 4 异常传播
}
}
std::find
std::packaged_task
来转移值和异常,在主线程上对返回值和异常进行处理;或者使用 std::promise
对工作线程上的最终结果直接进行设置。这完全依赖于你想怎么样处理工作线程上的异常。如果想停止第一个异常(即使还没有对所有元素进行处理),就可以使用 std::promise
对异常和最终值进行设置。另外,如果想要让其他工作线程继续查找,可以使用 std::packaged_task
来存储所有的异常,当线程没有找到匹配元素时,异常将再次抛出。这种情况下,我会选择 std::promise
,因为其行为和 std::find
更为接近。这里需要注意一下搜索的元素是不是在提供的搜索范围内。因此,在所有线程结束前,获取future上的结果。如果被future阻塞住,所要查找的值不在范围内,就会持续的等待下去。实现代码如下: template<typename Iterator, typename MatchType>
Iterator parallel_find(Iterator first, Iterator last, MatchType match)
{
struct find_element // 1 函数调用操作实现查找操作
{
void operator()(Iterator begin, Iterator end,
MatchType match,
std::promise* result,
std::atomic<bool>* done_flag) {
try
{
for (;(begin != end) && !done_flag->load();++begin)
{// 2 通过在给定数据块中的元素,检查每一步上的标识
if (*begin == match)
{
result->set_value(begin); // 3 匹配元素找到,设置promise
done_flag->store(true); // 4 原子变量设置
return;
}
}
}
catch (...) // 5 捕获所有异常
{
try
{
result->set_exception(std::current_exception()); // 6 设置异常
done_flag->store(true);
}
catch (...) // 7 捕获所有异常,丢弃
{
}
}
}
};
unsigned long const length = std::distance(first, last);
if (!length)
return last;
unsigned long const min_per_thread = 25;
unsigned long const max_threads =
(length + min_per_thread - 1) / min_per_thread;
unsigned long const hardware_threads =
std::thread::hardware_concurrency();
unsigned long const num_threads =
std::min(hardware_threads != 0 ? hardware_threads : 2, max_threads);
unsigned long const block_size = length / num_threads;
std::promise result; // 8 停止收获
std::atomic<bool> done_flag(false); // 9 原子变量标识
std::vector<std::thread> threads(num_threads - 1);
{ // 10 “启动-汇入”代码放在一个块中,所有线程都会在找到匹配元素时13进行汇入
join_threads joiner(threads);
Iterator block_start = first;
for (unsigned long i = 0;i<(num_threads - 1);++i)
{
Iterator block_end = block_start;
std::advance(block_end, block_size);
threads[i] = std::thread(find_element(), // 11 范围内元素查找
block_start, block_end, match,
&result, &done_flag);//引用
block_start = block_end;
}
find_element()(block_start, last, match, &result, &done_flag); // 12
}//joiner析构,线程全部汇入
if (!done_flag.load()) //13
{
return last;
}
return result.get_future().get(); // 14 获取返回值或异常
}
使用 std::async
实现的并行find算法: template<typename Iterator, typename MatchType>
Iterator parallel_find_impl(Iterator first, Iterator last, MatchType match,
std::atomic<bool>& done)// 1 递归传入标识,引用方式
{
try
{
unsigned long const length = std::distance(first, last);
unsigned long const min_per_thread = 25; // 2 让单个线程处理最少的数据项
if (length<(2 * min_per_thread)) // 3 数据块大小不足以分成两半,当前线程完成工作
{
for (;(first != last) && !done.load();++first) // 4 循环查找
{
if (*first == match)
{
done = true; // 5 标志设置
return first;
}
}
return last; // 6 没有找到则返回尾迭代器(并非最后一个元素)
}
else
{
Iterator const mid_point = first + (length / 2); // 7 数据中点
std::future async_result =
std::async(¶llel_find_impl, // 8 异步第二部分查找
mid_point, last, match, std::ref(done));
Iterator const direct_result =
parallel_find_impl(first, mid_point, match, done); // 9 递归第一部分查找
return (direct_result == mid_point) ?
async_result.get() : direct_result; // 10 返回结果确定或异常传播
/*相等,返回第二部分结果(通过此结果确定是否找到);
否则,返回第一部分(已经找到)。
如果异步查找真实的运行在其他线程上,
那么async_result变量的析构函数将会等待该线程完成,
所以这里不会有线程泄露(线程没有完成就被销毁)。*/
}
}
catch (...)//如果不使用try/catch就需要等待线程任务完成
{
done = true; // 11 只能捕捉在done发生的异常,并且当有异常抛出。
throw;
}
}
template<typename Iterator, typename MatchType>
Iterator parallel_find(Iterator first, Iterator last, MatchType match)
{
std::atomic<bool> done(false);
return parallel_find_impl(first, last, match, done); // 12 主入口点
}
stackoverflow:A thread leak is a resource leak where the resource being leaked is a thread. A thread dump is a way to dump all the information of threads for analysis (such as finding thread leaks).std::async
可以用来提供“异常-安全”和“异常-传播”特性。实现中一个重要的特性就是,不能保证所有数据都能被 std::find 串行处理。其他并行算法可以借鉴这个特性,因为要让一个算法并行起来这是必须具有的特性。如果有顺序问题,元素就不能并发的处理了。 std::partial_sum
std::partial_sum
会计算给定范围中的每个元素,并用计算后的结果将原始序列中的值替换掉。比如,有一个序列[1,2,3,4,5],在执行该算法后会成为:[1,3(1+2),6(1+2+3),template<typename Iterator>
void parallel_partial_sum(Iterator first, Iterator last)
{
typedef typename Iterator::value_type value_type;
struct process_chunk // 1 函数对象的对应类
{
void operator()(Iterator begin, Iterator last,
std::future* previous_end_value,//前块中最后一个值,请注意是指针
std::promise* end_value)//当前范围内最后一个值的原始值
{
try
{
Iterator end = last;
++end;
std::partial_sum(begin, end, begin); // 2 调用库函数
if (previous_end_value) // 3 不是第一块
{
value_type& addend = previous_end_value->get();
// 4 等待前面线程传递值后,取得前块中尾值或抛出异常
*last += addend;
// 5 为了将算法最大程度的并行,首先需要对最后一个元素进行更新
if (end_value)//如果不是最后一块
{
end_value->set_value(*last); // 6 值传递给下一个数据块
}
std::for_each(begin, last, [addend](value_type& item) // 7 传入lambda
{
item += addend;
});
}
else if (end_value)//是第一块,且不是最后一块
{
end_value->set_value(*last); // 8 值传递给下一个数据块
}
}
catch (...) // 9 捕获异常
{
if (end_value)
{
end_value->set_exception(std::current_exception()); // 10 设置异常
}
else
{
throw; // 11 如果是最后一块,直接抛出,所有异常将重新抛出
}
}
}
};
unsigned long const length = std::distance(first, last);
if (!length)
return last;
unsigned long const min_per_thread = 25; // 12 最小数据块
unsigned long const max_threads =
(length + min_per_thread - 1) / min_per_thread;
unsigned long const hardware_threads =
std::thread::hardware_concurrency();
unsigned long const num_threads =
std::min(hardware_threads != 0 ? hardware_threads : 2, max_threads);
unsigned long const block_size = length / num_threads;
typedef typename Iterator::value_type value_type;
std::vector<std::thread> threads(num_threads - 1); // 13 线程容器
std::vector<std::promise >
end_values(num_threads - 1); // 14 承诺容器,存储每块中的最后一个值
std::vector<std::future >
previous_end_values; // 15 期望容器,对前一块中的最后一个值进行检索
previous_end_values.reserve(num_threads - 1); // 16 预留内存
join_threads joiner(threads);
Iterator block_start = first;
for (unsigned long i = 0;i<(num_threads - 1);++i)
{
Iterator block_last = block_start;
std::advance(block_last, block_size - 1);
// 17 迭代器指向了每个数据块的最后一个元素,而不是作为一个普通值传递到最后
threads[i] = std::thread(process_chunk(), // 18
block_start, block_last,
(i != 0) ? &previous_end_values[i - 1] : 0,//前块中最后一个值
&end_values[i]);//当前范围内最后一个值的原始值
block_start = block_last;
++block_start; // 19 指向当前范围第一个元素
previous_end_values.push_back(end_values[i].get_future()); // 20 存储本块尾值
}
Iterator final_element = block_start;
std::advance(final_element, std::distance(block_start, last) - 1);
// 21 获取之前数据块中最后一个元素的迭代器
process_chunk()(block_start, final_element, // 22
(num_threads>1) ? &previous_end_values.back() : 0,
0);
//最后块,抛出动作一定会在主线程上进行。
}
因为线程间需要同步,这里的代码就不容易使用 std::async 重写。任务等待会让线程中途去执行其他的任务,所以所有的任务必须同时执行。class barrier
{
unsigned const count;
std::atomic<unsigned> spaces;
std::atomic<unsigned> generation;
public:
explicit barrier(unsigned count_) : // 1 指定数量
count(count_), spaces(count), generation(0)
{}
void wait()
{
unsigned const my_generation = generation; // 2
if (!--spaces) // 3 有线程在等待
{//spaces减至0时
spaces = count; // 4 重置
++generation; // 5 增加,向等待线程发出信号
}
else
{
while (generation == my_generation) // 6 自旋锁
std::this_thread::yield(); // 7 避免cpu忙等待
}
}
};
使用自旋等待的情况下,如果让线程等待很长时间就不会很理想,并且如果超过count数量的线程对wait()进行调用,这个实现就没有办法工作了。如果想要很好的处理这样的情况,必须使用一个更加健壮(更加复杂)的实现。不论怎么样,这些都需要你考虑到;需要有固定数量的线程执行同步循环。好吧,大多数情况下线程数量都是固定的。无论是让所有线程循环处理范围内的所有元素,还是让栅栏来同步线程,都会递减count的值。我会选择后者,因为其能避免线程做不必要的工作,仅仅是等待最终步骤完成。这需要对程序进行修改: struct barrier
{
//改为原子变量,多线程对其进行更新的时候,就不需要添加额外的同步
std::atomic<unsigned> count;
std::atomic<unsigned> spaces;
std::atomic<unsigned> generation;
barrier(unsigned count_) :
count(count_), spaces(count_), generation(0)
{}
void wait()
{
unsigned const gen = generation.load();
if (!--spaces)
{
spaces = count.load();//重置
++generation;
}
else
{
while (generation.load() == gen)
{
std::this_thread::yield();
}
}
}
//当一个线程完成其工作,并在等待的时候,才能对其进行调用它:
void done_waiting()
{
--count;//1 递减,下次重置spaces将会变小
if (!--spaces)//2 递减
{
spaces = count.load();
//3 一组当中最后一个线程需要对计数器进行重置,并且递增generation的值
++generation;
}
//最后一个线程不需要等待
}
};
此处需要合理设置count值,并在合适的地方调用wait()
和done_waiting()
。class join_threads;//参考前面定义
struct barrier;
template<typename Iterator>
void parallel_partial_sum(Iterator first, Iterator last)
{
typedef typename Iterator::value_type value_type;
struct process_element // 1
{
void operator()(Iterator first, Iterator last,
std::vector & buffer,
unsigned i, barrier& b)
{
value_type& ith_element = *(first + i);
bool update_source = false;
for (unsigned step = 0, stride = 1;stride <= i;++step, stride *= 2)
{
// 2 每一步,都会从原始数据或缓存中获取第i个元素
value_type const& source = (step % 2) ?
buffer[i] : ith_element;
value_type& dest = (step % 2) ?
ith_element : buffer[i];
value_type const& addend = (step % 2) ? // 3 获取到的元素加到指定stride的元素中去
buffer[i - stride] : *(first + i - stride);
dest = source + addend; // 4
update_source = !(step % 2);
b.wait(); // 5
}
if (update_source) // 6 更新
{
ith_element = buffer[i];
}
b.done_waiting(); // 7 本轮加和结束
}
};
unsigned long const length = std::distance(first, last);
if (length <= 1)
return;
std::vector buffer(length);//缓存
barrier b(length);//栅栏,count=5
std::vector<std::thread> threads(length - 1); // 8 线程容器
//线程的数量是根据列表中的数据量来定的,并非硬件支持数(api获取)
join_threads joiner(threads);
Iterator block_start = first;
for (unsigned long i = 0;i<(length - 1);++i)
{
threads[i] = std::thread(process_element(), first, last, // 9 运行一组线程
std::ref(buffer), i, std::ref(b));
}
process_element()(first, last, buffer, length - 1, b); // 10 主线程最后的调用
}
int main()
{
std::vector<int> v{ 1,2,3,4,5 };
parallel_partial_sum(v.begin(), v.end());
std::for_each(v.begin(), v.end(),
[](const int &i) {std::cout << i << std::endl;});//lambda参数是元素类型
return 0;
}
在一开始,这段程序我看的并不是很懂,包括栅栏的写法。后来,在一步一步的调试后,发现这段代码写的很是优雅。每个线程处理一轮部分加的操作,配合栅栏,将操作划分为几个阶段,达到最终目的。每一轮完成,栅栏count减一,下一阶段开始时候spaces也就少了一个。process_element
执行时抛出一个异常,其就会终止整个应用。这里可以使用一个 std::promise
来存储异常,或仅使用一个被互斥量保护的 std::exception_ptr
即可。