linux中的线程安全(二)

信号量

进化版的互斥锁(1 --> N)

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

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

主要应用函数:

        sem_init函数

        sem_destroy函数

        sem_wait函数

        sem_trywait函数  

        sem_timedwait函数     

        sem_post函数

以上6 个函数的返回值都是:成功返回0, 失败返回-1,同时设置errno。(注意,它们没有pthread前缀)

        sem_t类型,本质仍是结构体。但应用期间可简单看作为整数,忽略实现细节(类似于使用文件描述符)。

sem_t sem; 规定信号量sem不能 < 0。头文件 <semaphore.h>

信号量基本操作:

sem_wait:        1. 信号量大于0,则信号量--              (类比pthread_mutex_lock)

          |                   2. 信号量等于0,造成线程阻塞

        对应

          |

        sem_post:     将信号量++,同时唤醒阻塞在信号量上的线程 (类比pthread_mutex_unlock)

但,由于sem_t的实现对用户隐藏,所以所谓的++、--操作只能通过函数来实现,而不能直接++、--符号。

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

sem_init函数

初始化一个信号量

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

        参1:sem信号量

参2:pshared取0用于线程间;取非0(一般为1)用于进程间

参3:value指定信号量初值

sem_destroy函数

销毁一个信号量

        int sem_destroy(sem_t *sem);

sem_wait函数

给信号量加锁 --

        int sem_wait(sem_t *sem);

sem_post函数

给信号量解锁 ++

         int sem_post(sem_t *sem); 

sem_trywait函数

尝试对信号量加锁 --    (与sem_wait的区别类比lock和trylock)

         int sem_trywait(sem_t *sem);     

sem_timedwait函数

限时尝试对信号量加锁 --

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

        参2:abs_timeout采用的是绝对时间。                    

        定时1秒:

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

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

                 t.tv_sec = cur+1; 定时1秒

                 t.tv_nsec = t.tv_sec +100;

sem_timedwait(&sem, &t); 传参

生产者消费者信号量模型

【练习】:使用信号量完成线程间同步,模拟生产者,消费者问题。                                  

分析:

        规定:    如果□中有数据,生产者不能生产,只能阻塞。

                         如果□中没有数据,消费者不能消费,只能等待数据。

        定义两个信号量:S满 = 0, S空 = 1 (S满代表满格的信号量,S空表示空格的信号量,程序起始,格子一定为空)

        所以有:        T生产者主函数 {                           T消费者主函数 {

                                     sem_wait(S空);                            sem_wait(S满);

                             生产....                                           消费....

                                     sem_post(S满);                            sem_post(S空);

                                  }                                                          }

        假设:    线程到达的顺序是:T生、T生、T消。

        那么:    T生1 到达,将S空-1,生产,将S满+1

                         T生2 到达,S空已经为0, 阻塞

                         T消  到达,将S满-1,消费,将S空+1

        三个线程到达的顺序是:T生1、T生2、T消。而执行的顺序是T生1、T消、T生2

        这里,S空 表示空格子的总数,代表可占用信号量的线程总数-->1。其实这样的话,信号量就等同于互斥锁。

        但,如果S空=2、3、4……就不一样了,该信号量同时可以由多个线程占用,不再是互斥的形式。因此我们说信号量是互斥锁的加强版。

/*信号量实现 生产者 消费者问题*/

#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <stdio.h>
#include <semaphore.h>

#define NUM 5

int queue[NUM];                                     //全局数组实现环形队列
sem_t blank_number, product_number;                 //空格子信号量, 产品信号量

void *producer(void *arg)
{
    int i = 0;

    while (1) {
        sem_wait(&blank_number);                    //生产者将空格子数--,为0则阻塞等待
        queue[i] = rand() % 1000 + 1;               //生产一个产品
        printf("----Produce---%d\n", queue[i]);
        sem_post(&product_number);                  //将产品数++

        i = (i+1) % NUM;                            //借助下标实现环形
        sleep(rand()%3);
    }
}

void *consumer(void *arg)
{
    int i = 0;

    while (1) {
        sem_wait(&product_number);                  //消费者将产品数--,为0则阻塞等待
        printf("-Consume---%d\n", queue[i]);
        queue[i] = 0;                               //消费一个产品
        sem_post(&blank_number);                    //消费掉以后,将空格子数++

        i = (i+1) % NUM;
        sleep(rand()%3);
    }
}

int main(int argc, char *argv[])
{
    pthread_t pid, cid;

    sem_init(&blank_number, 0, NUM);                //初始化空格子信号量为5
    sem_init(&product_number, 0, 0);                //产品数为0

    pthread_create(&pid, NULL, producer, NULL);
    pthread_create(&cid, NULL, consumer, NULL);

    pthread_join(pid, NULL);
    pthread_join(cid, NULL);

    sem_destroy(&blank_number);
    sem_destroy(&product_number);

    return 0;
}

【推演练习】:      理解上述模型,推演,如果是两个消费者,一个生产者,是怎么样的情况。            

【作业】:结合生产者消费者信号量模型,揣摩sem_timedwait函数作用。编程实现,一个线程读用户输入, 另一个线程打印“hello world”。如果用户无输入,则每隔5秒向屏幕打印一个“hello world”;如果用户有输入,立刻打印“hello world”到屏幕。

进程间同步

互斥量mutex

进程间也可以使用互斥锁,来达到同步的目的。但应在pthread_mutex_init初始化之前,修改其属性为进程间共享。mutex的属性修改函数主要有以下几个。

主要应用函数:

        pthread_mutexattr_t mattr 类型:              用于定义mutex锁的【属性】

        pthread_mutexattr_init函数:                    初始化一个mutex属性对象

                 int pthread_mutexattr_init(pthread_mutexattr_t *attr);

        pthread_mutexattr_destroy函数:             销毁mutex属性对象 (而非销毁锁)

                 int pthread_mutexattr_destroy(pthread_mutexattr_t *attr);

        pthread_mutexattr_setpshared函数:       修改mutex属性。

                 int pthread_mutexattr_setpshared(pthread_mutexattr_t *attr, int pshared);

                 参2:pshared取值:

                         线程锁:PTHREAD_PROCESS_PRIVATE (mutex的默认属性即为线程锁,进程间私有)

                         进程锁:PTHREAD_PROCESS_SHARED

进程间mutex示例

进程间使用mutex来实现同步:

#include <fcntl.h>

#include <pthread.h>

#include <sys/mman.h>

#include <sys/wait.h>

 

struct mt {

    int num;

    pthread_mutex_t mutex;

    pthread_mutexattr_t mutexattr;

};

 

int main(void)

{

    int fd, i;

    struct mt *mm;

    pid_t pid;

 

    fd = open("mt_test", O_CREAT | O_RDWR, 0777);

    ftruncate(fd, sizeof(*mm));

    mm = mmap(NULL, sizeof(*mm), PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);

    close(fd);

    unlink("mt_test");

    //mm = mmap(NULL, sizeof(*mm), PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANON, -1, 0);

    memset(mm, 0, sizeof(*mm));

 

    pthread_mutexattr_init(&mm->mutexattr);                                  //初始化mutex属性对象

    pthread_mutexattr_setpshared(&mm->mutexattr, PTHREAD_PROCESS_SHARED);    //修改属性为进程间共享

    pthread_mutex_init(&mm->mutex, &mm->mutexattr);                          //初始化一把mutex琐

 

    pid = fork();

    if (pid == 0) {

        for (i = 0; i < 10; i++) {

            pthread_mutex_lock(&mm->mutex);

            (mm->num)++;

            printf("-child----num++   %d\n", mm->num);

            pthread_mutex_unlock(&mm->mutex);

            sleep(1);

        }

    } else if (pid > 0) {

        for ( i = 0; i < 10; i++) {

            sleep(1);

            pthread_mutex_lock(&mm->mutex);

            mm->num += 2;

            printf("-parent---num+=2  %d\n", mm->num);

            pthread_mutex_unlock(&mm->mutex);

        }

        wait(NULL);

    }

 

    pthread_mutexattr_destroy(&mm->mutexattr);          //销毁mutex属性对象

    pthread_mutex_destroy(&mm->mutex);                //销毁mutex

    munmap(mm,sizeof(*mm));                          //释放映射区

    return 0;

}                                                                                                                                                                       【process_mutex.c】

文件锁

        借助 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

              off_t l_start;               起始偏移:1000

              off_t l_len;               长度:0表示整个文件加锁

              pid_t l_pid;     持有该锁的进程ID:(F_GETLK only)

              ...

         };

进程间文件锁示例

多个进程对加锁文件进行访问:                                                                                                            

#include <stdio.h>

#include <fcntl.h>

#include <unistd.h>

 

void sys_err(char *str)

{

    perror(str); exit(1);

}

int main(int argc, char *argv[])

{

    int fd;

    struct flock f_lock;

 

    if (argc < 2) {

        printf("./a.out 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;               /* 0表示整个文件加锁 */

 

    fcntl(fd, F_SETLKW, &f_lock);

    printf("get flock\n");

    sleep(10);

 

    f_lock.l_type = F_UNLCK;

    fcntl(fd, F_SETLKW, &f_lock);

    printf("un flock\n");

 

    close(fd);  return 0;

}                                                                                                                                                                               【file_lock.c】

依然遵循“读共享、写独占”特性。但!如若进程不加锁直接操作文件,依然可访问成功,但数据势必会出现混乱。

【思考】:多线程中,可以使用文件锁吗?

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

哲学家用餐模型分析

多线程版:

        选用互斥锁mutex,如创建5个, pthread_mutex_t m[5];

        模型抽象:   

                 5个哲学家 --> 5个线程;   5支筷子 --> 5把互斥锁                int left(左手), right(右手)

                 5个哲学家使用相同的逻辑,可通用一个线程主函数,void *tfn(void *arg),使用参数来表示线程编号:int i = (int)arg;

                 哲学家线程根据编号知道自己是第几个哲学家,而后选定锁,锁住,吃饭。否则哲学家thinking。

                                                                        A   B   C   D   E

                 5支筷子,在逻辑上形成环: 0   1   2   3   4   分别对应5个哲学家:

                                

 

        所以有:

                 if(i == 4)  

                         left = i, right = 0;

                 else

                         left = i, right = i+1;

        振荡:如果每个人都攥着自己左手的锁,尝试去拿右手锁,拿不到则将锁释放。过会儿五个人又同时再攥着左手锁尝试拿右手锁,依然拿不到。如此往复形成另外一种极端死锁的现象——振荡。

        避免振荡现象:只需5个人中,任意一个人,拿锁的方向与其他人相逆即可(如:E,原来:左:4,右:0 现在:左:0, 右:4)。

        所以以上if else语句应改为:

                 if(i == 4)  

                         left = 0, right = i;

                 else

                         left = i, right = i+1;

        而后, 首先应让哲学家尝试加左手锁: 

while {

                         pthread_mutex_lock(&m[left]);     如果加锁成功,函数返回再加右手锁,

                                                                                    如果失败,应立即释放左手锁,等待。

                         若,左右手都加锁成功 --> 吃 --> 吃完 --> 释放锁(应先释放右手、再释放左手,是加锁顺序的逆序)

                 }

        主线程(main)中,初始化5把锁,销毁5把锁,创建5个线程(并将i传递给线程主函数),回收5个线程。

避免死锁的方法:

        1. 当得不到所有所需资源时,放弃已经获得的资源,等待。

        2. 保证资源的获取顺序,要求每个线程获取资源的顺序一致。如:A获取顺序1、2、3;B顺序应也是1、2、3。若B为3、2、1则易出现死锁现象。                                                                                           

多进程版

相较于多线程需注意问题:

        需注意如何共享信号量 (注意:坚决不能使用全局变量 sem_t s[5])

实现:

        main函数中:      

循环 sem_init(&s[i], 0, 1); 将信号量初值设为1,信号量变为互斥锁。

                 循环 sem_destroy(&s[i]);

                 循环 创建 5 个子进程。 if(i < 5) 中完成子进程的代码逻辑。

                 循环 回收 5 个子进程。

        子进程中:

if(i == 4) 

left = 0, right == 4;

                 else 

left = i, right = i+1;  

                 while (1) {

                         使用 sem_wait(&s[left]) 锁左手,尝试锁右手,若成功 --> 吃; 若不成功 --> 将左手锁释放。

                         吃完后, 先释放右手锁,再释放左手锁。

                 }

【重点注意】:

直接将sem_t s[5]放在全局位置,试图用于子进程间共享是错误的!应将其定义放置与mmap共享映射区中。main中:

sem_t *s = mmap(NULL, sizeof(sem_t) * 5, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANON, -1, 0);

        使用方式:将s当成数组首地址看待,与使用数组s[5]没有差异。

  

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值