Liunx线程同步基础

目录

线程同步

数据混乱的原因

互斥锁 mutex

创建互斥锁 pthread_mutex_init函数

销毁互斥锁 pthread_mutex_destroy函数

加锁操作 pthread_mutex_lock函数

加锁操作 pthread_mutex_unlock函数

尝试加锁操作 pthread_mutex_trylock函数

互斥锁使用技巧

练习:利用互斥锁访问临界资源

多线程经典问题:死锁

容易发生死锁的情况

避免方法

读写锁

创建读写锁 pthread_rwlock_init函数

销毁读写锁 pthread_rwlock_destroy函数

请求读锁 pthread_rwlock_rdlock函数

请求写锁 pthread_rwlock_wrlock函数

 尝试请求读锁 pthread_rwlock_rdlock函数

尝试请求写锁 pthread_rwlock_wrlock函数

解锁 pthread_rwlock_unlock 函数

练习:利用读写锁对全局变量进行互斥访问

条件变量

创建条件变量 pthread_cond_init函数

销毁条件变量 pthread_cond_destroy函数

阻塞等待 pthread_cond_wait函数

限时等待 pthread_cond_timedwait函数

唤醒阻塞在条件变量的线程 pthread_cond_signal函数

广播唤醒阻塞在条件变量的线程 pthread_cond_broadcast函数

线程同步经典模型 - 生产者消费者模型

条件变量的优点

练习:生产者消费者条件变量模型

信号量

创建信号量 sem_init函数

销毁信号量 sem_destroy函数

加锁信号量 sem_wait函数

解锁信号量 sem_post函数

尝试加锁信号量 sem_trywait函数

限时尝试加锁信号量 sem_timedwait 函数

练习:信号量实现的生产者消费者


线程同步

线程同步,指一个线程发出某一功能调用时,在没有得到结果之前,该调用不返回。同时其它线程为保证数据一致性,不能调用该功能。
作用:协同步调,对公共资源(临界资源)【按序】访问。防止数据混乱,产生与时间有关的错误

数据混乱的原因

<1> 资源共享(独享资源则不会)
<2> 调度随机(意味着数据访问会出现竞争)
<3> 线程间缺乏必要同步机制

互斥锁 mutex

Linux 中提供一把互斥锁 mutex(也称之为互斥量)。
每个线程在对资源操作前都尝试先加锁,成功加锁才能操作,操作结束解锁。
同一时刻,只能有一个线程持有该锁。
锁本质为建议锁!对公共数据进行保护。所有线程【应该】在访问公共数据前先拿锁再访问。但,锁本身不具备强制性。

创建互斥锁 pthread_mutex_init函数

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

#include <pthread.h>

pthread_mutex_t mutex;

int pthread_mutex_init(pthread_mutex_t *restrict mutex,
             const pthread_mutexattr_t *restrict attr);
参1:传出参数,调用时应传&mutex ---> pthread_mutex_t mutex
参 2:互斥量属性。是一个传入参数,通常传 NULL,选用默认属性(线程间共享)
返回值:成功:0
       失败:错误号

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

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

销毁互斥锁 pthread_mutex_destroy函数

#include <pthread.h>

int pthread_mutex_destroy(pthread_mutex_t *mutex);
返回值:成功:0
       失败:错误号

加锁操作 pthread_mutex_lock函数

加锁。 可理解为将 mutex--(或 -1),操作后 mutex 的值为 0
lock 尝试加锁,如果加锁不成功,线程阻塞,阻塞到持有该互斥量的其他线程解锁为止。

#include <pthread.h>

int pthread_mutex_lock(pthread_mutex_t *mutex);
返回值:成功:0
       失败:错误号

加锁操作 pthread_mutex_unlock函数

解锁。 可理解为将 mutex ++(或 +1),操作后 mutex 的值为 1
unlock 主动解锁函数, 同时将阻塞在该锁上的所有线程全部唤醒,至于哪个线程先被唤醒,取决于优先级、调度。默认:先阻塞、先唤醒。

#include <pthread.h>

int pthread_mutex_unlock(pthread_mutex_t *mutex);
返回值:成功:0
       失败:错误号

尝试加锁操作 pthread_mutex_trylock函数

trylock 加锁失败直接返回错误号(如: EBUSY),不阻塞。

#include <pthread.h>

int pthread_mutex_trylock(pthread_mutex_t *mutex);
返回值: 成功:0
       失败:错误号

互斥锁使用技巧

注意事项:
尽量保证锁的粒度, 越小越好。(访问共享数据前,加锁。访问结束【立即】解锁。)
互斥锁,本质是结构体。 方便理解可以看成整数。 初值为 1。(pthread_mutex_init() 函数调用成功。)

练习:利用互斥锁访问临界资源

使用mutex(互斥量、互斥锁)一般步骤:

1. pthread_mutex_t lock; 创建锁
2 pthread_mutex_init; 初始化 1
3. pthread_mutex_lock;加锁 1-- --> 0
4. 访问共享数据(stdout)
5. pthrad_mutext_unlock();解锁 0++ --> 1
6. pthead_mutex_destroy;销毁锁

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) {  
     pthread_t tid;  
     srand(time(NULL));  
     int ret = pthread_mutex_init(&mutex, NULL);    // 初始化互斥锁  
     if(ret != 0){  
         fprintf(stderr, "mutex init error:%s\n", strerror(ret));  
         exit(1);  
     }  
     pthread_create(&tid, NULL, tfn, NULL);  
     while (1) {  
         pthread_mutex_lock(&mutex);     // 加锁  
         printf("HELLO ");  
         sleep(rand() % 3);  
         printf("WORLD\n");  
         pthread_mutex_unlock(&mutex);   // 解锁  
         sleep(rand() % 3);  
     }  
     pthread_join(tid, NULL);  
     pthread_mutex_destory(&mutex);     // 销毁互斥锁  
     return 0;  
} 

多线程经典问题:死锁

死锁:在并发环境下,各进程因竞争资源而造成的一种互相等待对方手里的资源,导致各进程都阻塞,都无法向前推进的现象,就是"死锁"。
发生死锁后若无外力干涉,这些进程都将无法向前推进。

容易发生死锁的情况

1. 线程试图对同一个互斥量 A 加锁两次。
2. 线程 1 拥有 A 锁,请求获得 B 锁;线程 2 拥有 B 锁,请求获得 A 锁

避免方法

<1> 保证资源的获取顺序,要求每个线程获取资源的顺序一致
<2> 当得不到所有所需资源时,放弃已经获得的资源,等待。

读写锁

与互斥量类似,但读写锁允许更高的并行性。其特性为: 写独占,读共享。但相较于互斥锁而言,当读线程多的时候,提高访问效率
特别强调: 读写锁只有一把, 但其具备两种状态:
1. 线程以读模式请求加锁状态 (读锁)
2. 线程以写模式请求加锁状态 (写锁)
读锁、写锁并行阻塞写锁优先级高

创建读写锁 pthread_rwlock_init函数

pthread_rwlock_t rwlock

int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, 
                const pthread_rwlockattr_t *restrict attr);
参 2: attr 表读写锁属性,通常使用默认属性, 传 NULL 即可。
返回值:成功返回0,失败返回错误号

销毁读写锁 pthread_rwlock_destroy函数

int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
返回值:成功返回0,失败返回错误号

请求读锁 pthread_rwlock_rdlock函数

int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
返回值:成功返回0,失败返回错误号

请求写锁 pthread_rwlock_wrlock函数

int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
返回值:成功返回0,失败返回错误号

 尝试请求读锁 pthread_rwlock_rdlock函数

int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
返回值:成功返回0,失败返回错误号

尝试请求写锁 pthread_rwlock_wrlock函数

int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
返回值:成功返回0,失败返回错误号

解锁 pthread_rwlock_unlock 函数

int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
返回值:成功返回0,失败返回错误号

练习:利用读写锁对全局变量进行互斥访问

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

int counter;
pthread_rwlock_t rwlock;

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

    while(1){
        pthread_rwlock_wrlock(&rwlock);
        t = counter;
        usleep(1000);
        printf("=========write %d: %lu: counter=%d ++counter=%d\n", i, pthread_self(), t,++counter);
        pthread_rwlock_unlock(&rwlock);
        usleep(1000);
    }
    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(2000);
    }
    return NULL;
}

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

    pthread_t tid[8];
    int i, ret;

    pthread_rwlock_init(&rwlock, NULL);

    for(i = 0;i<3;i++){
        ret = pthread_create(&tid[i],NULL,th_write ,(void *)i);
        if(ret != 0){
            fprintf(stderr, "pthread_create error:%s\n", strerror(ret));
            exit(1);
        }
    }

    for(i = 0; i < 5; i++){
        ret = pthread_create(&tid[i+3], NULL, th_read, (void *)i);
        if(ret != 0){
            fprintf(stderr, "pthread_create error:%s\n", strerror(ret));
            exit(1);
        }
    }

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

    pthread_rwlock_destroy(&rwlock);
    return 0;
}

条件变量

条件变量本身不是锁!但它也可以造成线程阻塞。通常与互斥锁配合使用。

创建条件变量 pthread_cond_init函数

#include <pthread.h>


pthread_cond_t cond; //用于定义条件变量

int pthread_cond_init(pthread_cond_t *restrict cond, 
            const pthread_condattr_t *restrict attr);
参 2: attr 表条件变量属性,通常为默认值,传 NULL 即可
返回值:成功:0
       失败: 错误号

可以使用静态初始化的方法, 初始化条件变量:
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

销毁条件变量 pthread_cond_destroy函数

int pthread_cond_destroy(pthread_cond_t *cond);
返回值:成功:0
       失败: 错误号

阻塞等待 pthread_cond_wait函数

1) 阻塞等待条件变量满足
2) 解锁已经加锁成功的信号量 (相当于 pthread_mutex_unlock(&mutex)),1、2两步为一个原子操作
3) 当条件满足,函数返回时,解除阻塞并重新申请获取互斥锁。重新加锁信号量 (相当于, pthread_mutex_lock(&mutex);)

int pthread_cond_wait(pthread_cond_t *restrict cond, 
                    pthread_mutex_t *restrict mutex);
返回值:成功:0
       失败: 错误号

限时等待 pthread_cond_timedwait函数

int pthread_cond_timedwait(pthread_cond_t *restrict cond, 
                pthread_mutex_t *restrict mutex, 
                const struct timespec *restrict abstime);

参三: 参看man sem_timedwait 函数,查看 struct timespec 结构体。
struct timespec {
    time_t tv_sec; /* seconds */ 秒
    long tv_nsec; /* nanosecondes*/ 纳秒
}
形参abstime:绝对时间。相对于 1970 年 1 月 1 日 00:00:00 秒的时间
正确传参:
    time_t cur = time(NULL); 获取当前时间。
    struct timespec t; 定义 timespec 结构体变量 t
    t.tv_sec = cur+1; 定时 1 秒
    pthread_cond_timedwait (&cond, &mutex, &t); 传参

返回值:成功:0
       失败: 错误号

唤醒阻塞在条件变量的线程 pthread_cond_signal函数

唤醒至少一个阻塞在条件变量上的线程

int pthread_cond_signal(pthread_cond_t *cond);
返回值:成功:0
      失败:错误号

广播唤醒阻塞在条件变量的线程 pthread_cond_broadcast函数

唤醒全部阻塞在条件变量上的线程

int pthread_cond_broadcast(pthread_cond_t *cond);
返回值:成功:0
       失败:错误号

线程同步经典模型 - 生产者消费者模型

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

条件变量的优点

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

练习:生产者消费者条件变量模型

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

struct tmp{
    struct tmp *next;
    int val;
};

struct tmp *head;

pthread_cond_t tcond = PTHREAD_COND_INITIALIZER;
pthread_mutex_t tmutex = PTHREAD_MUTEX_INITIALIZER;

void err_thread(const char* str, int ret){
    if(ret != 0){
        fprintf(stderr, "%s: %s\n", str, strerror(ret));
        pthread_exit(NULL);
    }
}

void *product(void *arg){

    for(;;){
        struct tmp *ptmp = malloc(sizeof(struct tmp));
        ptmp->val = rand() %1000 + 1;
        printf("------prosuct %d\n", ptmp->val);
        pthread_mutex_lock(&tmutex);
        ptmp->next = head;
        head = ptmp;
        pthread_mutex_unlock(&tmutex);
        pthread_cond_signal(&tcond);
        sleep(rand()%3);
    }
    return NULL;
}

void *consumer(void *arg){
    for(;;){
        struct tmp *ctmp;

        pthread_mutex_lock(&tmutex);
        while(head == NULL){
            pthread_cond_wait(&tcond, &tmutex);
        }
        ctmp = head;
        head = ctmp->next;
        pthread_mutex_unlock(&tmutex);
        printf("---------------consumer%lu : %d\n",pthread_self(), ctmp->val);
        free(ctmp);
        sleep(rand()%3);
    }

    return NULL;
}

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

    pthread_t pid, cid[3];
    int ret, i;

    srand(time(NULL));

    ret = pthread_create(&pid, NULL, product, NULL);
    err_thread("pthread_create error", ret);

    for(i = 0;i < 3; i++){
        ret = pthread_create(&cid[i], NULL, consumer, NULL);
        err_thread("pthread_create error", ret);
    }

    ret = pthread_join(pid, NULL);
    err_thread("pthread_join error", ret);
    for(i = 0; i< 3;i++){
        ret = pthread_join(cid[i],NULL);
        err_thread("pthread_join error", ret);
    }

    return 0;
}

信号量

应用于线程、进程间同步。
相当于 初始化值为 N 的互斥量。 N值,表示可以同时访问共享数据区的线程数。

创建信号量 sem_init函数

#include <semaphore.h>

sem_t sem;  规定信号量 sem 不能 < 0

int sem_init(sem_t *sem, int pshared, unsigned int value);
参 1: sem 信号量
参 2: pshared 取 0 用于线程间;取非 0(一般为 1)用于进程间
参 3: value 指定信号量初值

返回值:成功返回 0, 
       失败返回-1,同时设置 errno

销毁信号量 sem_destroy函数

#include <semaphore.h>

int sem_destroy(sem_t *sem);
返回值:成功返回 0, 
       失败返回-1,同时设置 errno

加锁信号量 sem_wait函数

一次调用,做一次-- 操作, 当信号量的值为 0 时,再次 -- 就会阻塞。

int sem_wait(sem_t *sem);

返回值:成功返回 0, 
       失败返回-1,同时设置 errno

解锁信号量 sem_post函数

一次调用,做一次++ 操作. 当信号量的值为 N 时, 再次 ++ 就会阻塞。

int sem_post(sem_t *sem);

返回值:成功返回 0, 
       失败返回-1,同时设置 errno

尝试加锁信号量 sem_trywait函数

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

int sem_trywait(sem_t *sem);

返回值:成功返回 0, 
       失败返回-1,同时设置 errno

限时尝试加锁信号量 sem_timedwait 函数

int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
参 2: abs_timeout 采用的是绝对时间。

返回值:成功返回 0, 
       失败返回-1,同时设置 errno

定时 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 <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <semaphore.h>
#include <pthread.h>

#define NUM 5

int queue[NUM];
pthread_mutex_t mutex =  PTHREAD_MUTEX_INITIALIZER;
sem_t blank_number, product_number;

void err_printf(const char* str,int ret){
    if(ret != 0){
        fprintf(stderr, "%s:%s\n", str, strerror(ret));
        pthread_exit(NULL);
    }
}

void *product(void *arg){
    int i = 0;
    while(1){
        sem_wait(&blank_number);
        queue[i] = rand()% 1000+1;
        printf("======product=====%d\n", queue[i]);
        sem_post(&product_number);

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

void *consumer(void *arg){
    int i =0;
    while(1){
        sem_wait(&product_number);
        printf("-----------consumer------%d\n", queue[i]);
        queue[i] = 0;
        sem_post(&blank_number);

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

    return NULL;
}

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

    pthread_t pid, cid;
    int ret;

    sem_init(&blank_number, 0 , NUM);
    sem_init(&product_number, 0, 0);


    ret = pthread_create(&pid, NULL, product, NULL);
    err_printf("pthread_create error", ret);

    ret = pthread_create(&cid, NULL, consumer, NULL);
    err_printf("pthread_create error", ret);
     
    pthread_join(pid, NULL);
    pthread_join(cid, NULL);

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

    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值