Unix\Linux多线程复健(二)线程同步

news2025/1/12 11:57:55

线程同步

并非让线程并行,而是有先后的顺序执行,当有一个线程对内存操作时,其他线程不可以对这个内存地址操作

线程之间的分工合作

线程的优势之一:能够通过全局变量共享信息

临界区:访问某一共享资源的代码片段,此段代码应为原子操作

全局变量在共享内存中

一个进程中有多个线程,共享全局变量,这是一件既有用又有些危险的特性

需要线程同步来维护临界资源的安全性

下面的程序中两个线程堆全局变量number进行增加操作,因异步而发生错误

#include<unistd.h>
#include<string.h>
#include<pthread.h>
#include<stdio.h>
#include<stdlib.h>
#define MAX 50
int number;
void* funcA(void* arg){
    for(int i = 0;i<MAX;i++){
        int cur = number;
        cur++;
        usleep(10);
        number = cur;
        printf("thread A,id = %lu,number = %d\n",pthread_self(),number);
    }
    return NULL;
}

void* funcB(void* arg){
    for(int i = 0;i<MAX;i++){
        int cur = number;
        cur++;
        number = cur;
        printf("thread B,id = %lu,number = %d\n",pthread_self(),number);
        usleep(5);
    }
    return NULL;
}
int main()
{
    
    pthread_t t1,t2;
    pthread_create(&t1,NULL,funcA,NULL);
    pthread_create(&t2,NULL,funcB,NULL);
    pthread_join(t1,NULL);
    pthread_join(t2,NULL);
    return 0;
}
//结果多数情况并非100

由于物理内存,缓存,cpu的读写,A和B拿到的不总是最新的数据

A在失去时间片被挂起前没有将数据更新到物理内存,则B获得时间片后获取的数据为旧数据

image

使用线程同步,保证数据写回物理内存,其他线程才会有资格执行

同步的方法:

对共享资源加锁(一般为全局数据区变量或堆区)这类共享资源又叫临界资源

四种:互斥锁,读写锁,条件变量,信号量

互斥锁

互斥锁确保同时仅有一个线程可以访问某共享资源,实现对任意共享资源的原子访问

互斥锁锁定一个代码块,使得线程之间顺序处理(串行)

互斥锁状态:(1)locked (2)unlocked

任何时候至多一个线程可以锁定该互斥锁,只有锁的所有者才能解锁

//互斥锁
pthread_mutex_t mutex;
//锁对象中保存了锁的状态信息,若为锁定状态则还记录给这把锁加锁的线程id,一个互斥锁变量只能被一个线程锁定,一般一个共享资源对应一把互斥锁
//互斥锁操作:(1)初始化
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
           const pthread_mutexattr_t *restrict attr);
//要想对mutex指针指向的内存进行操作,加restrict关键字来修饰指针,其他指针不能访问,unique,attr为互斥锁的属性
//(2)释放互斥锁资源
int pthread_mutex_destroy(pthread_mutex_t *mutex);
//(3)加锁解锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
//首先会判断参数 mutex 互斥锁中的状态是不是锁定状态,没上锁则加锁成功且记录线程,如果已锁则线程会阻塞在这把锁上
int pthread_mutex_trylock(pthread_mutex_t *mutex);
//如果已锁,则调用此函数加锁的线程不会被阻塞,失败直接返回错误号
int pthread_mutex_unlock(pthread_mutex_t *mutex);
//加锁解锁的应为同一线程
//修改之前的计数程序
#define MAX 50
int number;
pthread_mutex_t mutex;

void* funcA(void* arg){
    for(int i = 0;i<MAX;i++){
        //临界区在满足条件下越小越好
        pthread_mutex_lock(&mutex);
        int cur = number;
        cur++;
        usleep(10);
        number = cur;
        printf("thread A,id = %lu,number = %d\n",pthread_self(),number);
        pthread_mutex_unlock(&mutex);
    }
    return NULL;
}

void* funcB(void* arg){
    for(int i = 0;i<MAX;i++){
        pthread_mutex_lock(&mutex);
        int cur = number;
        cur++;
        number = cur;
        printf("thread B,id = %lu,number = %d\n",pthread_self(),number);
        pthread_mutex_unlock(&mutex);
    }
    return NULL;
}
int main()
{
    pthread_t t1,t2;
    pthread_mutex_init(&mutex,NULL);
    pthread_create(&t1,NULL,funcA,NULL);
    pthread_create(&t2,NULL,funcB,NULL);
    pthread_join(t1,NULL);
    pthread_join(t2,NULL);
    pthread_mutex_destroy(&mutex);
    return 0;
}

死锁

使用互斥锁可能会带来死锁

死锁使一个或多个线程被挂起(阻塞),无法继续执行

产生死锁的几种情况:
(1)在一个线程中对一个已经加锁的普通锁再次加锁

lock();
lock();//上面已经加了锁,这里阻塞,所以也无法释放锁

(2)多个线程按着不同顺序来申请多个互斥锁,容易产生死锁

(3)忘记释放锁

实例:

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<pthread.h>
int a = 0;
int b = 0;
pthread_mutex_t mutex_a;
pthread_mutex_t mutex_b;

void * thread(void* arg){
    pthread_mutex_lock(&mutex_a);
    printf("child thread,got mutex_a,waiting for mutex_b\n");
    sleep(5);//这里休眠5秒目的是让main thread可以获得锁
    ++a;
    pthread_mutex_lock(&mutex_b);
    a+=b++;
    pthread_mutex_unlock(&mutex_b);
    pthread_mutex_unlock(&mutex_a);
    pthread_exit(NULL);
}

int main()
{
    //main thread
    pthread_t id;
    pthread_mutex_init(&mutex_a,NULL);
    pthread_mutex_init(&mutex_b,NULL);
    pthread_create(&id,NULL,thread,NULL);
    pthread_mutex_lock(&mutex_b);
    printf("main thread,got mutex_b,waiting for mutex_a\n");
    sleep(5);//和子线程中的sleep同理
    ++b;
    pthread_mutex_lock(&mutex_a);
    b+=a++;
    pthread_mutex_unlock(&mutex_b);
    pthread_mutex_unlock(&mutex_a);
    pthread_join(id,NULL);
    pthread_mutex_destroy(&mutex_a);
    pthread_mutex_destroy(&mutex_b);
    return 0;
}

线程A锁住了资源1,现在想去给资源2加锁,而这时线程B锁住了资源2,想去给资源1加锁

造成循环等待,死锁

image

如图所示,蚌住了

  1. 加锁使用trylock
  2. 顺序访问共享资源
  3. 引入死锁检测方案(第三方库)
  4. 检查代码避免多次加锁

读者/写者问题,读写锁

读写锁可以提高读写操作的效率,如果使用互斥锁则读操作之间是串行的,用读写锁的读操作是并行的

特点:读写互斥,写优先级更高,读读并行,写写互斥

多个线程有读有写,且读操作较多,使用读写锁才更优秀

pthread_rwlock_t rwlock;//会记录锁的状态,锁定的是读还是写,哪个线程将此锁锁定
// 初始化读写锁
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,
           const pthread_rwlockattr_t *restrict attr);
// 释放读写锁
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
//成功返回0,失败返回错误号
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
//此函数锁定读操作若读写锁已锁定读,依然可以用此函数加锁但是锁定了写再调用就会阻塞
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
//锁定写操作,类似互斥锁
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
// 失败不会阻塞当前线程, 直接返回错误号
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
//解锁:
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

示例:

#include<unistd.h>
#include<string.h>
#include<pthread.h>
#include<stdio.h>
#include<stdlib.h>
#define MAX 50
int number;
pthread_rwlock_t rwlock;

void* read_func(void* arg){
    for(int i = 0;i<MAX;i++){
        //临界区在满足条件下越小越好
        pthread_rwlock_rdlock(&rwlock);
        //读操作
        printf("thread A,id = %lu,number = %d\n",pthread_self(),number);
        pthread_rwlock_unlock(&rwlock);
        usleep(rand()%5);
    }
    return NULL;
}

void* write_func(void* arg){
    for(int i = 0;i<MAX;i++){
        pthread_rwlock_wrlock(&rwlock);
        int cur = number;
        cur++;
        number = cur;
        printf("thread B,id = %lu,number = %d\n",pthread_self(),number);
        pthread_rwlock_unlock(&rwlock);
        usleep(5);
    }
    return NULL;
}
int main()
{
    pthread_t t_read[5],t_write[3];
    pthread_rwlock_init(&rwlock,NULL);
    for(int i = 0;i<5;i++){
        pthread_create(&t_read[i],NULL,read_func,NULL);
    }
    for(int i = 0;i<3;i++){
        pthread_create(&t_write[i],NULL,write_func,NULL);
    }
    
    for(int i = 0;i<5;i++){
        pthread_join(t_read[i],NULL);
    }
    for(int i = 0;i<3;i++){
         pthread_join(t_write[i],NULL);
    }
    pthread_rwlock_destroy(&rwlock);
    return 0;
}

增加结果有序且正确

条件变量

有些情况不能实现线程同步,和互斥锁配合使用

条件变量只有在满足指定的条件下才会阻塞线程,不满足条件则多个线程可以同时进入临界区

pthread_cond_t cond;
//阻塞多个线程,要存储多个线程的id
// 初始化
int pthread_cond_init(pthread_cond_t *restrict cond,
      const pthread_condattr_t *restrict attr);
// 销毁      
int pthread_cond_destroy(pthread_cond_t *cond);
//线程阻塞
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
//互斥锁用于同步,条件变量用于阻塞
// 将线程阻塞一定的时间长度, 时间到达后解除阻塞
//在阻塞线程时候,如果线程已经对互斥锁 mutex 上锁,那么会将这把锁打开,这样做是为了避免死锁
//当线程解除阻塞的时候,函数内部会帮助这个线程再次将这个 mutex 互斥锁锁上,继续向下访问临界区
int pthread_cond_timedwait(pthread_cond_t *restrict cond,
           pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);
//秒和纳秒
struct timespec {
	time_t tv_sec;
	long   tv_nsec;
};
//体中记录的时间是从1970.1.1到某个时间点的时间
time_t time(time_t * timer)
//函数返回从TC1970-1-1 0:0:0开始到现在的秒数

从现在开始到将来xx秒:

struct timespec tm;
    tm.tv_nsec = 0;
    tm.tv_sec = time(NULL) + 100;
// 唤醒阻塞在条件变量上的线程, 至少有一个被解除阻塞,个数不定
int pthread_cond_signal(pthread_cond_t *cond);
// 唤醒阻塞在条件变量上的线程, 被阻塞的线程全部解除阻塞
int pthread_cond_broadcast(pthread_cond_t *cond);

生产者消费者问题

系统中有一组生产者进程和一组消费者进程,生产者进程每次生产一个产品放入缓冲区,消费者进程每次从缓冲区中取出一个产品并使用。
生产者、消费者共享一个初始为空、大小为n的缓冲区。
只有缓冲区没满时,生产者才能吧产品放入缓冲区,否则必须等待。
只有缓冲区不为空时,消费者才能从中取出产品,否则必须等待。
缓冲区是临界资源,各进程必须互斥地访问。

生产者消费者协作,互相唤醒

#include<iostream>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
#include<pthread.h>
#include<fcntl.h>
#include<list>
#include<forward_list>
using namespace std;

struct Node{
    int num;
};
forward_list<Node>linkedlist;
void* produ(void* arg){
    while(1){
        sleep(1);
        Node newnode{rand()%1000};
        linkedlist.push_front(newnode);
        printf("add node,number = %d,tid:%ld\n",newnode.num,pthread_self());
    }
}   
void* customer(void* arg){
    while(1){
        sleep(1);
        Node newnode = linkedlist.front();
        linkedlist.pop_front();
        printf("del node,number = %d,tid:%ld\n",newnode.num,pthread_self());
    }
}



int main()
{
    pthread_t pro[5],cus[5];
    for(int i = 0;i<5;i++){
        pthread_create(&pro[i],nullptr,produ,nullptr);
        pthread_create(&cus[i],nullptr,customer,nullptr);
    }

    for(int i = 0;i<5;i++){
        pthread_join(pro[i],nullptr);
        pthread_join(cus[i],nullptr);  
    }
    while(1){

    }
    return 0;
}

image

加锁:

多个线程向链表添加节点应互斥进行,取走节点也应互斥进行

所以应对生产者临界区,消费者临界区加锁

链表的生产没有上限,但是消费有,所以若为空则阻塞消费者线程

生产者添加了节点则通知消费者解除阻塞

pthread_mutex_t mutex;
pthread_cond_t cond;

struct Node{
    int num;
    struct Node* next;
};
struct Node* head;//头节点
void* func_producer(void* arg){//添加节点
    while(1){
        pthread_mutex_lock(&mutex);
        struct Node* newnode = (struct Node*)malloc(sizeof(struct Node));
        newnode->num = rand()%1000;
        newnode->next = head;
        head = newnode;
        printf("生产者,id:%lu, number:%d\n",pthread_self(),newnode->num);
        pthread_mutex_unlock(&mutex);
        pthread_cond_broadcast(&cond);
        sleep(rand()%3);
    }
    return NULL;
}
void* func_consumer(void* arg){//添加节点
    while(1){
        pthread_mutex_lock(&mutex);
        while(head == NULL){//链表为空
            //阻塞消费者
            pthread_cond_wait(&cond,&mutex);
        }
        struct Node* node = head;
        printf("生产者,id:%lu, number:%d\n",pthread_self(),node->num);
        head = head->next;
        free(node);
        pthread_mutex_unlock(&mutex);
        sleep(rand()%3);
    }
    return NULL;
}
int main()
{
    pthread_mutex_init(&mutex,NULL);
    pthread_cond_init(&cond,NULL);
    pthread_t t1[5],t2[5];
    for(int i = 0;i<5;i++){
        pthread_create(&t1[i],NULL,func_producer,NULL);
    }
    for(int i = 0;i<5;i++){
        pthread_create(&t2[i],NULL,func_consumer,NULL);
    }
    for(int i = 0;i<5;i++){
        pthread_join(t1[i],NULL);
    }
    for(int i = 0;i<5;i++){
        pthread_join(t2[i],NULL);
    }

    pthread_mutex_destroy(&mutex);
    pthread_cond_destroy(&cond);
}

pthread_cond_wait在函数内部会自动的把互斥锁打开,其他线程则可以抢到这把互斥锁,生产者才能生产,生产1个后唤醒多个消费者,消费者又执行while(head==NULL)pthread_cond_wait

若唤醒多个阻塞在wait的线程,则多个线程会抢占这把互斥锁,没抢到的仍然阻塞

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/179206.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【JavaEE初阶】第六节.多线程 (基础篇 )线程安全问题(下篇)

前言 一、内存可见性 二、内存可见性的解决办法 —— volatile关键字 三、wait 和notify 关键字 3.1 wait() 方法 3.2 notify() 方法 3.3 notify All() 方法 3.4 wait 和 sleep 的对比 总结 前言 本节内容接上小节有关线程安全问题&#xff1b;本节内容我们将介绍有关…

CUDA编程笔记(6)

文章目录前言全局内存的访问模式合并访问和非合并访问使用全局内存进行矩阵转置矩阵复制矩阵转置总结前言 全局内存的合理使用 全局内存的访问模式 合并访问和非合并访问 合并访问指的是一个线程束&#xff08;同一个线程块中相邻的wrapSize个线程。现在GPU的内建变量wrapSi…

Linux系统之网络客户端工具

Linux系统之网络客户端工具一、Links工具1.Links工具介绍2.安装Links软件3.Links工具的使用4.打印网页源码输出5.打印url版本到标准格式输出二、wget工具1.wget工具介绍2.安装wget软件3.wget工具的使用三、curl工具1.curl工具的介绍2.curl的常用参数3.curl的基本使用四、scp工具…

机器学习(二)--NumPy

本篇文章介绍了一些Numpy的基础操作。NumPy 是Python语言的一个扩充程序库。支持高级大量的维度数组与矩阵运算&#xff0c;此外也针对数组运算提供大量的数学函数库。&#x1f4d9;参考&#xff1a;NumPy 数据类型 | 菜鸟教程 (runoob.com)1.Numpy ndarray对象Numpy最重要的一…

Introduction to Multi-Armed Bandits——04 Thompson Sampling[2]

Introduction to Multi-Armed Bandits——04 Thompson Sampling[2] 参考资料 Russo D J, Van Roy B, Kazerouni A, et al. A tutorial on thompson sampling[J]. Foundations and Trends in Machine Learning, 2018, 11(1): 1-96. ts_tutorial 项目代码地址: https://githu…

蓝桥杯刷题014——求阶乘(二分法)

求阶乘 蓝桥杯2022省赛题目 问题描述 满足 N ! 的末尾恰好有 K 个 0 的最小的 N 是多少? 如果这样的 N 不存在输出 −1 。 输入格式 一个整数 K 。 输出格式 一个整数代表答案。 样例输入 2样例输出 10评测用例规模与约定 对于 30% 的数据, 1≤K≤10^6. 对于 100% 的数据, …

新瑞鹏冲刺上市:持续亏损,旗下宠物医院屡被罚,彭永鹤为董事长

家门口的宠物医院所属集团也要上市了。 1月24日&#xff0c;新瑞鹏宠物医疗集团有限公司&#xff08;New Ruipeng Pet Group Inc.&#xff0c;下称“新瑞鹏”或“新瑞鹏集团”&#xff09;在美国证监会&#xff08;SEC&#xff09;公开提交招股书&#xff0c;准备在美国纳斯达…

LabVIEW什么时候需要实时系统

LabVIEW什么时候需要实时系统实时计算系统能够非常可靠地执行具有非常具体时序要求的程序&#xff0c;这对于许多科学和工程项目来说都很重要。构建实时系统所需的关键组件是实时操作系统&#xff08;RTOS&#xff09;。精确计时对于许多工程师和科学家来说&#xff0c;在安装了…

C 语言零基础入门教程(十)

C 作用域规则 任何一种编程中&#xff0c;作用域是程序中定义的变量所存在的区域&#xff0c;超过该区域变量就不能被访问。C 语言中有三个地方可以声明变量&#xff1a; 1、函数或块内部的局部变量 2、在所有函数外部的全局变量 3、在形式参数的函数参数定义中 让我们来看看什…

返回值的理解

前言 我们写的函数是怎么返回的&#xff0c;该如何返回一个临时变量&#xff0c;临时变量不是出栈就销毁了吗&#xff0c;为什么可以传递给调用方&#xff1f;返回对象的大小对使用的方式有影响吗&#xff1f;本文将带你探究这些问题&#xff0c;阅读本文需要对函数栈帧有一定…

Win10+GTX3060+Python+PyTorch+Tensorflow安装

本文是个备忘录&#xff0c;是折腾半个下午的成果&#xff0c;记下来免得忘记了。 0. 安装Win10&#xff0c;安装显卡驱动程序。 1. 弄清楚目前版本的PyTorch和Tensorflow支持哪个版本的Python。截至本文编写时&#xff0c;PyTorch需要Python的3.7~3.9&#xff0c;Tensorflow…

【NI Multisim 14.0虚拟仪器设计——放置虚拟仪器仪表(字发生器)】

目录 序言 &#x1f34d;放置虚拟仪器仪表 &#x1f349;字发生器 &#xff08;1&#xff09;“控件”选项组 &#xff08;2&#xff09;“显示”选项组 &#xff08;3&#xff09;“触发”选项组 &#xff08;4&#xff09;“频率”选项组 &#xff08;5&#xff09;字符…

CSS 艺术之暗系魔幻卡牌

CSS 艺术之暗系魔幻卡牌参考描述效果支线HTML图片主线去除元素的部分默认属性定义 CSS 变量body#card自定义属性定义动画#card::before#card::afterimg代码总汇参考 项目描述MDNWeb 文档搜索引擎Bing 描述 项目描述Edge109.0.1518.61 (正式版本) (64 位) 效果 注&#xff1a;…

DaVinci:HDR 调色

调色页面&#xff1a;HDR 调色Color&#xff1a;HDR GradeHDR 调色 HDR Grade调板不仅可用于 HDR 视频的调色&#xff0c; 也可用于 SDR 视频。其调色功能与标准色轮类似&#xff0c;但能调整的区域却要细致很多&#xff0c;同时&#xff0c;它还是可感知色彩空间的工具。高动态…

41.Isaac教程--使用DOPE进行3D物体姿态估计

使用DOPE进行3D物体姿态估计 ISAAC教程合集地址: https://blog.csdn.net/kunhe0512/category_12163211.html 深度对象姿态估计 (DOPE:Deep Object Pose Estimation) 从单个 RGB 图像执行已知对象的检测和 3D 姿态估计。 它使用深度学习方法来预测对象 3D 边界框的角点和质心的…

【数据结构】单调栈、单调队列

单调栈 单调栈 单调 栈 模拟单调递增栈的操作&#xff1a; 如果栈为 空 或者栈顶元素 大于 入栈元素&#xff0c;则入栈&#xff1b;否则&#xff0c;入栈则会破坏栈内元素的单调性&#xff0c;则需要将不满足条 件的栈顶元素全部弹出后&#xff0c;将入栈元素入栈。 单调…

研究分析如何设计高并发下的弹幕系统

一、需求背景为了更好的支持直播业务&#xff0c;产品设计为直播业务增加弹幕功能,但是最初的弹幕设计使用效果并不理想&#xff0c;经常出现卡顿、弹幕偏少等需要解决的问题。二、问题分析按照背景来分析&#xff0c;系统主要面临以下问题&#xff1a;带宽压力&#xff1b;弱网…

[基础]qml基础控件

TextText元素可以显示纯文本或者富文本(使用HTML标记修饰的文本)。它有font,text,color,elide,textFormat,wrapMode,horizontalAlignment,verticalAlignment等属性。主要看下clip&#xff0c;elide&#xff0c;textFormat&#xff0c;warpMode属性clipText 项目是可以设置宽度的…

Apache Spark 机器学习 特征抽取 4-2

Word2Vec 单词向量化是一个估算器&#xff0c;将文档转换成一个按照固定顺序排列的单词序列&#xff0c;然后&#xff0c;训练成一个Word2VecModel单词向量化的模型&#xff0c;该模型将每个单词映射成一个唯一性的、固定大小的向量集&#xff0c;对每个文档的所有单词进行平均…

【数据结构和算法】实现线性表中的静态、动态顺序表

本文是数据结构的开篇&#xff0c;上文学习了关于使用C语言实现静态、动态、文件操作的通讯录&#xff0c;其中使用到了结构体这一类型&#xff0c;实际上&#xff0c;是可以属于数据结构的内容&#xff0c;接下来我们来了解一下顺序表的相关内容。 目录 前言 一、线性表 一…