C++实战-Linux多线程(入门到精通)(三)

简介: C++实战-Linux多线程(入门到精通)(三)

读写锁

与互斥量类似,但读写锁允许更高的并行性。其特性为:写独占,读共享

       当有一个线程已经持有互斥锁时,互斥锁将所有试图进入临界区的线程都阻塞。但是考虑一种情况,当前持有互斥锁的线程只是要读访问共享资源,而同时有其他几个线程也想读取这个共享资源,但是由于互斥锁的排它性,所有其它线程都无法获取锁,也就无法获取访问共享资源了,但实际上多个线程同时读访问共享资源并不会导致问题。

       在对数据的读写操作中,更多是读操作,写操作相对较少。例如:数据库数据的读写应用。为了满足当前能够允许多个读出,但只允许一个写入的需求,线程提供了读写锁来实现。

读写锁的特点:

       1.如果有其他线程读数据,则允许其他线程执行读操作,但不允许写操作

       2.如果有其他线程写数据,则其他线程都不允许读、写操作

       3.写是独占的,写的优先级高(防止写着饿死)

说了这么多,发现就是操作系统课本中的读者写者问题....哈哈哈

相关函数

读写锁类型:pthread_rwlock_t

pthread_rwlock_init函数

       int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,

                                            const pthread_rwlockattr_t *restrict attr);

       作用:初始化一把读写锁

       参数:

                  attr表示读写锁属性,通常使用默认属性,传NULL即可

pthread_rwlock_destroy函数

       int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);

       作用:销毁一把读写锁

pthread_rwlock_rdlock函数

       int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);

       作用:以读方式请求读写锁(请求读锁)

pthread_rwlock_wrlock函数

       int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);

      作用:以写方式请求读写锁(请求写锁)

pthread_rwlock_unlock函数

       int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

       作用:解锁

pthread_rwlock_tryrdlock函数

       int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);

       作用:非阻塞请求读锁

pthread_rwlock_trywrlock函数

       int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);

       作用:非阻塞请求写锁

上个案例(同时有多个线程对同一共享数据读、写操作)

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
pthread_rwlock_t rwlock;
int counter = 0;
//写线程
void *th_write(void *arg)
{
    int t;
    int i = (int)arg;
    while(1)
    {
        pthread_rwlock_wrlock(&rwlock);    //上写锁
        sleep(2);
        printf("writer:%d %lu counter:%d\n",i,pthread_self(),++counter);
        pthread_rwlock_unlock(&rwlock);    //解锁
        usleep(10000);
    }
}
//读线程
void *th_read(void *arg)
{
    int i = (int)arg;
    while(1)
    {
        pthread_rwlock_rdlock(&rwlock);    //上读锁
        printf("read:%d %lu counster:%d\n",i,pthread_self(),counter);
        pthread_rwlock_unlock(&rwlock);
        sleep(3);
    }
}
int main(void)
{
    int i;
    pthread_t tid[8];
    pthread_rwlock_init(&rwlock,NULL);
    for(i=0;i<3;++i)
    {
        pthread_create(&tid[i],NULL,th_write,(void *)i);
    }
    for(i=0;i<5;++i)
    {   
        pthread_create(&tid[i],NULL,th_read,(void *)i);
    }
    //回收子线程
    for(i=0;i<8;++i)
    {
        pthread_join(tid[i],NULL);
    }
    //销毁读写锁
    pthread_rwlock_destroy(&rwlock);
    return 0;
}

条件变量

条件变量本身不是锁,但是可以引起线程阻塞。通常与互斥锁配合使用。给多线程提供一个合适的场所。(线程同步问题,互斥锁和读写锁解决线程互斥)

相关函数

条件变量的类型:pthread_cond_t

pthread_cond_init函数

      int pthread_cond_int(pthread_cond_t *restrict cond,

                                            const pthread_condattr_t *restrict attr);

       参数:attr表示条件变量的属性,通常传NULL

       可以使用静态初始化方法,初始化条件变量

       pthread_cond_t cond = PTHREAD_COND_INITIALIZER

pthread_cond_destroy函数

       int pthread_cond_destroy(pthread_cond_t *cond);

       作用:销毁一个条件变量

pthread_cond_wait函数

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

       作用:1.阻塞等待一个条件变量cond满足[不满足:阻塞等待满足   满足:不阻塞]

                  2.释放已掌握的互斥量(解锁互斥量)相当于 pthread_mutex_unlock(&mutex);

                  [1,2为原子操作,不可分割]

                  3.当被唤醒,pthread_cond_wait函数返回时,解除阻塞并重新申请获取互斥锁

                       pthread_mutex_lock(&mutex);

pthread_cond_timedwait函数

       int pthread_cond_timewat(pthread_cond _t *restrict cond,

                               pthread_mutex_t *restrict mutex,

                               const struct timespec *restrict abstime);

       作用:限时等待一个条件变量

       参数:

       abstime是一个绝对时间,time(NULL)返回的就是一个绝对时间,

       alarm(1)是相对时间,相对当前时间1s。      

使用方法:

       错误用法:

       struct timespec t = {1,0};

       pthread_cond_timedwait(&cond,&mutex,&t); //只能定时到 1970.1.1 00:00:01

       正确用法:

       time_t cur = time(NULL);    //获取当前时间

       struct timespec t;

       t.tv_sec = cur+1;                //定义一秒

        pthread_cond_timedwait(&cond,&mutex,&t);

pthread_cond_signal函数

      int pthread_cond_signal(pthread_cond_t *cond);

       作用:唤醒一个或多个等待的线程

pthread_cond_broadcast函数

       int pthread_cond_broadcast(pthread_cond_t *cond);

       作用:通过广播唤醒所有的线程

   

案例见消费者生产者模型  

生产者消费者模型

生产者模型中的对象:生产者、消费者、容器

一端专门生产商品,一端专门消费商品。那么如此以来就会有一些问题需要注意,当容器满了,生产者阻塞等待消费者消费商品使容器可以存放物品。当容器为空,则需要阻塞等待生产者生产物品并存放容器内。

进入容器的线程每一时刻保持只有一个(形成互斥关系,生产者与消费者保持同步关系)

为什么要讲生产者和消费者模型,因为在实际的项目开发中,这个模式用到的地方很多

生产者消费者模型实现(条件变量版)

线程同步典型的案例即为生产者消费者模型,而借助条件变量来实现这一模型,使比较方便的。假定有两个线程,一个模拟生产者的行为,一个模拟消费者的行为。两个线程同时操作一个共享资源(一般称之为汇聚),生产者向其中添加商品,消费者从中消费产品。

因为程序中的插入删除操作比频繁(我就用链表来实现容器了)

#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
struct msg
{
    struct msg *next;
    int num;
};
struct msg *head;
//静态方法初始化
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
//消费者
void* consumer(void *p)
{
    struct msg *mp;
    while(1)
    {
        //取产品
        pthread_mutex_lock(&mutex);
        while(head==NULL)   //不能用 if  防止虚假唤醒
        {
            pthread_cond_wait(&cond,&mutex);
        }
        mp = head;
        head = mp->next;
        pthread_mutex_unlock(&mutex);
        //消费产品
        printf("消费:%d\n",mp->num);
        free(mp);
        sleep(rand()%5);
    }
    return NULL;
}
//生产者
void *product(void *arg)
{
    struct msg *mp;
    while(1)
    {
        //生产产品
        mp = malloc(sizeof(struct msg));
        mp->num = rand()%200;
        printf("生产:%d\n",mp->num);
        //防止产品
        pthread_mutex_lock(&mutex);
        mp->next = head;
        head = mp;
        pthread_mutex_unlock(&mutex);
        pthread_cond_signal(&cond);
        sleep(rand()%5); 
    }
    return NULL;
}
int main(void)
{
    pthread_t pid,cid;
    srand(time(NULL));
    pthread_create(&pid,NULL,product,NULL);
    pthread_create(&cid,NULL,consumer,NULL);
    pthread_join(pid,NULL);
    pthread_join(cid,NULL);
    return 0;
}

条件变量的优点:

       相对于mutex而言,条件变量可以减少竞争。

       如果直接使用mutex,除了生产者、消费者之间要竞争互斥量以外,消费者之间也要竞争互斥量,但如果汇聚(链表)中没有数据,消费者之间在的竞争是无意义的。有了条件变量机制以后,只有生产者完成生产,才会引起消费者之间的竞争,提高程序效率。

信号量

       由于互斥锁的粒度比较大,如果我们希望在多个线程间对某一个对象的部分数据进行共享,使用互斥锁是没有办法实现的,只能将整个数据对象锁住。这样虽然达到了多线程操作共享数据时保证数据正确性的目的,却无形中导致线程的并发性下降。线程从并发执行,变成了串行执行。与直接使用单进程无异。

       信号量是相对折中的处理方式,既能保证同步,数据不混乱,又能提高线程并发。

       (进程间也可以使用哦)

信号量像是PV操作

相关函数

       信号量的类型:sem_t

       信号量的初始值,决定了占用信号量的线程个数

       

       sem_init函数

               int sem_init(sem_t *sem,int pshared,unsigned int value);

               参数:

                       sem_t  信号量

                       pshared  0为线程,非0(一般为1)用于进程

                       value   指定信号量的初值

       sem_destroy函数

              int sem_destroy(sem_t *sem);

               作用:销毁一个信号量

       sem_wait函数

               int sem_wait(sem_t *sem);

               作用:先判断sem==0阻塞,value  -1

                          对信号量的值减1,如果为0,阻塞

      sem_trywait函数

               int sem_trywait(sem_t *sem);

               作用:尝试对信号量减1  (非阻塞)

       sem_timedwait函数

               sem_timedwait(sem_t *sem,const struct timespec *abs_timeout);

               作用:限时尝试对信号量加锁 --

               参数:abs_timeout是个绝对时间

               定时1秒:

                       time_t cur = time(NULL);                //获取当前时间

                       struct timespec t;                             //定义 timespec 结构体变量 t

                       t.tv_sec = sur + 1;

                       //t.tv_nsec = t.tv_sec + 100;

                       sem_timedwait(&sem,&t);

       sem_post函数

               int sem_post(sem_t *sem);

               作用:对信号量加1

       sem_getvalue函数

               sem_getvalue(sem_t *sem,int *sval);

               作用:获取当前信号量的值

案例见生产者消费者模型

生产者消费者模型实现(信号量版)

#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <semaphore.h>
#define NUM 5
int queue[NUM];     //缓冲区
sem_t produc_number,blank_number;
void *product(void *arg)
{
    int i = 0;
    while(1)
    {
        sem_wait(&blank_number);    //缓冲区是否已满
        queue[i] = rand()%100 + 1;   //生产产品
        printf("放缓冲区:%d\n",queue[i]);
        sem_post(&produc_number);   //产品数 ++
        i = (i+1)%NUM;
        sleep(rand()%3);
    }
    return NULL;
}
void *consumer(void *arg)
{
    int i = 0;
    while(1)
    {
        sem_wait(&produc_number); //产品数量--
        printf("取走缓冲区:%d\n",queue[i]);
        queue[i] = 0;
        sem_post(&blank_number);    //格子数目++
        i = (i+1)%NUM;  //循环队列,下一个位置
        sleep(rand()%3);
    }
    return NULL;
}
int main(void)
{
    pthread_t pid,cid;
    sem_init(&blank_number,0,NUM);      //初始时缓冲区空格子为5(或者说初始时生产者为5)
    sem_init(&produc_number,0,0);       //初始时产品数为0(或者说消费者为0)        
    pthread_create(&pid,NULL,product,NULL);
    pthread_create(&cid,NULL,consumer,NULL);
    pthread_join(pid,NULL);
    pthread_join(cid,NULL);
    //线程销毁
    sem_destroy(&blank_number);
    sem_destroy(&produc_number);
    return 0;
}

文件锁

借助fcntl函数来实现锁机制,操作文件的进程没有获取锁时,可以打开,但无法执行read、write操作,fcntl函数:获取、设置文件访问控制权限

int fcntl(int fd,int cmd,.../*arg*/);

参2:

     F_SETLK(struct flock*)   设置文件锁(trylock)

     F_SETLKW(struct flock*) 设置文件锁(lock) W-->wait

     F_GETLK(struct flock*);  获取文件锁

参3:

      struct flock{

               ...

               short l_type;                        锁的类型:F_RDLCK F_WRLCK  F_UNLCK(解锁)

               short l_whence;                  偏移位置:SEEK_SET  SEEK_CUR SEEK_END

               short l_start;                       起始位置:1000

               short l_len;                          长度:0表示整个问价加锁

               short l_pid;                          持有该锁的进程ID:F_GETLK

               ...

       }

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
void sys_err(char *str)
{
    perror(str);
    exit(-1);
}
int main(int arg,char *argv[])
{
    int fd;
    struct flock f_lock;
    if(arg < 2)
    {
        printf("./a.txt filename\n");
        exit(1);
    }
    if(fd == open(argv[1],O_RDWR) < 0)
    {
        sys_err("open");
    }
    // f_lock.l_type = F_WRLCK;    //设置写锁
    f_lock.l_type = F_RDLCK;    //设置读锁
    f_lock.l_whence = SEEK_SET; //文件头部
    f_lock.l_start = 0;
    f_lock.l_len = 0;
    fcntl(fd,F_SETLKW,&f_lock);     //上锁
    printf("get flock\n");
    sleep(10);
    f_lock.l_type = F_UNLCK;
    fcntl(fd,F_SETLK,&f_lock);      //解锁
    printf("un flock\n");
    close(fd);
    return 0;
}

依旧遵循“读共享,写独占”。但进程不加锁直接操作文件,依旧可以访问成功,但数据势必出现混乱。

多线程间共享文件描述符,而给文件加锁,是通过修改文件描述符所指向的文件结构体中的成员变量实现的,所以多线程无法使用文件锁。

相关文章
|
2月前
|
安全 程序员 编译器
【实战经验】17个C++编程常见错误及其解决方案
想必不少程序员都有类似的经历:辛苦敲完项目代码,内心满是对作品品质的自信,然而当静态扫描工具登场时,却揭示出诸多隐藏的警告问题。为了让自己的编程之路更加顺畅,也为了持续精进技艺,我想借此机会汇总分享那些常被我们无意间忽视却又导致警告的编程小细节,以此作为对未来的自我警示和提升。
288 10
|
1月前
|
缓存 安全 C++
C++无锁队列:解锁多线程编程新境界
【10月更文挑战第27天】
55 7
|
1月前
|
消息中间件 存储 安全
|
1月前
|
Unix Linux Shell
linux入门!
本文档介绍了Linux系统入门的基础知识,包括操作系统概述、CentOS系统的安装与远程连接、文件操作、目录结构、用户和用户组管理、权限管理、Shell基础、输入输出、压缩打包、文件传输、软件安装、文件查找、进程管理、定时任务和服务管理等内容。重点讲解了常见的命令和操作技巧,帮助初学者快速掌握Linux系统的基本使用方法。
77 3
|
2月前
|
存储 消息中间件 资源调度
C++ 多线程之初识多线程
这篇文章介绍了C++多线程的基本概念,包括进程和线程的定义、并发的实现方式,以及如何在C++中创建和管理线程,包括使用`std::thread`库、线程的join和detach方法,并通过示例代码展示了如何创建和使用多线程。
58 1
|
2月前
|
存储 并行计算 安全
C++多线程应用
【10月更文挑战第29天】C++ 中的多线程应用广泛,常见场景包括并行计算、网络编程中的并发服务器和图形用户界面(GUI)应用。通过多线程可以显著提升计算速度和响应能力。示例代码展示了如何使用 `pthread` 库创建和管理线程。注意事项包括数据同步与互斥、线程间通信和线程安全的类设计,以确保程序的正确性和稳定性。
|
1月前
|
自然语言处理 编译器 Linux
告别头文件,编译效率提升 42%!C++ Modules 实战解析 | 干货推荐
本文中,阿里云智能集团开发工程师李泽政以 Alinux 为操作环境,讲解模块相比传统头文件有哪些优势,并通过若干个例子,学习如何组织一个 C++ 模块工程并使用模块封装第三方库或是改造现有的项目。
|
2月前
|
Ubuntu Linux 编译器
Linux/Ubuntu下使用VS Code配置C/C++项目环境调用OpenCV
通过以上步骤,您已经成功在Ubuntu系统下的VS Code中配置了C/C++项目环境,并能够调用OpenCV库进行开发。请确保每一步都按照您的系统实际情况进行适当调整。
542 3
|
2月前
|
存储 前端开发 C++
C++ 多线程之带返回值的线程处理函数
这篇文章介绍了在C++中使用`async`函数、`packaged_task`和`promise`三种方法来创建带返回值的线程处理函数。
82 6
|
2月前
|
C++
C++ 多线程之线程管理函数
这篇文章介绍了C++中多线程编程的几个关键函数,包括获取线程ID的`get_id()`,延时函数`sleep_for()`,线程让步函数`yield()`,以及阻塞线程直到指定时间的`sleep_until()`。
41 0