线程同步与互斥

同步互斥概念

互斥:
  互斥就是指某一资源同时只能允许一个访问者对其进行访问,具有唯一性和排他性,但是互斥无法限制访问者对资源的访问顺序,即访问是无序的。
  对于线程来说,互斥就是说两个线程之间不可以同时运行,他们之间会相互排斥,必须等一个线程运行完毕之后,另一个才能运行。
同步:
  同步是指在互斥的基础上(大多数情况),通过其他机制实现访问者对资源的有序访问。在大多数情况下,同步已经实现了互斥,特别是所写入资源的情况必定是互斥的,少数情况可以是指允许多个访问者同时访问资源。
  对于线程来说,同步也是不能同时运行,但是它必须按照某种次序来运行相应。也就是按照一定的顺序运行线程,这种先后次序依赖于要完成的特定的任务。显然,同步是一种更复杂的互斥,互斥是一种特殊的同步。

线程间的同步互斥

mutex(互斥量)

  • 多个线程并发的操作共享变量,会带来一些问题
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>

int g_count = 0;

void* ThreadEntry1(void* arg){
    (void)arg;
    while(1){
        ++g_count;
        sleep(1);
    }
    return NULL;
}

void* ThreadEntry2(void* arg){
    (void)arg;
    while(1){
        printf("g_count = %d\n", g_count);
        sleep(1);
    }
    return NULL;
}

int main(){
    pthread_t tid1, tid2;
    pthread_create(&tid1, NULL, ThreadEntry1, p);
    pthread_create(&tid2, NULL, ThreadEntry2, p);
    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);
    return 0;
}

一个线程要把某个全局变量增加1另一个线程读出,这个操作并不是原子操作,至少需要三条指令才能完成:

  1. 将全局变量从内存中加载到寄存器
  2. 寄存器的值加1
  3. 将寄存器的值写回内存

由于存在线程安全问题,当线程一进行加一操作时,可能还未完成加一操作线程而便来读取,这将会导致与预期结果不同。

要解决以上问题,需要做到三点:

  • 代码必须要有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区。
  • 如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区。
  • 如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区。

此时引入互斥量的概念:
描述

(1)创建锁资源:
pthread_mutex_t mutex;
  创建锁资源就像创建变量一样,锁的类型是pthread_mutex_t。因为线程是共享数据区和堆区的,所以我们可以创建全局或静态的锁变量或者在堆上创建,这样就可以使得所有线程都可以看见锁,所以说实现线程间通信是非常简单的。
(2)初始化锁:
pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;
int pthread_mutex_init(pthread_mutex_t* mutex, const pthread_mutexattr_t* attr);
如果创建的是全局或静态的锁的话,可以用宏PTHREAD_MUTEX_INITIALIZER初始化。也可以用函数初始化。
attr:变量表示锁的属性。为NULL的话就相当于用宏初始化。
返回值:成功返回0,失败返回错误码。
(3)销毁锁:
int pthread_mutex_destroy(pthread_mutex_t* mutex);
成功返回0,失败返回错误码。
注意:

  • 使用PTHREAD_ MUTEX_ INITIALIZER初始化的互斥量不需要销毁
  • 不要销毁一个已经加锁的互斥量
  • 已经销毁的互斥量,要确保后面不会有线程再尝试加锁

(4)加锁:
int pthread_mutex_lock(pthread_mutex_t* mutex);
返回值:成功返回0,失败返回错误码。
(5)解锁:
int pthread_mutex_unlock(pthread_mutex_t *mutex);
返回值:成功返回0,失败返回错误码。
  一个线程可以调用pthread_mutex_lock获得锁,如果这时另一个线程先获得锁,则当前线程需要挂起等待,直到另一个线程调用pthread_mutex_unlock解锁,当前线程被唤醒,才能获得锁,并继续执行。可以看到,互斥锁就像轻量级的二元信号量一样,只能用在线程间,而信号量既可以用在线程间,也可以用在进程间。

死锁

死锁危害:线程一直等待,可能导致整个服务器崩溃
死锁的两个常见场景:

  • 一个线程获取到锁之后,又尝试获取锁,就会出现死锁
  • 两个线程A和B,线程A获取了锁1,线程B获取了锁2。然后A尝试获取锁2,B尝试获取锁1。这个时候双方都无法拿到对方的锁,并且会在获取锁的函数中阻塞等待
    如果线程数和锁的数目更多了,就会使死锁问题更容易出现,问题场景更复杂

对于这种需要获取多个锁的场景,规定所有的线程都按照固定的顺序来获取锁,能够一定程度上避免死锁。

避免死锁:临界区代码”短平快”

  • :临界区代码简短明了
  • :临界区代码逻辑清晰,没有复杂的函数调用,尤其是尽量不要申请其他互斥资源
  • :临界区代码执行速度快

线程安全与可重入

可重入函数:在多个执行流中被同时调用不会存在逻辑问题
线程安全函数:在多线程中被同时调用不会存在逻辑问题

  • 可重入函数一般情况下都是线程安全的
  • 线程安全函数不一定是可重入的
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <signal.h>

pthread_mutex_t g_lock;
int g_count = 0;

//Func函数线程安全,不可重入
void Func(){
    pthread_mutex_lock(&g_lock);
    printf("lock!\n");
    ++g_count;
    sleep(3);
    printf("unlock!\n");
    pthread_mutex_unlock(&g_lock);
}

void* ThreadEntry(void* arg){
    (void)arg;
    while(1){
        Func();
    }
    return NULL;
}

void MyHandler(int sig){
    (void)sig;
    Func();
}

int main(){
    signal(SIGINT, MyHandler);
    pthread_mutex_init(&g_lock, NULL);
    ThreadEntry(&g_count);
    pthread_mutex_destroy(&g_lock);
    return 0;
}

  在lock!打印完后按下Ctrl+c,这时进程收到此信号就会进入到信号处理函数中,也尝试获取锁,信号处理函数就会阻塞在加锁函数中。同时我们知道信号处理函数如果不执行结束,操作系统是不会切换回原有的执行逻辑的,这就意味着主线程再也没有机会释放锁了。因此对于这个场景下, Func函数是线程安全的,但是不可重入。

条件变量

为什么要有条件变量:
  线程间同步还有这样一种情况,线程A需要等待某个条件成立才能继续向下执行,现在这个条件不成立,线程A就被阻塞等待,而线程B在执行的过程中使这个条件成立了,就唤醒线程A继续执行。在pthread库中通过条件变量(Condition Variable)来阻塞等待一个条件,或唤醒等待这个条件的线程。

条件变量函数:
(1)条件变量的创建:
int pthread_cond_t cond;
  条件变量的创建和普通变量一样,为了能够实现共享条件变量,所以将条件变量创建为全局或静态变量,或者在堆上创建。

(2)条件变量的初始化:
条件变量和mutex是非常相似的,如果创建成全局或静态的,则可以使用宏来初始化:
pthread_cond_t cond=PTHREAD_COND_INITIALIZER;
还可以使用函数来初始化:
int pthread_cond_init(pthread_cond_t* cond, const pthread_condattr_t* attr);
返回值:成功返回0,失败返回错误码。
cond:要初始化的条件变量
attr:NULL

(3)条件变量的销毁:
int pthread_cond_destroy(pthread_cond_t* cond);
返回值:成功返回0,失败返回错误码。

(4)等待条件满足:
int pthread_cond_wait(pthread_cond_t* cond,pthread_mutex_t* mutex);
返回值:成功返回0,失败返回错误码。
cond:要在这个条件变量上等待
mutex:互斥量
  可以预想到,在wait中肯定是要释放锁的。如果解锁和等待不是原子操作的话,那么在解锁之后,等待之前,条件可能已经满足,信号已经发出,但是该信号可能被错过(竞态条件),导致永远阻塞在wait中。所以解锁和等待必须是一个原子操作。
描述

(5)唤醒等待:
int pthread_cond_broadcast(pthread_cond_t *cond);
返回值:成功返回0,失败返回错误码。
broadcast可以唤醒所有在这个cond上等待的线程。
int pthread_cond_signal(pthread_cond_t *cond);
返回值:成功返回0,失败返回错误码。
singnal可以唤醒某个在条件变量cond上等待的线程。在signal里面肯定是要再次获取锁资源的。

如:蓝球运动员的传球投篮,只有球传到运动员手中,他才可以去投篮

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

pthread_cond_t cond;
pthread_mutex_t lock;

void* Pass(void* arg){
    (void)arg;
    while(1){
        printf("传球!\n");
        pthread_cond_signal(&cond);
        usleep(789123);
    }
    return NULL;
}

void* Shot(void* arg){
    (void)arg;
    while(1){
        pthread_cond_wait(&cond, &lock);
        printf("投篮!\n");
        usleep(123456);
    }
    return NULL;
}

int main(){
    //模拟蓝球训练场景
    //球员1,球员2
    //球员1负责传球,球员2负责投篮
    pthread_t tid1, tid2;
    pthread_cond_init(&cond, NULL);
    pthread_mutex_init(&lock, NULL);
    pthread_create(&tid1, NULL, Pass, NULL);
    pthread_create(&tid2, NULL, Shot, NULL);
    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);
    pthread_mutex_destroy(&lock);
    pthread_cond_destroy(&cond);
    return 0;
}

读写锁

  在一些程序中存在读者写者问题,也就是说,对某些资源的访问会存在两种可能的情况:一种是访问必须是排它的,就是独占的意思,这称作写操作;另一种情况就是访问方式可以是共享的,就是说可以有多个线程同时去访问某个资源,这种就称作读操作。这个问题模型是从对文件的读写操作中引申出来的。
  通常而言,在读的过程中,往往伴随着查找操作,中间耗时很长,给这段代码加锁的话会极大的降低我们的效率。针对这种多读少写的情况,我们通常采用读写锁。 (写独占,读共享,写锁优先级高
  读写锁是本质上一种特殊的自旋锁,他把对共享资源的访问者划分成读者和写者,读者只对共享资源进行访问,写者只对共享资源进行写操作,一个读写锁同时只能有一个写者或多个读者,但是不能同时既有写者又有读者。 读写锁比起mutex具有更高的适用性,具有更高的并行性,可以有多个线程同时占用读模式的读写锁,但是只能有一个线程占用写模式的读写锁。
  自旋锁的作用是为了解决某项资源的互斥使用。因为自旋锁不会引起调用者睡眠,所以自旋锁的效率远高于互斥锁。虽然它的效率比互斥锁高,但是它也有些不足之处:

  1. 自旋锁一直占用CPU,他在未获得锁的情况下,一直运行,如果不能在很短的时间内获得锁,就会大大消耗CPU资源
  2. 在用自旋锁时有可能造成死锁,当递归调用时有可能造成死锁,调用有些其他函数也可能造成死锁,如 copy_to_user()、copy_from_user()、kmalloc()等。因此我们要慎重使用自旋锁,自旋锁只有在内核可抢占式或SMP的情况下才真正需要,在单CPU且不可抢占式的内核下,自旋锁的操作为空操作。自旋锁适用于锁使用者保持锁时间比较短的情况下。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>

pthread_rwlock_t rwlock;
int count = 0;

void* writer(void* arg){
    (void)arg;
    while(1){
        pthread_rwlock_wrlock(&rwlock);
        ++count;
        printf("write:%d\n", count);
        pthread_rwlock_unlock(&rwlock);
        sleep(1);
    }
    return NULL;
}

void* reader(void* arg){
    (void)arg;
    while(1){
        pthread_rwlock_rdlock(&rwlock);
        printf("read:%d\n", count);
        pthread_rwlock_unlock(&rwlock);
        sleep(1);
    }
    return NULL;
}

int main(){
    pthread_t tid[8];
    pthread_rwlock_init(&rwlock, NULL);
    int i = 0;
    for(; i < 3; ++i){
        pthread_create(&tid[i], NULL, writer, NULL);
    }
    for(i = 0; i < 5; ++i){
        pthread_create(&tid[i], NULL, reader, NULL);
    }
    for(i = 0; i < 8; ++i){
        pthread_join(tid[i], NULL);
    }
    pthread_rwlock_destroy(&rwlock);
    return 0;
}
  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值