Linux Day15:线程安全

2023-09-17 18:40:48

一、线程安全方法

线程安全即就是在多线程运行的时候,不论线程的调度顺序怎样,最终的结果都是
一样的、正确的。那么就说这些线程是安全的。
要保证线程安全需要做到:
1) 对线程同步,保证同一时刻只有一个线程访问临界资源。 (信号量,互斥锁,读写锁,条件变量)
2)在多线程中使用线程安全的函数(可重入函数),所谓线程安全的函数指的是:如果一个
函数能被多个线程同时调用且不发生竟态条件,则我们程它是线程安全的。

二、线程不安全举例

  strtok()函数

代码示例

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<pthread.h>
#include<string.h>
void *fun(void *arg)
{
    char arr[] = "1 2 3 4 5 6 7 8";
    char *p = strtok(arr, " ");
    while (p != NULL)
    {
        printf("p=%s\n", p);
        sleep(1);
        p = strtok(NULL, " ");
    }
}
int main()
{
    pthread_t id;
    pthread_create(&id, NULL, fun, NULL);
    char str[] = "a b c d e f g h";
    char *s = strtok(str, " ");
    while (s != NULL)
    {
        printf("s=%s\n", s);
        sleep(1);
        s = strtok(NULL, " ");
    }
    pthread_join(id, NULL);
    exit(0);
}

结果

分析

strtok()函数原理是他里面有一个全局/静态变量,只有一份用来存储读取字符串的下一个位置,注意只有一份,当我们有两个字符串的时候,这个时候就会出现下一个字符串位置窜取上一个字符串位置,就会出现打印一个a剩下的打印数字去。所以strtok()函数不能在多线程中使用,但是为了使用这个字符串分割功能,又出现了一个新的函数,strtok_r()函数

 char *strtok_r(char *str, const char *delim, char **saveptr);

这里我们专门申请一个指针用来存储当前字符串的下一个位置,防止出现混乱。

strtok_r()

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <string.h>
void *fun(void *arg)
{
    char arr[] = "1 2 3 4 5 6 7 8";
    char*ptr=NULL;
    char *p = strtok_r(arr, " ",&ptr);
    while (p != NULL)
    {
        printf("p=%s\n", p);
        sleep(1);
        p = strtok_r(NULL, " ",&ptr);
    }
}
int main()
{
    pthread_t id;
    pthread_create(&id, NULL, fun, NULL);
    char str[] = "a b c d e f g h";
    char*ptr=NULL;
    char *s = strtok_r(str, " ",&ptr);
    
    while (s != NULL)
    {
        printf("s=%s\n", s);
        sleep(1);
        s = strtok_r(NULL, " ",&ptr);
    }
    pthread_join(id, NULL);
    exit(0);
}

如果该函数实现的功能中有静态变量或者全局变量时,要考虑是否满足于多线程当中。

三、依次打印a,b,c每次只打印一个字符

实现原理

 定义三个信号量用于控制字符的打印,设置其初始值为1,0,0,因为刚开始要打印a,那么该信号量应该为1,这样就能让a的线程去p操作,进而让其余线程等待v操作的出现。有点类似于单循环链表的感觉。

代码实现

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <semaphore.h>
#include <pthread.h>
#include <unistd.h>
sem_t sem1;
sem_t sem2;
sem_t sem3;
void *funa(void *arg)
{
    for (int i = 0; i < 5; i++)
    {
        sem_wait(&sem1);
        printf("a");
        fflush(stdout);
        sleep(1);
        sem_post(&sem2);
    }
}
void *funb(void *arg)
{
    for (int i = 0; i < 5; i++)
    {
        sem_wait(&sem2);
        printf("b");
        fflush(stdout);
        sleep(1);
        sem_post(&sem3);
    }
}
void *func(void *arg)
{
    for (int i = 0; i < 5; i++)
    {
        sem_wait(&sem3);
        printf("c");
        fflush(stdout);
        sleep(1);
        sem_post(&sem1);
    }
}
int main()
{
    pthread_t id1, id2, id3;
    sem_init(&sem1, 0, 1);
    sem_init(&sem2, 0, 0);
    sem_init(&sem3, 0, 0);
    pthread_create(&id1, NULL, funa, NULL);
    pthread_create(&id2, NULL, funb, NULL);
    pthread_create(&id3, NULL, func, NULL);
    pthread_join(id1, NULL);
    pthread_join(id2, NULL);
    pthread_join(id3, NULL);

    
    sem_destroy(&sem1);
    sem_destroy(&sem2);
    sem_destroy(&sem3);
    exit(0);
}

四、读写锁

     场景:大部分情况都是读的线程,少部分写,但完全可以同时读,也不会改变临界资源,而如果全用互斥锁,则程序的性能就会下降

    区别:假如有多个读的线程,少部分写,在读的线程都加读锁,其它有读锁的线程也能读,没读锁的线程读不了,而加上写锁,其它读锁写锁都不能用,只能阻塞住。
 API函数


int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);//读锁
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);//销毁锁
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);//初始化锁
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);//写锁
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);//解锁

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>
#include <semaphore.h>
#include <string.h>
pthread_rwlock_t rwlock;
void *fun1(void *arg)
{
    for (int i = 0; i < 10; i++)
    {
        pthread_rwlock_rdlock(&rwlock);
        printf("fun1 read start\n");
        sleep(1);
        printf("fun1 read end\n");
        pthread_rwlock_unlock(&rwlock);
        sleep(1);
    }
}
void *fun2(void *arg)
{
    for (int i = 0; i < 10; i++)
    {
        pthread_rwlock_rdlock(&rwlock);
        printf("fun2 read start\n");
        sleep(3);
        printf("fun2 read end\n");
        pthread_rwlock_unlock(&rwlock);
        sleep(1);
    }
}
void* fun3(void *arg)
{
     for (int i = 0; i < 10; i++)
    {
        pthread_rwlock_wrlock(&rwlock);
        printf("------write  start\n");
        sleep(3);
        printf("-------write   end\n");
        pthread_rwlock_unlock(&rwlock);
        sleep(1);
    }
}
int main()
{
    pthread_rwlock_init(&rwlock, NULL);
    pthread_t id1, id2, id3;
    pthread_create(&id1, NULL, fun1, NULL);
    pthread_create(&id1, NULL, fun2, NULL);
    pthread_create(&id1, NULL, fun3, NULL);
    pthread_join(id1, NULL);
    pthread_join(id2, NULL);
    pthread_join(id3, NULL);
    pthread_rwlock_destroy(&rwlock);
    exit(0);
}

截取部分结果

在执行写锁的时候不能出现读取操作,同样在执行读取操作的时候不能有写的操作。

五、条件变量

条件变量提供了一种线程间的通知机制:当某个共享数据达到某个值的时候,唤醒等待这个共享数据的线程。

主程序给消息队列的线程发送消息,唤醒线程

int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *attr);

int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);

int pthread_cond_signal(pthread_cond_t *cond); //唤醒单个线程

int pthread_cond_broadcast(pthread_cond_t *cond); //唤醒所有等待的线程

int pthread_cond_destroy(pthread_cond_t *cond);

  

个人理解:就是你把线程放在wait等待队列中,阻塞住,然后当你满足个条件时,你就把放进去的线程从等待队列中拿出来用。而下面的代码案例中,则是创建两个线程,然后进入while循环不断的把自己放在wait队列中,当主函数满足条件,则随便从wait队列中拿一个线程用,用完了后然后自己又循环回来,把自己加入到wait等待队列中
 

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<pthread.h>
 
pthread_mutex_t mutex;
pthread_cond_t cond;
 
void* funa(void* arg)
{
	char* s = (char*)arg;

	while(1)
	{
        //加锁是为了能有个同步
		pthread_mutex_lock(&mutex);//如果这锁没人加,则能通过
		pthread_cond_wait(&cond,&mutex);//加入条件变量的等待队列 --阻塞 然后释放锁     等自己被唤醒时,加锁,看有没有人出或者入,再出队列
		pthread_mutex_unlock(&mutex);

		if (strncmp(s,"end",3) == 0)
		{
    		break;
		}

		printf("A thread read:%s",s);
	}
}

void* funb(void *arg)
{
	char* s = (char*)arg;

	while(1)
	{
		pthread_mutex_lock(&mutex);
		pthread_cond_wait(&cond,&mutex); //加入等待队列
		pthread_mutex_unlock(&mutex);

		if (strncmp(s,"end",3) == 0)
 		{
			break;
		}

		printf("B thread read:%s",s);
	}
}

int main()
{
	pthread_mutex_init(&mutex,NULL);
	pthread_cond_init(&cond,NULL);

	char buff[128] = {0};
	pthread_t ida,idb;
	pthread_create(&ida,NULL,funa,buff);
	pthread_create(&idb,NULL,funb,buff);

	while(1)
	{
		fgets(buff,128,stdin);
		if(strncmp(buff,"end",3) == 0 )
		{
			pthread_cond_broadcast(&cond);
			break;
		}
		else
		{
			pthread_cond_signal(&cond);
		}
	}

	pthread_join(ida,NULL);
	pthread_join(idb,NULL);
	pthread_mutex_destroy(&mutex);
	pthread_cond_destroy(&cond);

	exit(0);
}

更多推荐

Swift 5.5之Continuation

Continuation是Swift5.5中引入的一种新的编程模型,用于管理异步任务的结果。它允许您在异步任务完成后使用结果继续执行代码,可以与Async/Await一起使用,以简化异步编程。下面是使用Continuation的基本步骤:导入Continuation模块在使用Continuation之前,需要在代码文件

mysql知识大全

MySQL知识大全(2)MySqL基础为1—7(增删改查基础语法),MySQL进阶知识为8—11(约束、数据库设计、多表查询、事务)1、数据库相关概念以前我们做系统,数据持久化的存储采用的是文件存储。存储到文件中可以达到系统关闭数据不会丢失的效果,当然文件存储也有它的弊端。假设在文件中存储以下的数据:姓名年龄性别住址张

Python案例实现|租房网站数据表的处理与分析

在综合实战项目中,“北京链家网”租房数据的抓取任务已在上一篇完成,得到了数据表bj_lianJia.csv,如图1所示。该数据表包含ID、城区名(district)、街道名(street)、小区名(community)、楼层信息(floor)、有无电梯(lift)、面积(area)、房屋朝向(toward)、户型(mo

leetcode 10. 正则表达式匹配

2023.9.20感觉是目前做过dp题里最难的一题了...本题首要的就是需要理解题意,翻了评论区我才发现之前一直理解的题意是错的。我原来理解的“*匹配0次”是指:*直接消失,不会影响到前面的字符。但是*和前一个字符其实是连体的,所以说:*如果匹配0次,那么前一个字符就没了,消失了;*如果匹配1次,那么才相当于*消失了,

【Python】PySpark 数据处理 ① ( PySpark 简介 | Apache Spark 简介 | Spark 的 Python 语言版本 PySpark | Python 语言场景 )

文章目录一、PySpark简介1、ApacheSpark简介2、Spark的Python语言版本PySpark3、PySpark应用场景4、Python语言使用场景一、PySpark简介1、ApacheSpark简介Spark是Apache软件基金会顶级项目,是开源的分布式大数据处理框架,专门用于大规模数据处理,是一款

Windows11系统C盘用户文件夹下用户文件夹为中文,解决方案

说明:1.博主电脑为Windows11操作系统,亲测有效,修改后无任何影响,软件都可以正常运行!2.Windows10系统还不知道可不可行,因为Windows11的计算机管理中没有本地用户和组,博主在csdn上看到很多博主有发Windows10的解决方案,有通过“注册表”的,也有通过“本地用户和组”的,大家可以自己去小

OpenCV实现“蓝线挑战“特效

原理算法原理可以分为三个流程:1、将视频(图像)从(顶->底)或(左->右)逐行(列)扫描图像。2、将扫描完成的行(列)像素重新生成定格图像。3、使用原帧图像像素填充未扫描到的像素。图像扫描首先第一步,拿到一个视频(很多帧图像)可以简单的看成图像处理。我们需要将图像从顶到底逐行进行像素扫描,当然也可以从左到右逐列扫描,

在服务器上创建git仓库

1、在服务器上创建git仓库选择一个创建文件夹的地方,这个地方不会将源码存放在这里,只用于版本控制#创建一个专门放置git的文件夹,也可以叫其它名mkdirgit&&cdgit#创建自己项目的文件夹,文件夹后面要带.gitmkdirmy_object.git&&cdmy_object.git#初始化gitinit--b

Vue3中如何通过内嵌iframe传递参数与接收参数

前言Vue3是一种用于构建用户界面的JavaScript框架,它提供了很多方便的功能和工具来开发交互式的Web应用程序。其中一个常见的需求是在Vue应用程序中内嵌一个iframe,并且需要在两者之间传递参数。本文将介绍如何在Vue3中实现此功能,包括如何在Vue组件中内嵌iframe以及如何传递参数和接收参数。内嵌if

虹科产品 | HK-ATTO 光纤通道卡利用FC-NVMe 提升全闪存存储阵列性能

一、虹科ATTO光纤通道HBA随着对高速数据访问和低延迟存储解决方案的需求日益增长,虹科ATTO最新的光纤通道创新技术带来了改变游戏规则的突破。原生光纤通道和第二代FC-NVMe标准使虹科ATTO光纤通道HBA能够提供无与伦比的速度和效率,显著加快全球数据中心的全闪存阵列性能。原生光纤通道支持可确保数据密集型共享工作负

redis常见问题

Redis的数据结构有哪些?请简要描述它们的特点和应用场景。答:Redis支持的数据结构包括字符串(String)、哈希表(Hash)、列表(List)、集合(Set)、有序集合(SortedSet)等。字符串是最基本的数据类型,可以存储文本或二进制数据。哈希表适合存储对象形式的数据,方便单独读写字段。列表可以用于实现

热文推荐