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;
}

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

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

相关文章
|
1天前
|
编译器 C语言 C++
【C++入门学习指南】:函数重载提升代码清晰度与灵活性
【C++入门学习指南】:函数重载提升代码清晰度与灵活性
9 0
|
1天前
|
安全 编译器 程序员
【C++入门】内联函数、auto与基于范围的for循环
【C++入门】内联函数、auto与基于范围的for循环
|
1天前
|
存储 安全 编译器
【C++入门】缺省参数、函数重载与引用(下)
【C++入门】缺省参数、函数重载与引用
|
1天前
|
编译器 C语言 C++
【C++入门】缺省参数、函数重载与引用(上)
【C++入门】缺省参数、函数重载与引用
|
1天前
|
C语言 C++
【C++入门】关键字、命名空间以及输入输出
【C++入门】关键字、命名空间以及输入输出
|
1天前
|
人工智能 分布式计算 Java
【C++入门】初识C++
【C++入门】初识C++
|
1天前
|
C++ Python
C++教学——从入门到精通 10.循环
学习编程建议先Python后C++,以避免C++思维影响。课程涵盖for、while和do while循环。for循环示例:`for(int i=0;i&lt;n;i++)`,用于计算114514天后的金币总数(1145140个)。死循环通过`for(int i=0;;i++)`实现,用`break`退出。while循环格式`while(条件)`,同样可解决金币问题。do while循环特点是先执行后判断,结构为`do{...}while(条件)`。
6 2
|
2天前
|
存储 Linux C++
【进厂修炼 - First week】Linux & C++
【进厂修炼 - First week】Linux & C++
|
2天前
|
C++
C++入门项目——通讯管理系统
C++入门项目——通讯管理系统
|
2天前
|
存储 编译器 C++
C++基础入门(超详细)
C++基础入门(超详细)