多线程共享变量会涉及到数据的安全问题。
验证测试程序如下,两个线程共同对一个(非全局)变量操作,根据初始打印可知地址一样,非全局。
#include <stdio.h>
#include <pthread.h>
static pthread_t thread_a_id;
static pthread_t thread_b_id;
void *printA(void *pA)
{
printf("%p!\n",pA);
int *p = pA;
while(1)
{
*p = 2;
if(5 == *p)
{
printf("=============\n");
}
}
return NULL;
}
void *changeA(void *pA)
{
printf("%p!\n",pA);
int *p = pA;
while(1){
*p = 5;
}
return NULL;
}
int main()
{
int a;
a = 1;
int iRet = 0;
iRet = pthread_create(&thread_a_id,NULL,printA,&a);
if(iRet != 0)
{
printf("create failed!\n");
}
iRet = pthread_create(&thread_b_id,NULL,changeA,&a);
if(iRet != 0)
{
printf("create failed!\n");
}
while(1)
{
sleep(5);
}
}
因为while(1)运转过快,难以人工分辨,所以设置遇到变量值不符合预期时打印即可。
运行结果:
=============
=============
=============
=============
=============
=============
证明线程间数据被互相篡改,不安全。
线程b就不打印了,肯定也会遇到a被改成2的情况。(为了演示更全面,下边的例子会把a和b线程改对等)
解决思路
我目前所用的,通常是数据加锁和变量不共享两种思路,但是数据不共享很局限,不能满足很多需求,比如多线程需要共同维护一个用户哈希表。
所以就需要给数据加锁。
手动加(实际上简陋、不可用的烂)锁:
#include <stdio.h>
#include <pthread.h>
static pthread_t thread_a_id;
static pthread_t thread_b_id;
static int i = 0;
void *printA(void *pA)
{
printf("%p!\n",pA);
int *p = pA;
while(1)
{
if(i == 0)
{
i = 1;//lock
*p = 2;
if(5 == *p)
{
printf("=============\n");
}
i = 0;//unlock
}
}
return NULL;
}
void *changeA(void *pA)
{
printf("%p!\n",pA);
int *p = pA;
while(1){
if(i == 0)
{
i = 1;//lock
*p = 5;
if(2 == *p)
{
printf("*************\n");
}
i = 0;//unlock
}
}
return NULL;
}
int main()
{
int a;
a = 1;
int iRet = 0;
iRet = pthread_create(&thread_a_id,NULL,printA,&a);
if(iRet != 0)
{
printf("create failed!\n");
}
iRet = pthread_create(&thread_b_id,NULL,changeA,&a);
if(iRet != 0)
{
printf("create failed!\n");
}
while(1)
{
sleep(5);
}
}
结果打印:
=============
*************
=============
*************
*************
*************
=============
*************
*************
*************
*************
=============
*************
=============
=============
*************
*************
=============
=============
*************
*************
=============
=============
*************
=============
=============
一塌糊涂,不管用。
这个程序证明了C语言的语句非原子操作,是可以分解成更多更细的指令的,不是一个真正意义上的原语,所以程序连死锁的机会都没有,各种乱跑。
PS:在所谓的lock操作i = 1;后加一个i等于0的判断,就可以知道i也是发生变化的。
while(1)
{
if(i == 0)
{
i = 1;//lock
if(0 == i)
{
printf("..............\n");
}
*p = 2;
if(5 == *p)
{
printf("=============\n");
}
i = 0;//unlock
}
}
最终方法
手动设的普通变量解决不了安全性问题,怎么办?其实,pthread库里是自带方法了。应该是线程锁吧,有指定的变量pthread_mutex_t和指定操作pthread_mutex_lock()、pthread_mutex_unlock()
#include <stdio.h>
#include <pthread.h>
static pthread_t thread_a_id;
static pthread_t thread_b_id;
static int i = 0;
static pthread_mutex_t sMux;
void *printA(void *pA)
{
printf("%p!\n",pA);
int *p = pA;
while(1)
{
pthread_mutex_lock(&(sMux));//lock
*p = 2;
if(5 == *p)
{
printf("=============\n");
}
pthread_mutex_unlock(&(sMux));//unlock
}
return NULL;
}
void *changeA(void *pA)
{
printf("%p!\n",pA);
int *p = pA;
while(1){
pthread_mutex_lock(&(sMux));//lock
*p = 5;
if(2 == *p)
{
printf("*************\n");
}
pthread_mutex_unlock(&(sMux));//unlock
}
return NULL;
}
int main()
{
int a;
a = 1;
int iRet = 0;
iRet = pthread_create(&thread_a_id,NULL,printA,&a);
if(iRet != 0)
{
printf("create failed!\n");
}
iRet = pthread_create(&thread_b_id,NULL,changeA,&a);
if(iRet != 0)
{
printf("create failed!\n");
}
while(1)
{
sleep(5);
}
}
运行结果
[root@jiaxun multi_thread]# ./a.out
0xbfa82be8!
0xbfa82be8!
不再打印多余信息,说明两个线程互相不干扰,不再有安全性问题。
PS:一个线程锁只能互斥保护一个临界资源,这个临界资源其实也不是固定的,全看lock在哪用,其实是一个锁同时只能锁一处资源。
很多不互斥使用的资源,如果用同一个锁,肯定就尴尬了。这就好比你上锁,把自己的锁锁到别人自行车上了,别人拿不了车。这是不想要的结果。
所以要给不同的临界资源陪不同的锁——多初始化一个锁变量,用那个变量去加锁解锁就好了。
测试代码:
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/syscall.h>
#define gettid() syscall(__NR_gettid)
pthread_t thread1;
pthread_t thread2;
pthread_mutex_t lock1;
pthread_mutex_t lock2;
void* pThreadFunc(void * pMutex)
{
//先锁
pthread_mutex_lock((pthread_mutex_t *)pMutex);
printf("the thread's id is %lu\n",gettid());
//再睡
printf("lock,sleep\n");
sleep(10);
printf("sleep over,unlock,exit!\n");
pthread_mutex_unlock((pthread_mutex_t *)pMutex);
return NULL;
}
int main(){
int iRet = 0;
void * status;
//初始化线程锁
pthread_mutex_init(&lock1,NULL);
//先多线程
iRet = pthread_create(&thread1, NULL, pThreadFunc, &lock1); //建立服务线程
if(iRet != 0)
{
return iRet;
}
iRet = pthread_create(&thread2, NULL, pThreadFunc, &lock2); //建立服务线程
if(iRet != 0)
{
return iRet;
}
pthread_join(thread1,&status);
pthread_join(thread2,&status);
}
结果:
# ./a.out
the thread's id is 9475
lock,sleep
the thread's id is 9476
lock,sleep
sleep over,unlock,exit!
sleep over,unlock,exit!
# ps -efL | grep ./a.out
root 9474 8507 9474 0 3 22:28 pts/1 00:00:00 ./a.out
root 9474 8507 9475 0 3 22:28 pts/1 00:00:00 ./a.out
root 9474 8507 9476 0 3 22:28 pts/1 00:00:00 ./a.out
root 9479 9447 9479 2 1 22:28 pts/2 00:00:00 grep
如果两个线程传同一个线程锁lock1,就会顺序执行:
# ./a.out
the thread's id is 9534
lock,sleep
sleep over,unlock,exit!
the thread's id is 9533
lock,sleep
sleep over,unlock,exit!
所以,只要
区分好那把锁锁哪辆车就行了。