并行编程总结

一.并行编程困难的历史原因

1. 并行系统曾经的高价格以及相对罕见。——已经解决
2. 研究人员以及从业人员的稀少。——已经解决
3. 缺少公开的并行代码。——已经解决
4. 缺少并行编程的工程经验。——已经解决
5. 任务间通信代价高昂,即使是共享内存的计算机系统也是如此。—— 目前仍然如此

二.并行编程的目标

相对于串行编程来说,并行编程有如下三个主要目标:
1. 性能
2. 生产率
3. 通用性

它说明一个事实:越往上层,生产率是如何变得越来越重要的。然而越往下层,性能和通用性就变得越来越重要。一方面,大量的开发工作消耗在上层,并且必须考虑通用性以降低成本。下层的性能损失很不容易在上层得到恢复。在靠近堆栈的顶端,也许只有少数的用户工作于特定的应用。这种情况下,生产率是最重要的。这解释了这样一种趋势:越往上层,采用额外的硬件通常比额外的开发者更划算。对于底层开发者,性能和通用性是主要关心的地方。

三.并行编程的替代方案

1. 顺序应用多实例化

2. 使用现有的并行软件(比如DBMS)

3. 性能优化(并行技术是一个有效的优化技术,但是它并不是唯一的技术)

四. 是什么使并行编程变得复杂

1. 工作分割

2. 并行访问控制

3. 资源分割和复制

4. 与硬件交互—— 硬件交互通常是操作系统的核心,编译器、库,或者其他的软件环境基础。
开发者涉及到新的硬件特性或者组件时,经常需要直接与这些硬件打交道。

尽管这四种并行编程方法是基础性的(可单独存在),但好的工程实践会组合使用这些方法:

这4 种任务必须在所有的并行编程中体现出来,但当然不是说开发者必须手工的实现这些任务。随着并行系统变的越来越便宜,越来越有效,我们可以预见这4 种任务会越来越自动化。比较典型的代表是SQL,基本实现了对单个长查询和多个独立的查询和更新操作进行自动并行化。

五. CPU性能影响点

CPU流水线————高度可预测的程序控制流程

内存引用————高度可预测的数据访问模式

原子操作————流水线必须延迟甚至需要冲刷以便一条原子操作成功完成
内存屏障————内存屏障的作用是防止CPU为了提升性能而进行的乱序执行,所以内存屏障几乎一定会降低CPU性能
原子操作和内存屏障的区别:原子操作通常只用于数据的单个元素。由于许多并行算法都需要在更新多个数据元素时,保证正确的执行顺序,大多数CPU提供了内存屏障。

cache miss————CPU高速缓存事实上对多CPU间频繁访问的变量起反效果。

I/O ———— I/O操作对性能的影响远大于前面几个性能影响点。(cache miss可以视为CPU之间的I/O操作,这应该是代价最低廉的I/O操作之一。)

六.对编程的要求

并行算法必须将每个线程设计成尽可能独立运行的线程。越少使用线程间通信手段,比如原子操作、锁或者其它消息传递方法,应用程序的性能可扩展性就会更好。

简而言之,想要达到优秀的并行性能可扩展性,就意味着在并行算法和实现中挣扎,小心的选择数据结构和算法,使用现有的并行软件和环境,或者将并行问题转换成已经有并行解决方案存在的问题。

七.并行编程领域的基本工具

1.脚本语言(&和管道)

2.POSIX多进程(进程之间不共享内存)

(1).API

       fork()/wait()

(2).特点

       父进程和子进程并不共享内存

int x = 0;
int pid;

pid = fork();
if (pid == 0) { /* child */
    x = 1;
    printf("Child process set x=1\n");
    exit(0);
}
if (pid < 0) { /* parent, upon error */
    perror("fork");
    exit(-1);
}

waitall();
printf("Parent process sees x=%d\n", x);

输出如下:
Child process set x=1
Parent process sees x=0

3.POSIX多线程(线程之间共享内存)

(1)API

       pthread_create()

       pthread_join()

   (2) 特点

        线程之间共享内存

int x = 0;

void *mythread(void *arg)
{
    x = 1;
    printf("Child process set x=1\n");
    return NULL;
}

int main(int argc, char *argv[])
{
    pthread_t tid;
    void *vp;
  
    if (pthread_create(&tid, NULL, mythread, NULL) !=
    {
        perror("pthread_create");
        exit(-1);
    }
    if (pthread_join(tid, &vp) != 0) {
        perror("pthread_join");
        exit(-1);
    }
    printf("Parent process sees x=%d\n", x);
    return 0;
}

程序输出:
Child process set x=1
Parent process sees x=1

4.POSIX互斥锁(用于解决线程之间共享内存引起的数据竞争问题)

5.POSIX 读写锁(读写锁是专门为大多数读的情况设计的——多读单写锁)

读写锁在临界区最小时开销最大,考虑到这一点,那么最好能有其他手段来保护极其短小的临界区——原子操作。

6.原子操作

1. gcc 编译器提供了许多附加的原子操作,包括__sync_fetch_and_sub()、__sync_fetch_and_or()、__sync_fetch_and_and()、__sync_fetch_and_xor()和__sync_fetch_and_nand()原语,这些操作都返回参数的原值。如果你一定需要变量的新值,可以使用__sync_add_and_fetch()、__sync_sub_and_fetch()、__sync_or_and_fetch()、__sync_and_and_fetch()、__sync_xor_and_fetch()和__sync_nand_and_fetch()原语。

2.__sync_synchronize()原语是一个“内存屏障”,它限制编译器CPU对指令乱序执行的优化。

3.在某些情况下,只限制编译器对指令的优化就足够了,CPU 的优化可以保留,此时就需要使用barrier()原语。

4.在某些情况下,只需要让编译器不优化某个内存访问就行了,此时可以使用ACCESS_ONCE()原语。

7. 趁手的工具——该如何选择?

根据经验定律,应该在能完成工作的工具中选择最简单的一个。如果可以,尽量串行编程。如果这还不够,那么使用shell 脚本来实现并行化。如果shell 脚本的fork()/exec()开销(在Intel 双核笔记本中最简单的C程序需要大概480 毫秒)太大,那么使用C 语言的fork()和wait()原语。如果这些原语的开销也太大(最小的子进程也需要80 毫秒),那么你可能需要用POSIX 线程库原语,选择合适的加锁、解锁原语和/或者原子操作。如果POSIX 线程库原语的开销仍然太大(一般低于毫秒级),那么就需要使用第8 章介绍的原语了。永远记住,进程内的通信和消息传递总是比共享内存的多线程执行要好

八.计数

1.并发计数的问题?

为了让每个CPU 得到机会增加一个指定全局变量,包含变量的缓存线需要在所有CPU 间传播,如图中红箭头所示。这种传播相当耗时,会导致糟糕性能。

2.统计计数器——基于数组的实现

data ownership模型——统计计数一般以每个线程一个计数器的方式处理(或者在内核运行时,每个CPU 一个),所以每个线程只更新自己的计数器。总的计数值就是所有线程计数器值的简单相加。

 

缺点:每个CPU可以快速地增加自己线程的变量值,不再需要要代价昂贵的跨越整个计算机系统的通信。但是这种在“更新”上扩展极佳的方法,在存在大量线程时,会带来“读取”上的巨大代价

static __inline__ void inc_count(void)
{
	unsigned long *p_counter = &__get_thread_var(counter);

	WRITE_ONCE(*p_counter, *p_counter + 1);
}							

static __inline__ unsigned long read_count(void)
{
	return READ_ONCE(global_count);
}							

void *eventual(void *arg)		
{
	int t;			
	unsigned long sum;

	while (READ_ONCE(stopflag) < 3) {
		sum = 0;
		for_each_thread(t)
			sum += READ_ONCE(per_thread(counter, t));
		WRITE_ONCE(global_count, sum);
		poll(NULL, 0, 1);
		if (READ_ONCE(stopflag)) {
			smp_mb();
			WRITE_ONCE(stopflag, stopflag + 1);
		}
	}
	return NULL;
}

 eventual()线程遍历所有线程,用atomic_xchg()函数减少每个线程的本地计数器的值,将减去的值的总和加到变量global_count 中。

3.统计计数器——基于每线程变量的实现

unsigned long __thread counter = 0;
unsigned long *counterp[NR_THREADS] = { NULL };		//每个线程都有一个自己私有的counter变量
unsigned long finalcount = 0;
DEFINE_SPINLOCK(final_mutex);

//每线程增加计数值的时候被调用
static __inline__ void inc_count(void)
{
	WRITE_ONCE(counter, counter + 1);
}			

//该函数获取总counter的时候被调用
static __inline__ unsigned long read_count(void)
{
	int t;
	unsigned long sum;

	spin_lock(&final_mutex);
	sum = finalcount;		
	for_each_thread(t)			
		if (counterp[t] != NULL)	
			sum += READ_ONCE(*counterp[t]);
	spin_unlock(&final_mutex);	
	return sum;
}

//下面两个函数分别在线程创建和线程退出的时候被调用					
void count_register_thread(unsigned long *p)
{
	int idx = smp_thread_id();

	spin_lock(&final_mutex);
	counterp[idx] = &counter;		//每个线程的counter指针指向各自的counter内存
	spin_unlock(&final_mutex);
}						

//count_unregister_thread()函数,每个之前调用过count_register_thread()函数的线程在退出时都需要调用该函数
void count_unregister_thread(int nthreadsexpected)
{
	int idx = smp_thread_id();

	spin_lock(&final_mutex);	
	finalcount += counter;		
	counterp[idx] = NULL;			
	spin_unlock(&final_mutex);		
}	

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

denglin12315

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值