多线程数据竞争问题
2016-11-09 17:09
246 查看
摘要
在Redis的Bio代码中, 需要使用for循环创建两个服务线程, 并且把两个整数0 1作为参数传到线程执行的函数中. 这个问题涉及到在linux c中, 用for循环创建多个线程并传参数时会遇到的数据竞争问题. 本文给出该问题的分析, 几段相关的错误代码的分析, 并结合redis的BIO模块代码给出总结.第一种错误的写法
我们的目标是在一个for循环里面,调用pthread_create函数创建线程,并且把循环用到的整数i作为参数传递,希望用这个i作为线程的标志, 首先来看一段经典的错误代码:#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个数字。但是通过运行我们发现,打印出来的数字有重复的,其中一次的运行结果如下:
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的时候, 进行数据复制, 让每个传入的指针都指向一个不一样的位置, 这样就避免了数据竞争, 代码如下:#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数组定义成全局变量. 完成这步以后, 我们的示例代码就可以完成预期的功能了.
第一种正确的写法
经过上面的分析, 给出基于全局变量的第一种写法:#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; }
第二种正确的写法
上面的做法需要额外开辟数组, 比较麻烦, 一种更加简洁的方法如下:#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循环创建了两个线程, 并且也是将一个整型传入线程函数, 其相关代码如下.创建线程的代码
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; }
线程内部的处理
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
原始链接:yiwenshao.github.io/2016/11/09/多线程的数据竞争问题/
文章作者:Yiwen Shao
许可协议: Attribution-NonCommercial 4.0
转载请保留以上信息, 谢谢!
相关文章推荐
- OpenMP编程的数据竞争问题
- [ js ] 可否用多线程的思路,解决大数量数据的性能问题?
- 多线程下数据同步问题
- Java 多线程 死锁 隐性死锁 数据竞争 恶性数据竞争 错误解决深入分析 全方向举例
- Java 多线程 死锁 隐性死锁 数据竞争 恶性数据竞争 错误解决深入分析 全方向举例
- VC多线程解决有些项目中的数据存取问题
- android 多线程问题,意外加载多倍数据
- mybatis多线程插入数据时出现的问题
- [Python] Python 多线程 Envent 解决数据共享问题
- Java如何处理多线程的数据同步问题
- [ js ] 可否用多线程的思路,解决大数量数据的性能问题?
- Delphi中多线程用Synchronize实现VCL数据同步显示 解决在线程中操作控件出现问题
- 多线程竞争资源问题
- JAVA基础知识之java多线程时数据同步问题
- DataGridView多线程更新数据的问题的解决办法
- C++多线程编程学习一 [关于数据竞争问题]
- 多线程内数据长效保护的问题二
- 数据加载缓慢的问题,用了多线程老是出错,请高手指教。。。
- Java如何利用synchronized处理多线程的数据同步问题
- 多线程之解决数据错位问题