摘要
在Redis的Bio代码中, 需要使用for循环创建两个服务线程, 并且把两个整数0 1作为参数传到线程执行的函数中. 这个问题涉及到在linux c中, 用for循环创建多个线程并传参数时会遇到的数据竞争问题. 本文给出该问题的分析, 几段相关的错误代码的分析, 并结合redis的BIO模块代码给出总结.
第一种错误的写法
我们的目标是在一个for循环里面,调用pthread_create函数创建线程,并且把循环用到的整数i作为参数传递,希望用这个i作为线程的标志, 首先来看一段经典的错误代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| #include <stdio.h> #include <pthread.h> #define THREAD_NUM 16
void *thread_func(void *arg) { int v = *(int*)arg; printf("v = %d\n", v); return (void*)0; }
int main(int argc, const char *argv[]) { pthread_t pids[THREAD_NUM]; int i; for (i = 0; i < THREAD_NUM; i++) { pthread_create(&pids[i], NULL, thread_func, (void*)(&i)); } for (i = 0; i < THREAD_NUM; i++) { pthread_join(pids[i], NULL); } return 0; }
|
我们这段代码能创建16个线程,然后分别传i的地址作为参数,希望能够打印出0-15这16个数字。但是通过运行我们发现,打印出来的数字有重复的,其中一次的运行结果如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| v = 1 v = 6 v = 3 v = 4 v = 2 v = 8 v = 8 v = 9 v = 9 v = 10 v = 11 v = 12 v = 13 v = 14 v = 15 v = 0
|
可以看到, 8出现了好几次, 而有些数字没有出现. 这是因为, 在pthread_create这个函数执行的时候, 会把指向i的指针作为参数传递给线程调用的函数, 然后各个线程开始执行. 如果主线程的代码, 也就是我们的for循环执行比较快, 会出现如下的错误情况:
比如当前i=5, 使用pthread_create创建了线程5. 这时候, 我们期望这个线程能够从我们传的指针中取出5, 并且打印5. 但是如果主线程执行更快, 进入下一个循环, i变成了6, 此时上一个循环创建的线程才开始读取这个值,那么该线程读取的值就是6,而不是我们期望的5,这就出现了错误.
上面的错误原因在于,我们创建一个线程的时候,线程的代码什么时候开始执行不受我们的控制.
第二种错误的写法
出现上面的问题的原因在于,主线程会不断更新i的值,而i这个值会被多个线程共享, 但是多个线程何时读取这个共享的i, 是不确定的. 为了解决这个问题, 直观的想法就是, 传递i的时候, 进行数据复制, 让每个传入的指针都指向一个不一样的位置, 这样就避免了数据竞争, 代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| #include<pthread.h> #include<unistd.h> #include<stdio.h> #define THREAD_NUM 16
void *thread_func(void *arg) { int v = *(int*)arg; printf("v = %d\n", v); return (void*)0; }
void init_job() { pthread_t pids[THREAD_NUM]; int index[THREAD_NUM]; for(int i=0;i<THREAD_NUM;i++){ index[i] = i; pthread_create(&pids[i],NULL,thread_func,(void*)(&index[i])); } }
int main(){ init_job(); sleep(1); return 0; }
|
这段代码避免了竞争, 但是依然不能打印0-15这16个值, 会出现重复值, 甚至一些未定义的值. 这个错误的原因在于,init_job函数执行完了以后, index数组会被收回, 这样我们传递的指针就会指向非法的位置, 当然就不能打印正确的值了. 解决这个问题的方法也很简单, 就是把index数组定义成全局变量. 完成这步以后, 我们的示例代码就可以完成预期的功能了.
第一种正确的写法
经过上面的分析, 给出基于全局变量的第一种写法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| #include<pthread.h> #include<unistd.h> #include<stdio.h> #define THREAD_NUM 16 int index[THREAD_NUM]; void *thread_func(void *arg) { int v = *(int*)arg; printf("v = %d\n", v); return (void*)0; } void init_job() { pthread_t pids[THREAD_NUM]; for(int i=0;i<THREAD_NUM;i++){ index[i] = i; pthread_create(&pids[i],NULL,thread_func,(void*)(&index[i])); } } int main(){ init_job(); sleep(1); return 0; }
|
第二种正确的写法
上面的做法需要额外开辟数组, 比较麻烦, 一种更加简洁的方法如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| #include <stdio.h> #include <pthread.h> #define THREAD_NUM 16
void *thread_func(void *arg) { int v = (int)arg; printf("v = %d\n", v); return (void*)0;}
int main(int argc, const char *argv[]) { pthread_t pids[THREAD_NUM]; int i; for (i = 0; i < THREAD_NUM; i++) { pthread_create(&pids[i], NULL, thread_func, (void*)(unsigned long)(i)); } for (i = 0; i < THREAD_NUM; i++) { pthread_join(pids[i], NULL); } return 0; }
|
我们传入的不是一个指向i的指针, 而是直接传i. 以64位系统为例, i是一个int型且为正数, 占用4Byte.void* 占用8byte. 所以先转化成unsigned long 类型, 然后强制转化成void*类型. 在函数传递参数的时候, 直接复制这个指针类型的值. 这个void*本身并不指向一个有效的位置, 但是其值和原始的整数i是一致的. 所以在函数thread_func中,使用int v = (int)arg就可以获得i类型. 由于这是一个值的复制而不是指针, 就不存在数据竞争的问题.
另外, 在c++中, 上面的写法可以修改成int v = (uintptr_t)arg. 这是一个和指针大小相同的unsigned int类型. 或者改成int v = static_cast(reinterpret_cast(arg)).
Redis Bio的做法
Ridis的bio中需要有两个后台线程处理任务, 其用for循环创建了两个线程, 并且也是将一个整型传入线程函数, 其相关代码如下.
创建线程的代码
1 2 3 4 5 6 7 8 9
| for (j = 0; j < BIO_NUM_OPS; j++) { void *arg = (void*)(unsigned long) j; if (pthread_create(&thread,&attr,bioProcessBackgroundJobs,arg) != 0) { serverLog(LL_WARNING,"Fatal: Can't initialize Background Jobs."); exit(1); } bio_threads[j] = thread; }
|
线程内部的处理
1 2 3 4 5 6
| void *bioProcessBackgroundJobs(void *arg) { struct bio_job *job; unsigned long type = (unsigned long) arg; sigset_t sigset; ...... }
|
可以看到, Redis的Bio中使用了上述的第二种解决方案。
总结
使用for循环结合pthread_create创建多个线程, 并且传递循环中使用的变量i作为参数的时候,会遇到数据竞争的问题, 本文介绍了问题出现的原因, 以及两种解决方案. 一种方法是依然传递指针, 并是做数据的拷贝, 使得多线程没有共享数据; 另一种是直接利用指针的空间来传递整数, 把指针当成整数来用,而不传递指针. 其中第二种方法在Redis的Bio代码中得到使用.
相关文献
[1] stackoverflow/questions/39117674/
[2] Begin linux programming 4th edithon NeilMatthew chapter 12
[3] Redis 官网
[4] stackoverflow/questions/332030