多线程同步与安全

线程与进程的区别

进程:正在运行的程序,资源分配的最小单位。

线程:进程中的执行路径,调度的最小单位。

linux中线程的实现

linux实现线程的机制非常特殊,从内核的角度来说,它没有线程这个概念。linux把所有线程当作进程来实现。内核并没有定义特殊的调度算法或是定义特别的结构体来表征线程。相反,线程仅仅被视为一个与其他进程共享某些资源的进程。每个线程都有自己的PCB,所以在内核中,它看起来像是一个普通的进程(只是和其他进程共享某些资源,如地址空间)。

多线程

所需头文件:

#include<pthread.h>

接口函数:

int pthread_create( pthread_t *thread,const pthread_attr_t *attr,void*(*start_routine)(void*) ,void* arg):创建线程

thread:创建线程的id

attr:线程的属性信息,一般写NULL

start_routine:线程函数

arg:线程函数的参数

void pthread_exit( void* retval ):退出线程并返回退出信息

retval:退出信息

int pthread_join( pthread* thread,void **retval ):等待指定线程thread退出

thread:线程id

retval:接收thread退出时返回的指定信息

举例:

①主线程打印10次main,子线程打印10次thread

②主线程打印10次main,子线程打印5次thread

结论:子线程结束并不会影响到主线程的运行

③主线程打印5次main,子线程打印10次thread

结论:主线程结束会影响到子线程的运行

如何解决主线程结束导致子线程未运行完毕的问题?

解决方法:调用pthread_exit和pthread_join方法

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<assert.h>
#include<pthread.h>

void *fun(void *arg)  //线程函数
{
    int i=0;
    for(i;i<10;i++)
    {
        printf("thread\n");
        sleep(1);
    }
    //线程结束,并返回结束信息
    pthread_exit("子线程结束");
}

int main()
{
    pthread_t id;
    //创建线程
    pthread_create(&id,NULL,fun,NULL);
    int i=0;
    for(i;i<5;i++)
    {
        printf("main\n");
        sleep(1);
    }
    char *s=NULL;
    pthread_join(id,(void**)&s); //等待id线程结束,结束之前会阻塞
    printf("main over : %s\n",s);

    exit(0);
}

运行结果:

并发访问同一内存的问题

问题:当我们同时创建5个线程,每个线程都对int x = 0进行自加操作,并打印每次的值

预期结果:最后打印出5000

实际结果:(不一定每次都是4999,也可能是5000,也可能是4997...)

为什么会出现这样的结果:

如何解决线程不同步的问题,我们引入了以下4种方法来进行线程同步。

线程同步

信号量

所需头文件:

#include<semaphore.h>

接口函数:

int sem_init( sem_t *sem,int pshare,unsigned int value ):创建并初始化信号量

sem:信号量变量

pshare:0代表用于多线程同步,>0代表其他进程也能共享这一信号量

value:信号量初始值

int sem_wait( sem_t *sem ):p操作

sem:信号量变量

int sem_post( sem_t *sem ):v操作

sem:信号量变量

int sem_destory( sem_t *sem ):销毁信号量

sem:信号量变量

代码实现:使用信号量解决并发访问同一内存的问题(上述结果打印不是5000的问题)

运行结果:

互斥锁

在进入每个线程时加锁,当此线程运行结束时解锁,这样就可以做到每次只有一个线程访问同一个资源。

所需头文件:

#include<pthread.h>

接口函数:

int pthread_mutex_init( pthread_mutex_t *mutex,pthread_mutexattr_t *attr ):创建互斥锁

mutex:锁变量

attr:互斥锁属性,一般给NULL

int pthread_mutex_lock( pthread_mutex_t *mutex ):对互斥锁进行加锁

mutex:锁变量

int pthread_mutex_unlock( pthread_mutex_t *mutex ):对互斥锁进行解锁

mutex:锁变量

int pthread_mutex_destory( pthread_mutex_t *mutex ):销毁互斥锁

mutex:锁变量

代码实现:使用互斥锁解决并发访问同一内存的问题(上述结果打印不是5000的问题)

运行结果:

读写锁

在多线程访问同一块资源时,往往是读操作多,写操作少。并且在读操作时,其他线程也可以进行读操作(如果我们使用互斥锁,那么其他线程就不能进行读操作),但是当一线程进行写操作时,其他线程不能进行读操作。为了实现这样的情况,引入了读写锁。

所需头文件:

#include<pthread.h>

接口函数:

int pthread_rwlock_init( pthread_rwlock_t *rwlock,pthread_rwlockattr_t *attr ):创建并初始化读写锁

rwlock:读写锁变量

attr:读写锁属性设置(一般写NULL)

int pthread_rwlock_rdlock( pthread_rwlock_t *rwlock ):加读锁

rwlock:读写锁变量

int pthread_rwlock_wrlock( pthread_rwlock_t *rwlock ):加写锁

rwlock:读写锁变量

int pthread_rwlock_unlock( pthread_rwlock_t *rwlock ):解锁(读和写)

rwlock:读写锁变量

举例:创建4个线程,两个线程同时进行读操作,两个线程进行写操作(预期结果:读操作可以同时进行;在读时不能写,反之也是;写操作也不能同时操作)

代码实现:

运行结果:

条件变量

(注意:条件变量需要和互斥锁一起使用)

所需头文件

#include<pthread.h>

接口函数:

int pthread_cond_init( pthread_cond_t* cond,pthread_condattr_t* attr ):创建条件变量

cond:条件变量

attr:条件变量属性(一般给NULL)

int pthread_cond_wait( pthread_cond_t* cond,pthread_mutex_t* mutex ):将线程加入等待队列中阻塞

cond:条件变量

mutex:互斥锁(辅助条件变量,使其能正常进行)

注意:在将线程放入等待队列中时,需要使用互斥锁

原因:

wait方法原理:

例如此段代码:

在进入wait函数前先加锁(保证此时只有这个进程在进行入队列的操作)

调用wait函数时,在函数内部会先解锁,将此线程的信息保存在一个容器中,当线程被唤醒时会再加锁。

wait结束被唤醒时时我们需要再解锁一次,才能进行其他的操作

int pthread_cond_signal( pthread_cond_t* cond ):唤醒一个线程工作

cond:条件变量

int pthread_cond_broadcast( pthread_cond_t* cond ):唤醒所有线程工作

cond:条件变量

int pthread_cond_destory( pthread_cond_t* cond ):销毁条件变量

cond:条件变量

举例:我们创建2个线程,主线程负责向一块空间写入数据(写入完成输入回车时,唤醒线程(如果输入end:唤醒并结束所有线程;其他输入只唤醒某一个线程)),两个子线程负责读取此空间写入的数据。

代码实现:

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<assert.h>
#include<pthread.h>

pthread_cond_t cond;     //定义条件变量
pthread_mutex_t mutex;   //定义互斥锁
char buff[128]={0};


void* work_fun(void* arg)  //线程函数
{
    char* s=(char*)arg;
    while(1)
    {
        pthread_mutex_lock(&mutex);  //加锁
        pthread_cond_wait(&cond,&mutex); //线程加入等待队列
        pthread_mutex_unlock(&mutex);  //解锁

        if(strncmp(buff,"end",3) == 0)
        {
            printf("%s 结束\n",s);
            break;
        }

        printf("%s %s",s,buff);
    }
}

int main()
{
    pthread_cond_init(&cond,NULL);   //创建条件变量
    pthread_mutex_init(&mutex,NULL);  //创建互斥锁
    pthread_t id1,id2;
    //创建线程
    pthread_create(&id1,NULL,work_fun,"线程1:");
    pthread_create(&id2,NULL,work_fun,"线程2:");
    while(1)
   {
        fgets(buff,128,stdin);

        if(strncmp(buff,"end",3)  == 0 )
        {
            pthread_cond_broadcast(&cond); //end->唤醒所有线程
            break;
        }

        pthread_cond_signal(&cond);  //其他->唤醒某个线程
    }
    //等待线程结束
    pthread_join(id1,NULL);
    pthread_join(id2,NULL);
    pthread_mutex_destroy(&mutex);  //销毁互斥锁
    pthread_cond_destroy(&cond);    //销毁条件变量
    printf("main over\n");
    exit(0);
}

运行结果:

线程安全

概念:在多线程中,无论调度顺序是什么样,最后都能得到正确且一致的结果。

线程不安全的例子:我们创建一个线程,让主线程和子线程同时使用strtok去分割两个不同的数组,并打印出来

(预期结果:两个数组都被完全打印)

代码:

造成这样结果的原因:

结论:

strtok这个函数其本身就不能在多线程中使用,属于线程不安全函数(不可重入函数),而除了strtok还有很多函数都是线程不安全的

(可重入函数:在调用此函数未结束时,再调用此函数,依然可以得到正确的结果,那么就称之为可重入函数)

解决方法:

调用线程安全版的strtok_r函数:

比普通的strtok函数多了一个二级指针来保存上一次分割完的地址

代码:

运行结果:

线程安全总结:

如何确保线程安全:(1)线程同步[ 信号量,互斥锁,读写锁,条件变量 ](2)使用线程安全函数

多线程中执行fork

  1. 多线程中某个线程执行fork后,子进程中的线程个数?

结论:多线程中某个线程执行fork后,子进程中只会有执行fork的这一条线程

  1. 进程fork前有锁,fork后子进程中锁的状态?

举个例子:让线程先进性加锁(fork前先睡眠1秒),确保线程加锁且没有解锁时(加锁后睡眠5秒再解锁),主线程进行fork,并打印出加锁解锁状态

代码:

解决方法:

pthread_atfork函数

所需头文件:

#include<pthread.h>

函数定义:

int pthread_atfork( void (*prepare)(void),void (*parent)(void),void (*child)(void) ):确保在fork执行之前锁已经空闲没有其他人使用(确定锁的状态)

prepare:在fork前执行,用来获取父进程的锁(对其进行加锁操作)

parent:在fork后且在fork返回之前执行,用来释放prepare中的锁(对其进行解锁操作)

child:和parent一样,在程序中可以用同一个函数

用oprhread_atfork解决之前的问题:

代码:

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<pthread.h>
#include<sys/wait.h>

pthread_mutex_t mutex;

void prepare(void)
{
    pthread_mutex_lock(&mutex);
}

void after(void)
{
    pthread_mutex_unlock(&mutex);
}

void* fun(void* arg)
{
    printf("fun lock\n");
    pthread_mutex_lock(&mutex);
    sleep(5);
    pthread_mutex_unlock(&mutex);
    printf("fun unlock\n");
}

int main()
{
    pthread_t id;
    pthread_atfork(prepare,after,after);
    pthread_mutex_init(&mutex,NULL);
    pthread_create(&id,NULL,fun,NULL);
    sleep(1);  //确保线程已经加锁

    pid_t pid = fork();
    if(pid == 0)
    {
        printf("child start to lock\n");
        pthread_mutex_lock(&mutex);
        printf("child start to unlock\n");
        pthread_mutex_unlock(&mutex);
        exit(0);
    }

    wait(NULL);
    pthread_join(id,NULL);
    return 0;
}

运行结果:

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

逐梦的白鹰

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值