Linux——线程同步

1,线程同步概念     

      在并发情况下,指令执行的先后顺序由内核决定,同一个线程内部,指令按照先后顺序执行,但不同线程之间的指令很难说清楚是哪一个先执行,如果运行的结果依赖于不同线程执行的先后的话,那么就会形成竞争条件,在这样的情况下,计算的结果很难预知,所以应该尽量避免竞争条件的形成。

      最常见的解决竞争条件的方法是将原先分离的两个指令构成一个不可分割的原子操作,而其他任务不能插入到原子操作中!

对多线程来说,同步指的是在一定时间内只允许某一个线程访问某个资源,而在此时间内,不允许其他线程访问该资源(按照认为设置的顺序访问临界资源)!

      线程同步的常见方法:互斥锁,条件变量,读写锁,信号量

2,互斥量(互斥锁)   

      Linux中提供一把互斥锁mutex(也称之为互斥量)。每个线程在对资源操作前都尝试先加锁,成功加锁才能操作,操作结束解锁。资源还是共享的,线程间也还是竞争的,但通过“锁”就将资源的访问变成互斥操作,而后与时间有关的错误也不会再产生了。同一时刻,只能有一个线程持有该锁。

      当A线程对某个全局变量加锁访问,B在访问前尝试加锁,拿不到锁,B阻塞。C线程不去加锁,而直接访问该全局变量,依然能够访问,但会出现数据混乱。所以,互斥锁实质上是操作系统提供的一把“建议锁”(又称“协同锁”),建议程序中有多线程访问共享资源的时候使用该机制。但,并没有强制限定。因此,即使有了mutex,如果有线程不按规则来访问数据,依然会造成数据混乱。下面介绍互斥锁中的主要应用函数:

      注意以上5个函数的返回值都是:成功返回0,失败返回错误号。     

      pthread_mutex_t 类型,其本质是一个结构体。为简化理解,应用时可忽略其实现细节,简单当成整数看待。

      pthread_mutex_t mutex;变量mutex只有两种取值1、0。

  • pthread_mutex_init函数

         初始化一个互斥锁(互斥量)--->初值可看作1

         int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);

         参1:传出参数,调用时应传&mutex        

         restrict关键字:只用于限制指针,告诉编译器,所有修改该指针指向内存中内容的操作,只能通过本指针完成。不能通过除本指针以外的其他变量或指针修改(不能通过赋值给其他指针,用其他指针的方式执行)。

         参2:互斥量属性。是一个传入参数,通常传NULL,选用默认属性(线程间共享)。       

         1)静态初始化:如果互斥锁 mutex 是静态分配的(定义在全局,或加了static关键字修饰),可以直接使用宏进行初始化。e.g. pthead_mutex_t muetx = PTHREAD_MUTEX_INITIALIZER;

         2)动态初始化:局部变量应采用动态初始化。e.g. pthread_mutex_init(&mutex, NULL)

  • pthread_mutex_destroy函数       

       销毁一个互斥锁

         int pthread_mutex_destroy(pthread_mutex_t *mutex)

  • pthread_mutex_lock函数     

       加锁。可理解为将mutex--(或-1)

         int pthread_mutex_lock(pthread_mutex_t *mutex);

  • pthread_mutex_unlock函数     

       解锁。可理解为将mutex ++(或+1)

         int pthread_mutex_unlock(pthread_mutex_t *mutex);

  • pthread_mutex_trylock函数      

       尝试加锁

         int pthread_mutex_trylock(pthread_mutex_t *mutex);

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

pthread_mutex_t mutex;      //定义锁

void *tfn(void *arg)
{
    srand(time(NULL));

    while (1) {
        pthread_mutex_lock(&mutex);

        printf("hello ");
        sleep(rand() % 3);	/*模拟长时间操作共享资源,导致cpu易主,产生与时间有关的错误*/
        printf("world\n");
        pthread_mutex_unlock(&mutex);

        sleep(rand() % 3);
    }

    return NULL;
}

int main(void)
{
    int flg = 5;
    pthread_t tid;
    srand(time(NULL));

    pthread_mutex_init(&mutex, NULL);  // mutex==1
    pthread_create(&tid, NULL, tfn, NULL);
    while (flg--) {

        pthread_mutex_lock(&mutex);

        printf("HELLO ");
        sleep(rand() % 3);
        printf("WORLD\n");
        pthread_mutex_unlock(&mutex);

        sleep(rand() % 3);

    }
    pthread_cancel(tid);
    pthread_join(tid, NULL);

    pthread_mutex_destroy(&mutex);  

    return 0;
}

/*线程之间共享资源stdout*/

      在访问共享资源前加锁,访问结束后立即解锁。锁的“粒度”应越小越好。

  • 死锁     

         1. 线程试图对同一个互斥量A加锁两次。

         2. 线程1拥有A锁,请求获得B锁;线程2拥有B锁,请求获得A锁        

3,读写锁

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

  • 读写锁状态      

     一把读写锁具备三种状态: 读模式下加锁状态 (读锁); 写模式下加锁状态 (写锁); 不加锁状态

  • 读写锁特性      
  1. 读写锁是“写模式加锁”时,解锁前,所有对该锁加锁的线程都会被阻塞。
  2. 读写锁是“读模式加锁”时,如果线程以读模式对其加锁会成功;如果线程以写模式加锁会阻塞。
  3. 读写锁是“读模式加锁”时,既有试图以写模式加锁的线程,也有试图以读模式加锁的线程。那么读写锁会阻塞随后的读模式锁请求。优先满足写模式锁。读锁、写锁并行阻塞,写锁优先级高

         读写锁也叫共享-独占锁。当读写锁以读模式锁住时,它是以共享模式锁住的;当它以写模式锁住时,它是以独占模式锁住的。写独占、读共享。

         读写锁非常适合于对数据结构读的次数远大于写的情况。  

   以下7 个函数的返回值都是:成功返回0,失败直接返回错误号。    

         pthread_rwlock_t类型   用于定义一个读写锁变量。

         pthread_rwlock_t rwlock;

  • pthread_rwlock_init函数      

      初始化一把读写锁

         int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);

         参2: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_tryrdlock函数

      非阻塞以读方式请求读写锁(非阻塞请求读锁):int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);

  • pthread_rwlock_trywrlock函数        

      非阻塞以写方式请求读写锁(非阻塞请求写锁):int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);

  • pthread_rwlock_unlock函数     

      解锁: int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

读写锁使用示例代码:


/* 3个线程不定时 "写" 全局资源,5个线程不定时 "读" 同一全局资源 */

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

int counter;                          //全局资源
pthread_rwlock_t rwlock;

void *th_write(void *arg)
{
    int t;
    int i = (int)arg;

    while (1) {
        t = counter;
        usleep(1000);

        pthread_rwlock_wrlock(&rwlock);
        printf("=======write %d: %lu: counter=%d ++counter=%d\n", i, pthread_self(), t, ++counter);
        pthread_rwlock_unlock(&rwlock);

        usleep(5000);
    }
    return NULL;
}

void *th_read(void *arg)
{
    int i = (int)arg;

    while (1) {
        pthread_rwlock_rdlock(&rwlock);
        printf("----------------------------read %d: %lu: %d\n", i, pthread_self(), counter);
        pthread_rwlock_unlock(&rwlock);

        usleep(900);
    }
    return NULL;
}

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+3], NULL, th_read, (void *)i);

    for (i = 0; i < 8; i++)
        pthread_join(tid[i], NULL);

    pthread_rwlock_destroy(&rwlock);            //释放读写琐

    return 0;
}

4,条件变量

    条件变量本身不是锁!但它也可以造成线程阻塞。通常与互斥锁配合使用。给多线程提供一个会合的场所。

    以下6 个函数的返回值都是:成功返回0,失败直接返回错误号。

         pthread_cond_t类型      用于定义条件变量

         pthread_cond_t cond;

  • pthread_cond_init函数     

       初始化一个条件变量:int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);                

            参2: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(参1)满足 

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

                            1.2.两步为一个原子操作不可再分。

                      3)当被唤醒,pthread_cond_wait函数返回时,解除阻塞并重新申请获取互斥锁pthread_mutex_lock(&mutex);

       注意使用这个函数之前,必须定义一个条件变量,一个互斥锁,并且对互斥锁进行了加锁。

  • pthread_cond_timedwait函数     

     限时等待一个条件变量:int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);    

     参3:  参看man sem_timedwait函数,查看struct timespec结构体。

                   struct timespec {

                            time_t tv_sec;          /* seconds */ 秒

                            long   tv_nsec;      /* nanosecondes*/ 纳秒

                   }                                                                        

形参abstime:绝对时间。                                                                                     

如:time(NULL)返回的就是绝对时间。而alarm(1)是相对时间,相对当前时间定时1秒钟。   

                            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;    定义timespec 结构体变量t

                            t.tv_sec = cur+1; 定时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 <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <stdio.h>

/*链表作为公享数据,需被互斥量保护*/
struct msg {
    struct msg *next;
    int num;
};

struct msg *head;
struct msg *mp;

/* 静态初始化 一个条件变量 和 一个互斥量*/
pthread_cond_t has_product = PTHREAD_COND_INITIALIZER;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;

void *consumer(void *p)
{
    for (;;) {
        pthread_mutex_lock(&lock);
        while (head == NULL) {           //头指针为空,说明没有节点    可以为if吗
            pthread_cond_wait(&has_product, &lock);
        }
        mp = head;      
        head = mp->next;    //模拟消费掉一个产品
        pthread_mutex_unlock(&lock);

        printf("-Consume ---%d\n", mp->num);
        free(mp);
        mp = NULL;
        sleep(rand() % 5);
    }
}

void *producer(void *p)
{
    for (;;) {
        mp = malloc(sizeof(struct msg));
        mp->num = rand() % 1000 + 1;        //模拟生产一个产品
        printf("-Produce ---%d\n", mp->num);

        pthread_mutex_lock(&lock);
        mp->next = head;
        head = mp;
        pthread_mutex_unlock(&lock);

        pthread_cond_signal(&has_product);  //将等待在该条件变量上的一个线程唤醒
        sleep(rand() % 5);
    }
}

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

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

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

    return 0;
}

条件变量的优点:

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

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

5,信号量   

        由于互斥锁的粒度比较大,如果我们希望在多个线程间对某一对象的部分数据进行共享,使用互斥锁是没有办法实现的,只能将整个数据对象锁住。这样虽然达到了多线程操作共享数据时保证数据正确性的目的,却无形中导致线程的并发性下降。线程从并行执行,变成了串行执行。与直接使用单进程无异。信号量,是相对折中的一种处理方式,既能保证同步,数据不混乱,又能提高线程并发。信号量的初值,决定了占用信号量的线程的个数。     

      以下6 个函数的返回值都是:成功返回0,失败返回-1,同时设置errno。(注意,它们没有pthread前缀,既可以使用在线程又可以使用在进程)

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

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

  • 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);    

      1. 信号量大于0,则信号量--             

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

  • 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);  

 使用信号量完成生产者消费者问题的代码如下:


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

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

        信号量用在多线程多任务同步的,一个线程完成了某一个动作就通过信号量告诉别的线程,别的线程再进行某些动作(大家都在semtake的时候,就阻塞在哪里)。而互斥锁是用在多线程多任务互斥的,一个线程占用了某一个资源,那么别的线程就无法访问,直到这个线程unlock,其他的线程才开始可以利用这个资源。比如对全局变量的访问,有时要加锁,操作完了,在解锁。有的时候锁和信号量会同时使用的”。也就是说,信号量不一定是锁定某一个资源,而是流程上的概念,比如:有A,B两个线程,B线程要等A线程完成某一任务以后再进行自己下面的步骤,这个任务并不一定是锁定某一资源,还可以是进行一些计算或者数据处理之类。而线程互斥量则是“锁住某一资源”的概念,在锁定期间内,其他线程无法对被保护的数据进行操作。在有些情况下两者可以互换。

6,进程间同步

  •  信号量

        上面所讲的信号量是可以使用在进程同步中的。

  •  文件锁       

     借助 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 <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.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;
}

 

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值