线程是进程内部的一条执行序列,那么对于一个进程中多个线程之间,数据的共享是怎么具体操作的呢,我们对线程之间,全局数据。堆区数据、文件描述符会进行测试,以及线程之间的同步。
二、线程之间数据共享测试:
全局数据共享测试:
代码如下:
data_pthread.c文件
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <assert.h>
#include <string.h>
#include <pthread.h>
//全局变量
int data = 10;
void *fun(void *arg)
{
data = 20;
pthread_exit("baby");//功能:结束线程,参数是传递线程结束信息
}
void main()
{
pthread_t id;
int res = pthread_create(&id,NULL,fun,NULL);
assert(res == 0);
sleep(2);
printf("data == %d\n",data);
char *p = NULL;
pthread_join(id,(void *)&p);//等待线程结束,参数是接收线程结束信息
printf("%s\n",p);
}
Linux下gcc命令:
gcc -o data data_pthread.c -lpthread
运行:./data
结果:
我们一开始定义一个全局数据data = 10;然后函数线程进行修改,修改data= 20;通过主线程打印data的值。如果主线程打印是20,说明全局数据共享。由上面的运行结果得出,线程之间全局数据共享。
堆区数据共享测试:
代码如下:
heap_pthread.c文件
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <assert.h>
#include <string.h>
#include <pthread.h>
int *p = NULL;
void *fun(void *arg)
{
p = (int *)malloc(10);
*p = 20;
}
void main()
{
pthread_t id;
int res = pthread_create(&id,NULL,fun,NULL);
assert(res == 0);
sleep(1);
printf("p = %d\n",*p);
pthread_join(id,NULL);
}
Linux下gcc命令:
gcc -o heap heap_pthread.c -lpthread
运行: ./heap
结果:
我们一开始定义一个指针,然后在函数线程中进行开辟空间,并且修改值为20,通过主线程进行打印,结果是20的话说明线程之间在堆区数据也共享,看运行结果则说明线程之间在堆区数据是共享的。
文件描述符共享:
代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>
#include <fcntl.h>
int fd = 0;
//判断同一进程下多线程对文件描述读共享
void *fun(void *arg)
{
fd = open("lock.c",O_RDONLY);
assert(fd != -1);
if (fd == -1)
{
pthread_exit("0");
}
pthread_exit("1");
}
int main()
{
pthread_t id;
int res = pthread_create(&id,NULL,fun,NULL);
assert(res == 0);
sleep(2);
char buff[128] = {0};
while(1)
{
int n = read(fd,buff,127);
if (n <= 0)
{
break;
}
printf("%s",buff);
}
printf("\n");
}
Linux下gcc命令:
gcc -o file file_pcb.c -lpthread
运行:./file
结果:
从函数线程打开一个文件,而从主线程进行读取文件中的内容到屏幕,如果文件描述符共享则成功打印,由上述运行结果得出文件描述符在线程之间是共享的。
三、线程同步:访问临界资源控制
a. 用信号量实现线程同步:
信号量一个特殊类型的变量,它可以被增加或减少,但对其的关键访问被保证是原子操作,在一个多线程程序中也是这样,所以当一个程序中有两个(多个)线程试图改变一个信号量的值,系统保证所有的操作都一次进行。
信号量的函数都以sem_开头。今天说的是线程中使用的信号量函数;
头文件:
semaphore.h
sem_init函数:初始化函数
int sem_init(sem_t *sem, int pshared, unsigned int value);
第一个参数sem指向的是信号量对象,设置它的共享选项,给它一个初始的整数值;
第二个参数pshared参数控制信号量的类型,如果其值为0,就表示这个信号量是的当前进程的局部信号量,否则,这个信号量就可以在多个进程之间共享;
第三个参数value是给信号量的初值;
P操作:int sem_wait(sem_t *sem); -1操作;通过sem_wait使得其他线程阻塞
V操作:int sem_post(sem_t *sem); +1操作;
两个函数的参数都是一个指针,这个指针指向的对象是sem_init调用初始化的信号量。
销毁:
int sem_destroy(sem_t *sem);
参数是一个指向信号量的指针,并清理该信号量所拥有的所有资源。如果企图清理的信号量正被一些线程等待,就会收到一个错误。
返回值:以上这些函数成功都会返回0;
测试练习:
题目:主线程接收用户输入,函数线程统计输入的字符个数,当用户输入end的时候结束统计。
代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <assert.h>
#include <pthread.h>
#include <fcntl.h>
#include <semaphore.h>
sem_t sem1;
sem_t sem2;
void *work_thread(void *arg)
{
char *buff = (char *)arg;
while(1)
{
sem_wait(&sem1);
if(strncmp(buff, "end", 3) == 0)
{
break;
}
int i = 0, count = 0;
for(; buff[i] != 0; ++i)
{
count++;
}
printf("字符个数为: %d\n", count);
sem_post(&sem2);
}
}
void main()
{
char buff[128] = {0};
pthread_t id;
sem_init(&sem1, 0, 0);
sem_init(&sem2, 0, 1);
int res = pthread_create(&id, NULL, work_thread, (void *)buff);
assert(res == 0);
while(1)
{
sem_wait(&sem2);
memset(buff, 0, 128);
printf("请输入需要统计的字符: ");
fflush(stdout);
fgets(buff, 128, stdin);
buff[strlen(buff) - 1] = 0;
sem_post(&sem1);
if(strncmp(buff, "end", 3) == 0)
{
break;
}
}
}
Linux下gcc命令:
gcc -o sem sem_pthread.c -lpthread
运行:./sem
结果:
信号量sem1控制的是主线程给函数线程提供数据;
信号量sem2控制的是主线程得到函数线程的统计字符的结果,并继续由用户输入字符。
b.用互斥锁实现线程同步
理论:互斥量允许程序员锁住某个对象,使得每次只能有一个线程访问它。为了控制对关键代码的访问,必须在进入这段代码之前锁住一个互斥量,然后在完成操作后解锁它。
头文件:pthread.h
对象 pthread_mutex_t mutex;
初始化:int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr);
加锁:int pthread_mutex_lock(pthread_mutex_t *mutex);
解锁:int pthread_mutex_unlock(pthread_mutex_t *mutex);
销毁:int pthread_mutex_destroy(pthread_mutex_t *mutex);
参数:以上函数的参数mutex都是声明过的对象的指针。这个对象的类型为pthread_mutex_t 。
init的第二个参数是设置互斥量的属性。而属性控制着互斥量的行为。属性类型默认为fast。
设置属性的缺点:如果程序对一个加锁的互斥量再次调用加锁函数,这时候,程序就会阻塞,而又因为拥有互斥量的这个线程正是被阻塞的线程,所以互斥量永远也不会解锁了,程序也就进入了死锁的状态。解决方法:改变互斥量的属性,如果遇到上述情况就返回一个错误,或者让它递归操作,给同一个进程加多个锁,相应的后面执行相同数量的解锁操作。
平常我们会把第二个参数设置为 NULL传递给属性指针,也就是默认行为。
测试练习:
题目:主线程接收用户输入,函数线程统计输入的字符个数,当用户输入end的时候结束统计。
代码如下:
lock_pthread.c文件;
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <assert.h>
#include <pthread.h>
#include <fcntl.h>
#include <semaphore.h>
pthread_mutex_t mutex;
void *work_thread(void *arg)
{
char *buff = (char *)arg;
while(1)
{
pthread_mutex_lock(&mutex);
if(strncmp(buff, "end", 3) == 0)
{
break;
}
int i = 0, count = 0;
for(; buff[i] != 0; ++i)
{
count++;
}
printf("输入的字符个数为: %d\n", count);
pthread_mutex_unlock(&mutex);
sleep(1);
}
}
void main()
{
char buff[128] = {0};
pthread_t id;
int res = pthread_create(&id, NULL, work_thread, (void *)buff);
assert(res == 0);
pthread_mutex_init(&mutex,NULL);
while(1)
{
pthread_mutex_lock(&mutex);
memset(buff, 0, 128);
printf("请输入需要统计的字符: ");
fflush(stdout);
fgets(buff, 128, stdin);
buff[strlen(buff) - 1] = 0;
pthread_mutex_unlock(&mutex);
if(strncmp(buff, "end", 3) == 0)
{
break;
}
sleep(1);
}
pthread_join(id,NULL);
pthread_mutex_destroy(&mutex);
}
Linux下gcc命令:
gcc -o lock lock_pthread.c -lpthread
运行:./lock
结果:
在程序的开始我们声明了一个互斥量对象,然后在函数线程中进行初始化互斥量。主线程首先试图对互斥量加锁,。如果它已经锁住,这个调用将被阻塞直到它被释放为止,当用户字符输入完毕后,主线程会解锁,这时候,函数线程会进行字符统计,并加锁,防止主线程操作临界资源,当函数线程统计字符完成后,会进行解锁操作,这时候主线程才能继续让用户输入字符。也就是周期性地循环操作加锁,解锁,直到遇到用户输入end,结束程序。
c.条件变量解决线程同步
条件变量是线程可用的另一种同步机制。条件变量给多个线程提供了一个会合的场所。条件变量与互斥量一起使用时、允许进程以无竞争的方式等待特定的条件发生。
条件本身是由互斥量保护的。线程在改变条件状态前必须首先锁住互斥量,其他线程在获得互斥量之前不会察觉到这种改变,因为必须锁定互斥量以后才能计算条件。
条件变量使用之前必须首先进行初始化。
头文件:pthread.h
条件变量类型:pthread_cond_t;
初始化函数:
int pthread_cond_init(pthread_cond_t *restrict cond, pthread_condattr_t *restrict attr);
attr参数设置为NULL;
在释放底层的内存空间之前,使用pthread_mutex_destroy函数对条件变量进行去初始化:
条件变量销毁:
int pthread_cond_destroy(pthread_cond_t *cond);
pthread_cond_wait函数:
使用pthread_cond_wait 等待条件变为真;
int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
传递给pthread_cond_wait的互斥量对条件进行保护,调用者把锁住的互斥量传给函数。
当pthread_cond_wait返回时,互斥量再次被锁住。
用于通知线程条件已经满足的两个函数:
pthread_cond_signal函数:
唤醒等待该条件的某个线程;
int pthread_cond_signal(pthread_cond_t *cond);
pthread_cond_broadcast函数:
唤醒等待该条件的所有线程;
int pthread_cond_broadcast(pthread_cond_t *cond);
两个函数返回值:成功返回0;失败返回错误编号;
调用pthread_cond_signal或者pthread_cond_broadcast,也称为向线程或条件发送信号。必须注意一定要在改变条件状态以后再给线程发送信号。
四、线程安全------可重入函数
首先,编写以下代码:
文件:safe_pthread1.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <assert.h>
#include <pthread.h>
#include <string.h>
void *fun(void *arg)
{
char arr[] = "a b c d e f g";
char *p = strtok(arr," ");
while(p != NULL)
{
printf("fun: %s\n",p);
sleep(1);
p = strtok(NULL," ");
}
}
void main()
{
pthread_t id;
int res = pthread_create(&id ,NULL,fun,NULL);
assert(res == 0);
char buff[] = "1 2 3 4 5 6 7";
char *p = strtok(buff," ");
while(p != NULL)
{
printf("main : %s\n",p);
sleep(1);
p = strtok(NULL, " ");
}
}
Linux下gcc命令:
gcc -o safe1 safe_pthread1.c -lpthread
运行:./safe1
结果:
按照正常逻辑,应该是交替打印1,a,2,b,3,c,4,d,5,e,6,f,7,g。但是只打印了数字只有一个1,再的全是字母,这是为什么呢?
说到这里,就要提出线程安全的定义了,如果一个函数在同一时刻可以被多个线程安全地调用,就称该函数是安全的,而我们这里的strtok函数在POSIX.1中是不能保证线程安全的函数。所以它出现了这种多个控制线程在同一时间潜在的调用了strtok这个不安全的函数,使得出现意想不到的结果。那么关于这些不安全的函数还有哪些呢?
以上都是不能保证线程安全的函数;
那么怎么解决呢?
针对以上可能不安全的函数,操作系统对支持线程安全的这一特性时,对非线程安全函数提供了可替代的安全版本;如下图:
那么我们对以上代码进行修改:
代码如下:
文件:safe_pthread.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <assert.h>
#include <pthread.h>
#include <string.h>
void *fun(void *arg)
{
char arr[] = "a b c d e f g";
char *q = NULL;
char *p = strtok_r(arr," ",&q);
while(p != NULL)
{
printf("fun: %s\n",p);
sleep(1);
p = strtok_r(NULL," ",&q);
}
}
void main()
{
pthread_t id;
int res = pthread_create(&id ,NULL,fun,NULL);
assert(res == 0);
char buff[] = "1 2 3 4 5 6 7";
char *q = NULL;
char *p = strtok_r(buff," ",&q);
while(p != NULL)
{
printf("main : %s\n",p);
sleep(1);
p = strtok_r(NULL, " ", &q);
}
}
Linux下gcc命令:
gcc -o safe safe_pthread.c -lpthread
运行:./safe
结果:
以上使用正确的安全版本的strtok_r函数,所运行得到的结果才是我们想要的。