我正在尝试实现基于原子的互斥锁。
我接替了它,但我有一个关于warps / deadlock的问题。
此代码效果很好。
bool blocked = true;
while(blocked) {
if(0 == atomicCAS(&mLock, 0, 1)) {
index = mSize++;
doCriticJob();
atomicExch(&mLock, 0);
blocked = false;
}
}
但是这个没有......
while(true) {
if(0 == atomicCAS(&mLock, 0, 1)) {
index = mSize++;
doCriticJob();
atomicExch(&mLock, 0);
break;
}
}
我认为这是退出循环的一个位置。在第一个,退出发生在条件是,在第二个它发生在if的结尾,所以线程等待其他warp完成循环,但其他线程也等待第一个线程...但我想我我错了,所以如果你能解释我:)。
谢谢!
答案 0 :(得分:4)
此处还有关于互斥锁的其他问题。你可能想看一些它们。例如,搜索“cuda critical section”。
假设一个人会工作,一个人不会因为它似乎适用于你的测试用例是危险的。管理互斥锁或关键部分,尤其是当协商在同一个warp 中的线程之间时,这是非常困难和脆弱的。一般的建议是避免它。如其他地方所述,如果必须使用互斥锁或关键部分,请让线程块中的单个线程协商任何需要它的线程,然后使用线程块内同步机制(例如__syncthreads()
)来控制线程块内的行为。 / p>
如果不查看编译器排序各种执行路径的方式,就无法真正回答这个问题(IMO)。因此,我们需要查看SASS代码(机器代码)。您可以使用cuda binary utilities执行此操作,并且可能希望同时参考PTX reference和SASS reference。这也意味着您需要完整代码,而不仅仅是您提供的代码段。
这是我的分析代码:
$ cat t830.cu
#include <stdio.h>
__device__ int mLock = 0;
__device__ void doCriticJob(){
}
__global__ void kernel1(){
int index = 0;
int mSize = 1;
while(true) {
if(0 == atomicCAS(&mLock, 0, 1)) {
index = mSize++;
doCriticJob();
atomicExch(&mLock, 0);
break;
}
}
}
__global__ void kernel2(){
int index = 0;
int mSize = 1;
bool blocked = true;
while(blocked) {
if(0 == atomicCAS(&mLock, 0, 1)) {
index = mSize++;
doCriticJob();
atomicExch(&mLock, 0);
blocked = false;
}
}
}
int main(){
kernel2<<<4,128>>>();
cudaDeviceSynchronize();
}
kernel1
是我对死锁代码的表示,kernel2
是我对“工作”代码的表示。当我在CUDA 7下在linux上编译并在cc2.0设备(Quadro5000)上运行时,如果我调用kernel1
代码将会死锁,如果我调用kernel2
(如图所示)它不会“T
我使用cuobjdump -sass
转储机器代码:
$ cuobjdump -sass ./t830
Fatbin elf code:
================
arch = sm_20
code version = [1,7]
producer = <unknown>
host = linux
compile_size = 64bit
code for sm_20
Fatbin elf code:
================
arch = sm_20
code version = [1,7]
producer = cuda
host = linux
compile_size = 64bit
code for sm_20
Function : _Z7kernel1v
.headerflags @"EF_CUDA_SM20 EF_CUDA_PTX_SM(EF_CUDA_SM20)"
/*0000*/ MOV R1, c[0x1][0x100]; /* 0x2800440400005de4 */
/*0008*/ MOV32I R4, 0x1; /* 0x1800000004011de2 */
/*0010*/ SSY 0x48; /* 0x60000000c0000007 */
/*0018*/ MOV R2, c[0xe][0x0]; /* 0x2800780000009de4 */
/*0020*/ MOV R3, c[0xe][0x4]; /* 0x280078001000dde4 */
/*0028*/ ATOM.E.CAS R0, [R2], RZ, R4; /* 0x54080000002fdd25 */
/*0030*/ ISETP.NE.AND P0, PT, R0, RZ, PT; /* 0x1a8e0000fc01dc23 */
/*0038*/ @P0 BRA 0x18; /* 0x4003ffff600001e7 */
/*0040*/ NOP.S; /* 0x4000000000001df4 */
/*0048*/ ATOM.E.EXCH RZ, [R2], RZ; /* 0x547ff800002fdd05 */
/*0050*/ EXIT; /* 0x8000000000001de7 */
............................
Function : _Z7kernel2v
.headerflags @"EF_CUDA_SM20 EF_CUDA_PTX_SM(EF_CUDA_SM20)"
/*0000*/ MOV R1, c[0x1][0x100]; /* 0x2800440400005de4 */
/*0008*/ MOV32I R0, 0x1; /* 0x1800000004001de2 */
/*0010*/ MOV32I R3, 0x1; /* 0x180000000400dde2 */
/*0018*/ MOV R4, c[0xe][0x0]; /* 0x2800780000011de4 */
/*0020*/ MOV R5, c[0xe][0x4]; /* 0x2800780010015de4 */
/*0028*/ ATOM.E.CAS R2, [R4], RZ, R3; /* 0x54061000004fdd25 */
/*0030*/ ISETP.NE.AND P1, PT, R2, RZ, PT; /* 0x1a8e0000fc23dc23 */
/*0038*/ @!P1 MOV R0, RZ; /* 0x28000000fc0025e4 */
/*0040*/ @!P1 ATOM.E.EXCH RZ, [R4], RZ; /* 0x547ff800004fe505 */
/*0048*/ LOP.AND R2, R0, 0xff; /* 0x6800c003fc009c03 */
/*0050*/ I2I.S32.S16 R2, R2; /* 0x1c00000008a09e84 */
/*0058*/ ISETP.NE.AND P0, PT, R2, RZ, PT; /* 0x1a8e0000fc21dc23 */
/*0060*/ @P0 BRA 0x18; /* 0x4003fffec00001e7 */
/*0068*/ EXIT; /* 0x8000000000001de7 */
............................
Fatbin ptx code:
================
arch = sm_20
code version = [4,2]
producer = cuda
host = linux
compile_size = 64bit
compressed
$
考虑单个warp,使用任一代码,所有线程必须获取锁(通过atomicCAS
)一次,以便代码成功完成。使用任一代码,warp中只有一个线程可以在任何给定时间获取锁,并且为了使warp中的其他线程(稍后)获取锁,该线程必须有机会释放它(通过atomicExch
)。
这些实现之间的关键区别在于编译器如何针对条件分支调度atomicExch
指令。
让我们考虑一下“死锁”代码(kernel1
)。在这种情况下,{<1}}指令直到之后一个(并且只有)条件分支(ATOM.E.EXCH
)指令才会发生。 CUDA代码中的条件分支表示经线发散的可能点,并且在经线发散之后的执行在某种程度上是未指定的并且直到机器的细节。但鉴于这种不确定性,获取锁的线程可能会等待其他线程完成其分支,在执行@P0 BRA 0x18;
指令之前,这意味着其他线程不会有机会获得锁定,我们陷入僵局。
如果我们将其与“工作”代码进行比较,我们会看到,一旦发出atomicExch
指令,该点与该点之间就会有 no 条件分支。发出ATOM.E.CAS
指令,从而释放刚获取的锁。由于获取锁定的每个线程(通过ATOM.E.EXCH
)将在任何条件分支发生之前释放它(通过ATOM.E.CAS
),因此没有任何可能(给定此代码实现)可见的死锁类型之前(ATOM.E.EXCH
)发生。
(kernel1
是预测的一种形式,您可以在PTX参考here中阅读它,以了解它如何导致条件分支。)
注意:我认为这两个代码都很危险,可能存在缺陷。即使当前的测试似乎没有发现“工作”代码的问题,我认为未来的CUDA编译器可能会选择以不同的方式安排事情,并破坏该代码。甚至可能编译不同的机器架构可能会产生不同的代码。我认为像this这样的机制更加健壮,完全避免了内部争用。然而,即使是这样的机制也可能导致线程间块死锁。必须在特定的编程和使用限制下使用任何互斥锁。