Skip to content

线程同步

线程同步,指一个线程发出某一功能调用时,在没有得到结果之前,该调用不返回。同时其它线程为保证数据一致性,不能调用该功能。

线程同步的例子

创建两个线程,让两个线程共享一个全局变量int number, 然后让每个线程数5000次数,看最后打印出这个number值是多少?

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

int num = 0;

void threadMain(void *arg) {
    for (int i = 0; i < 50000; ++i) {
        ++num;
        usleep(10);
    }

    printf("Thread finish, num=%d.\n", num);

    pthread_exit((void *) 0);
}

int main(int argc, char const *argv[]) {
    pthread_t tid;
    pthread_create(&tid, NULL, (void *) threadMain, NULL);

    for (int i = 0; i < 50000; ++i) {
        ++num;
        usleep(10);
    }

    pthread_join(tid, NULL);

    printf("Main finish, num=%d.\n", num);

    return 0;
}

代码片段说明

  • 代码中使用调用usleep是为了让两个子线程能够轮流使用CPU,避免一个子线程在一个时间片内完成50000次数数。
  • number执行++操作,使用了中间变量cur是为了尽可能的模拟cpu时间片用完而让出cpu的情况。

测试结果

  • 经过多次测试最后的结果显示,有可能会出现number值少于50000*2=100000的情况。

分析原因

  • 假如子线程A执行完了cur++操作,还没有将cur的值赋值给number失去了cpu的执行权,子线程B得到了cpu执行权,而子线程B最后执行完了number=cur,而后失去了cpu的执行权;此时子线程A又重新得到cpu的执行权,并执行number=cur操作,这样会把线程B刚刚写回number的值被覆盖了,造成number值不符合预期的值。

数据混乱的原因

  • 资源共享(独享资源则不会)
  • 调度随机(线程操作共享资源的先后顺序不确定)
  • 线程间缺乏必要的同步机制。

以上3点中,前两点不能改变,欲提高效率,传递数据,资源必须共享。只要共享资源,就一定会出现竞争。只要存在竞争关系,数据就很容易出现混乱。所以只能从第三点着手解决。使多个线程在访问共享资源的时候,出现互斥。

如何解决问题

原子操作的概念:原子操作指的是该操作要么不做,要么就完成。

使用互斥锁解决同步问题:使用互斥锁其实是模拟原子操作,互斥锁示意图:

Linux中提供一把互斥锁mutex(也称之为互斥量)。每个线程在对资源操作前都尝试先加锁,成功加锁才能操作,操作结束解锁。

资源还是共享的,线程间也还是竞争的,但通过“锁”就将资源的访问变成互斥操作,而后与时间有关的错误也不会再产生了。

线程1访问共享资源的时候要先判断锁是否锁着,如果锁着就阻塞等待;若锁是解开的就将这把锁加锁,此时可以访问共享资源,访问完成后释放锁,这样其他线程就有机会获得锁。

应该注意:图中同一时刻,只能有一个线程持有该锁,只要该线程未完成操作就不释放锁。

使用互斥锁之后,两个线程由并行操作变成了串行操作,效率降低了,但是数据不一致的问题得到解决了。

互斥锁

互斥锁主要相关函数

pthread_mutex_t 类型

  • 其本质是一个结构体,为简化理解,应用时可忽略其实现细节,简单当成整数看待。
  • pthread_mutex_t mutex; 变量mutex只有两种取值1、0。

pthread_mutex_init

函数描述

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

函数原型

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

函数参数

  • mutex:传出参数,调用时应传 &mutex
  • attr:互斥锁属性。是一个传入参数,通常传NULL,选用默认属性(线程间共享)。

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

  • 静态初始化:如果互斥锁 mutex 是静态分配的(定义在全局,或加了static关键字修饰),可以直接使用宏进行初始化。
cpp
pthead_mutex_t muetx = PTHREAD_MUTEX_INITIALIZER;
  • 动态初始化:局部变量应采用动态初始化。
pthread_mutex_init(&mutex, NULL);

pthread_mutex_destroy

函数描述

  • 销毁一个互斥锁

函数原型

cpp
int pthread_mutex_destroy(pthread_mutex_t *mutex);

函数参数

  • mutex—互斥锁变量

pthread_mutex_lock

函数描述

  • 对互斥所加锁,可理解为将mutex--

函数原型

cpp
int pthread_mutex_lock(pthread_mutex_t *mutex);

函数参数

  • mutex—互斥锁变量

pthread_mutex_unlock

  • 函数描述
  • 对互斥所解锁,可理解为将mutex++

函数原型

cpp
int pthread_mutex_unlock(pthread_mutex_t *mutex);

pthread_mutex_trylock

函数描述

  • 尝试加锁

函数原型

cpp
int pthread_mutex_trylock(pthread_mutex_t *mutex);

函数参数

  • mutex—互斥锁变量

加锁和解锁

  • lock尝试加锁,如果加锁不成功,线程阻塞,阻塞到持有该互斥量的其他线程解锁为止。
  • unlock主动解锁函数,同时将阻塞在该锁上的所有线程全部唤醒,至于哪个线程先被唤醒,取决于优先级、调度。默认:先阻塞、先唤醒。

练习:使用互斥锁解决两个线程数数不一致的问题。

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

总结:使用互斥锁之后,两个线程由并行变为了串行,效率降低了,但是可以使两个线程同步操作共享资源,从而解决了数据不一致的问题。

互斥锁的使用步骤

  1. 定义一把互斥锁,应该为一全局变量

    pthread_mutex_t mutex;

  2. 在main函数中对mutex进行初始化

    pthread_mutex_init(&mutex, NULL);

  3. 创建两个线程,在两个线程中加锁和解锁

    • 访问共享资源前加锁

      pthread_mutex_lock(&mutex)

    • 访问共享资源后解锁

      pthread_mutex_unlock(&mutex)

  4. 使用完毕后销毁互斥锁

    pthread_mutex_destroy(&mutex);

注意:必须在所有操作共享资源的线程上都加上锁否则不能起到同步的效果。

练习

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

int num = 0;
// 1.创建互斥锁
pthread_mutex_t t;

void threadMain(void *arg) {
    for (int i = 0; i < 50000; ++i) {
        // 3.使用互斥锁
        pthread_mutex_lock(&t);
        ++num;
        pthread_mutex_unlock(&t);
        usleep(10);
    }

    printf("Thread finish, num=%d.\n", num);

    pthread_exit((void *) 0);
}

int main(int argc, char const *argv[]) {
    // 2.初始化互斥锁
    pthread_mutex_init(&t, NULL);

    pthread_t tid;
    pthread_create(&tid, NULL, (void *) threadMain, NULL);

    for (int i = 0; i < 50000; ++i) {
        // 3.使用互斥锁
        pthread_mutex_lock(&t);
        ++num;
        pthread_mutex_unlock(&t);
        usleep(10);
    }

    pthread_join(tid, NULL);

    // 4.销毁互斥锁
    pthread_mutex_destroy(&t);

    printf("Main finish, num=%d.\n", num);

    return 0;
}

经过使用互斥锁改造上面的代码,总能得到预期的结果50000*2=100000

死锁

死锁并不是linux提供给用户的一种使用方法,而是由于用户使用互斥锁不当引起的一种现象。

常见的死锁有两种

  • 第一种:自己已经加锁了在解锁之前再次加锁,如下图代码片段

  • 第二种 线程A拥有A锁,请求获得B锁;线程B拥有B锁,请求获得A锁,这样造成线程A和线程B都不释放自己的锁,而且还想得到对方的锁,从而产生死锁,如下图所示:

  • 第三种 两个线程对共享资源进行访问,正常加锁解锁都没有问题,但是 拥有锁的一方突然挂了也会产生死锁

如何解决死锁

  • 让线程按照一定的顺序去访问共享资源
  • 在访问其他锁的时候,需要先将自己的锁解开
  • 调用pthread_mutex_trylock,如果加锁不成功会立刻返回

读写锁

什么是读写锁

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

读写锁使用场合

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

读写锁特性

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

读写锁场景练习

  • 线程A加写锁成功,线程B请求读锁
    • 线程B阻塞
  • 线程A持有读锁,线程B请求写锁
    • 线程B阻塞
  • 线程A拥有读锁,线程B请求读锁
    • 线程B加锁成功
  • 线程A持有读锁,然后线程B请求写锁,然后线程C请求读锁
    • B阻塞,c阻塞 - 写的优先级高
    • A解锁,B线程加写锁成功,C继续阻塞
    • B解锁,C加读锁成功
  • 线程A持有写锁,然后线程B请求读锁,然后线程C请求写锁
    • BC阻塞
    • A解锁,C加写锁成功,B继续阻塞
    • C解锁,B加读锁成功

读写锁总结

读并行,写独占,当读写同时等待锁的时候写的优先级高

读写锁主要操作函数

  • 定义一把读写锁
c
pthread_rwlock_t rwlock;
  • 初始化读写锁
c
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);
  • rwlock-读写锁

  • attr-读写锁属性,传NULL为默认属性

  • 销毁读写锁

cpp
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
  • 加读锁
cpp
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
  • 尝试加读锁
cpp
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
  • 加写锁
cpp
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
  • 尝试加写锁
c
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
  • 解锁
c
int pthread_rwlock_unlock(&pthread_rwlock_t *rwlock);

练习

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

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

// 定义共享数据
int num = 0;
// 1.定义读写锁
pthread_rwlock_t rwlock;

void threadRead(void *arg) {
    unsigned long tid = pthread_self();

    for (int i = 0; i < 50000; ++i) {
        // 3.对共享数据读取时加读锁
        pthread_rwlock_rdlock(&rwlock);
        printf("tid=%ld, num=%d\n", tid, num);
        // 3.对共享数据访问完毕后解锁
        pthread_rwlock_unlock(&rwlock);
        usleep(rand() % 100);
    }

    pthread_exit(NULL);
}

void threadWrite(void *arg) {
    for (int i = 0; i < 50000; ++i) {
        // 3.对共享数据写入时加写锁
        pthread_rwlock_wrlock(&rwlock);
        ++num;
        // 3.对共享数据访问完毕后解锁
        pthread_rwlock_unlock(&rwlock);
        usleep(rand() % 100);
    }

    pthread_exit(NULL);
}

int main(int argc, char const *argv[]) {
    // 2.初始化读写锁
    pthread_rwlock_init(&rwlock, NULL);
    srand(time(NULL));

    for (int i = 0; i < 3; ++i) {
        pthread_t tid;
        pthread_create(&tid, NULL, (void *) threadWrite, NULL);
    }

    for (int i = 0; i < 5; ++i) {
        pthread_t tid;
        pthread_create(&tid, NULL, (void *) threadRead, NULL);
    }

    // 使主线程休眠10秒等待其他线程退出
    sleep(10);

    // 4.使用完毕后销毁读写锁
    pthread_rwlock_destroy(&rwlock);

    return 0;
}

条件变量

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

使用互斥量保护共享数据;

使用条件变量可以使线程阻塞,等待某个条件的发生,当条件满足的时候解除阻塞.

条件变量的两个动作

  • 条件不满足,阻塞线程
  • 条件满足,通知阻塞的线程解除阻塞,开始工作.

pthread_cond_t cond;

条件变量相关函数

c
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);

函数描述

  • 初始化条件变量,定义一个条件变量

函数参数

  • cond:条件变量
  • attr:条件变量属性,通常传NULL

函数返回值

  • 成功返回0,失败返回错误号
c
int pthread_cond_destroy(pthread_cond_t *cond);

函数描述

  • 销毁条件变量

函数参数

  • 条件变量

返回值

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

函数描述

  • 条件不满足,引起线程阻塞并解锁;
  • 条件满足,解除线程阻塞,并加锁

函数参数

  • cond:条件变量
  • mutex:互斥锁变量

函数返回值

  • 成功返回0,失败返回错误号
c
int pthread_cond_signal(pthread_cond_t *cond);

函数描述

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

函数参数

  • 条件变量

函数返回值

  • 成功返回0,失败返回错误号

代码片段

上述代码中,生产者线程调用pthread_cond_signal函数会使消费者线程在pthread_cond_wait处解除阻塞。