计算机视觉 基于CUDA编程的入门与实践 线程及同步三

一、线程同步

        对于需要线程之间互相交换数据才能完成任务的场景,必须存在某种能让线程彼此交流的机制。就需要共享内存,当很多线程并行工作并且访问相同的数据或者存储器位置的时候,线程间必须正确的同步。

        不过,线程间交换数据并不一定需要使用共享内存,只是共享内存较快而已。使用全局内存同样可以。例如配合正确的同步操作或者原子操作(原子操作也支持全局内存),依然可以正确地完成任务。只是使用共享内存,很多情况下较快(延迟较低,带宽较大)而已。

        为了确保并行线程协作时的正确结果,我们必须同步线程。CUDA 提供了一个简单的屏障同步原语__syncthreads()。线程的执行只有在其块中的所有线程都执行了。因此,我们可以通过在存储之后和从共享内存加载任何线程之前调用上述争用条件来避免上述争用条件。

二、共享内存

        共享内存位于芯片内部,因此它比全局内存快得多。(CUDA里面存储器的快慢有两方面,一个是延迟低,一个是带宽大。这里特指延迟低),相比没有经过缓存的全局内存访问,共享内存大约在延迟上低100倍。同一个块中的线程可以访问相同的一段共享内存(注意:不同块中的线程所见到的共享内存中的内容是不相同的),这在许多线程需要与其他线程共享它们的结果的应用程序中非常有用。但是如果不同步,也可能会造成混乱或错误的结果。如果某线程的计算结果在写入到共享内存完成之前被其他线程读取,那么将会导致错误。因此,应该正确地控制或管理内存访问。这是由__syncthreads()指令完成的,该指令确保在继续执行程序之前完成对内存的所有写入操作。这也被称为barrier。barrier的含义是块中的所有线程都将到达该代码行,然后在此等待其他线程完成。当所有线程都到达了这里之后,它们可以一起继续往下执行。

        使用变量声明说明符在 CUDA C/C++ 设备代码中声明共享内存。有多种方法可以在内核中声明共享内存,具体取决于内存量是在编译时还是在运行时已知。

        为了演示共享内存和线程同步的使用,我们这里给出一个计算数组中当前元素之前所有元素的平均值的例子:

        定义了具有10个float元素的共享内存上的数组。通常,共享内存的大小应该等于每个块的线程数。因为我们要处理10个(元素)的数组,所以我们也将共享内存的大小定义成这么大。

#include <stdio.h>

__global__ void gpu_shared_memory(float *d_a)
{
	// 定义每个线程专用的局部变量
	int i, index = threadIdx.x;
	float average, sum = 0.0f;
	//定义共享内存
	__shared__ float sh_arr[10];

	sh_arr[index] = d_a[index];

	__syncthreads();    // 这将确保对共享内存的所有写入都已完成

	for (i = 0; i<= index; i++) 
	{ 
		sum += sh_arr[i]; 
	}
	average = sum / (index + 1.0f);
	d_a[index] = average; 
	sh_arr[index] = average;
}

int main(int argc, char **argv)
{
	//Define Host Array
	float h_a[10];   
	//Define Device Pointer
	float *d_a;       
	
	for (int i = 0; i < 10; i++) {
		h_a[i] = i;
	}
	// allocate global memory on the device
	cudaMalloc((void **)&d_a, sizeof(float) * 10);
	// now copy data from host memory  to device memory 
	cudaMemcpy((void *)d_a, (void *)h_a, sizeof(float) * 10, cudaMemcpyHostToDevice);
	
	gpu_shared_memory << <1, 10 >> >(d_a);
	// 将修改后的阵列复制回主机内存
	cudaMemcpy((void *)h_a, (void *)d_a, sizeof(float) * 10, cudaMemcpyDeviceToHost);
	printf("Use of Shared Memory on GPU:  \n");
	//Printing result on console
	for (int i = 0; i < 10; i++) {
		printf("The running average after %d element is %f \n", i, h_a[i]);
	}
	return 0;
}

三、原子操作

        考虑当大量的线程需要试图修改一段较小的内存区域的情形,当试图进行“读取-修改-写入”操作序列的时候,这种情形经常会带来很多麻烦。一个简单的例子是代码d_out[i]++,这代码首先将d_out[i]的原值从存储器中读取出来,然后执行了+1操作,再将结果回写到存储器。然而,如果多个线程试图在同一个内存区域中进行这个操作,则可能会得到错误的结果。

        为了解决这种问题,CUDA提供了类似atomicAdd这种原子操作函数(很多其它语言也都提供了原子操作的函数,世界大同的赶脚)。该函数会从逻辑上保证,每个调用它的线程对相同的内存区域上的“读取旧值-累加-回写新值”操作是不可被其他线程扰乱的原子性的整体完成的。

(1)非原子操作示例

#include <stdio.h>

#define NUM_THREADS 10000
#define SIZE  10

#define BLOCK_WIDTH 100

__global__ void gpu_increment_without_atomic(int *d_a)
{
	// 计算当前线程的线程id
	int tid = blockIdx.x * blockDim.x + threadIdx.x;

	// 每个线程在SIZE变量处增加元素换行
	tid = tid % SIZE;
	d_a[tid] += 1;
}

int main(int argc, char **argv)
{

	printf("%d total threads in %d blocks writing into %d array elements\n",
		NUM_THREADS, NUM_THREADS / BLOCK_WIDTH, SIZE);

	// 声明和分配主机内存
	int h_a[SIZE];
	const int ARRAY_BYTES = SIZE * sizeof(int);

	// 声明和分配GPU内存
	int * d_a;
	cudaMalloc((void **)&d_a, ARRAY_BYTES);
	// 将GPU内存初始化为零
	cudaMemset((void *)d_a, 0, ARRAY_BYTES);

	gpu_increment_without_atomic << <NUM_THREADS / BLOCK_WIDTH, BLOCK_WIDTH >> >(d_a);

	// 将数组复制回主机内存
	cudaMemcpy(h_a, d_a, ARRAY_BYTES, cudaMemcpyDeviceToHost);

	printf("Number of times a particular Array index has been incremented without atomic add is: \n");
	for (int i = 0; i < SIZE; i++)
	{
		printf("index: %d --> %d times\n ", i, h_a[i]);
	}

	cudaFree(d_a);
	return 0;
}

(2)原子操作示例

        用atomicAdd原子操作函数替换了之前的直接+=操作,该函数具有2个参数:第一个参数是我们要进行原子加法操作的内存区域;第二个参数是该原子加法操作具体要加上的值。

#include <stdio.h>

#define NUM_THREADS 10000
#define SIZE  10

#define BLOCK_WIDTH 100

__global__ void gpu_increment_atomic(int *d_a)
{
	// 计算当前线程的线程id
	int tid = blockIdx.x * blockDim.x + threadIdx.x;

	// 每个线程在SIZE变量处增加元素换行
	tid = tid % SIZE;
	atomicAdd(&d_a[tid], 1);
}

int main(int argc, char **argv)
{
	printf("%d total threads in %d blocks writing into %d array elements\n",
		NUM_THREADS, NUM_THREADS / BLOCK_WIDTH, SIZE);

	// 声明和分配主机内存
	int h_a[SIZE];
	const int ARRAY_BYTES = SIZE * sizeof(int);

	// 声明和分配GPU内存
	int * d_a;
	cudaMalloc((void **)&d_a, ARRAY_BYTES);
	// 将GPU内存初始化为零
	cudaMemset((void *)d_a, 0, ARRAY_BYTES);
	
	gpu_increment_atomic << <NUM_THREADS / BLOCK_WIDTH, BLOCK_WIDTH >> >(d_a);
	
	// 将数组复制回主机内存
	cudaMemcpy(h_a, d_a, ARRAY_BYTES, cudaMemcpyDeviceToHost);
	
	printf("Number of times a particular Array index has been incremented is: \n");
	for (int i = 0; i < SIZE; i++) 
	{ 
		printf("index: %d --> %d times\n ", i, h_a[i]); 
	}
	
	cudaFree(d_a);
	return 0;
}

猜你喜欢

转载自blog.csdn.net/bashendixie5/article/details/128473628