Linux - 线程的同步与互斥操作

同步与互斥概念

同步: 多个线程同时运行, 线程与线程之间可能存在某种关系, 需要让线程按照某种顺序进行执行.

如: 线程 A 产生需要处理的数据 X, 而处理这个数据的任务交给了线程 B. 那么线程 A, B 之间的运行关系应该是, A 线程先运行, 在 A 产生了数据 X 之后, B 线程在运行进行处理

互斥: 线程共享同一进程内的资源, 一个共享资源在被多个线程同时访问时, 就有可能会出现问题. 所以, 为了应对这种情况, 就需要让这些进程互斥的去访问这个共享资源.

如: 多个线程同时访问同一个文件, 然后同时向文件内写入数据, 在这种场景下, 每个线程存放的数据都有可能会被打乱, 导致文件中存储的数据不完整/无法识别. 所以我们就要让这些线程互斥的去向文件中写入数据.

锁的概念

在上面了解了同步和互斥的基本概念. 在了解锁之前还需要了解一些其他概念.

临界区资源: 被多个线程共享访问的资源就称为临界资源

临界区: 在每个线程内部, 访问临界区资源的代码, 就被称为临界区

原子性: 不会被打断的操作, 这个操作只有两种状态, 要么完成, 要么未完成 (不存在完成一部分)

大部分情况下, 线程使用的都是线程内的局部变量, 这些变量存储在线程的栈空间, 其他线程无法访问. 但也存在多线程共享某一个资源.

如经典例子: 卖票系统, 一共由10 0000张票, 两个线程同时售卖这10 0000张票.

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

int sum = 100000000;

void* func1(void* arg)
{
    int x = 0;
    while(sum > 0)
    {
        --sum;
    }
    return NULL;
}

int main()
{
    pthread_t t1;
    pthread_create(&t1, NULL, func1, NULL);
    pthread_t t2;
    pthread_create(&t2, NULL, func1, NULL);
    pthread_t t3;
    pthread_create(&t3, NULL, func1, NULL);
    pthread_join(t1, NULL);
    pthread_join(t2, NULL);
    pthread_join(t3, NULL);
    printf("sum: %d\n", sum);
    return 0;
}

会得到 sum 最后的结果为 -1. 

我们来分析一下:

那么为了解决上面的问题: 就出现了锁.

对于多个线程都要访问的共享资源, 要求最多只能由一个线程进行访问. 当有线程已经在访问共享资源之后, 其他进程就不能进入访问. 互斥锁就能完成这样的功能.

在同一时间内, 最多只能有一个线程获得锁, 然后向后执行代码, 其他没有获得锁的线程, 就会在获取锁的地方进行阻塞, 直到锁被释放并获取到锁之后, 才会继续向后执行. 

此时, 这些线程通过竞争这把锁, 它们之间就形成了互斥关系.

互斥锁的使用

1. 初始化互斥锁

初始化锁有两种方式: 静态初始化 和 动态初始化

#include <pthread.h>

// 静态初始化
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

// 动态初始化
pthread_mutex_t mutex;
if (pthread_mutex_init(&mutex, NULL) != 0)
{
    perror("pthread_mutex_init");
}

2. 加锁. 获得锁之后访问共享资源

使用函数 pthread_mutex_lock() 函数进行加锁

pthread_mutex_lock(&mutex);

// 访问共享资源

如果获得了锁, 那么就会继续向后执行代码.

如果锁已经被其他线程获取, 那么线程就会被阻塞在这里, 直到锁被释放, 然后再一次竞争锁.

3. 释放锁

释放锁需要使用函数 pthread_mutex_unlock() 函数来解锁.

pthread_mutex_unlock(&mutex);

调用这个函数后, 线程就释放了获取的锁, 如果想要再次获得锁, 就需要和所有的线程去竞争

4. 销毁锁

当不需要使用锁后, 可以使用函数 pthread_mutex_destroy() 函数销毁, 释放资源.

if (pthread_mutex_destroy(&mutex) != 0)
{
    perror("pthread_mutex_destroy");
}

如果是通过静态初始化得到的锁, 不需要销毁. 通过动态初始化得到的锁才需要使用这个函数进行销毁.

那么现在就能对上面的代码进行优化了: 

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

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
int sum = 100000000;

void* func1(void* arg)
{
    int x = 0;
    pthread_mutex_lock(&mutex);
    while(sum > 0)
    {
        --sum;
    }
    pthread_mutex_unlock(&mutex);
    return NULL;
}

int main()
{
    pthread_t t1;
    pthread_create(&t1, NULL, func1, NULL);
    pthread_t t2;
    pthread_create(&t2, NULL, func1, NULL);
    pthread_t t3;
    pthread_create(&t3, NULL, func1, NULL);
    pthread_join(t1, NULL);
    pthread_join(t2, NULL);
    pthread_join(t3, NULL);
    printf("sum: %d\n", sum);
    return 0;
}

当加入了互斥锁之后, 对于共享资源的访问就变得更加安全可控了.

线程同步

上面我们使用互斥锁让线程对于共享资源的访问变得更加安全可控. 

在上面的说明中也了解到了, 线程同步就是让线程按一定的顺序来运行.
如: 一个线程先生产数据, 当数据生产完成后, 由另一个线程来进行处理.

那么线程生产了数据后, 就要放入共享资源中, 这样才能被其他的线程访问并获取.
这样就需要使用锁, 线程不断地访问共享资源, 检测数据是否被生产出来, 这就需要不断地加锁解锁.由于就近原则, 这个消费线程释放锁后, 离锁更近, 就有更大的可能性获得锁. 这样下去就有可能导致生产线程长久无法获得锁.

在上面的例子中, 我们需要让生产线程先运行生产数据, 当数据产生后, 再让消费线程开始运行.

这里可以引入: 条件变量 (Condition Variable). 条件变量通常和互斥锁一起使用

1. 初始化条件变量

同样也分为两种: 静态初始化 和 动态初始化.

pthread_cond_t cond; // 定义条件变量

// 1. 静态初始化
cond = PTHREAD_COND_INITIALIZER;

// 2. 动态初始化
pthread_cond_init(&cond, NULL)

2. 等待条件变量

调用函数 pthread_cond_wait() 来进行等待.

pthread_mutex_lock(&mutex); // 加锁
while(!condition) // 当条件不符合时, 调用 pthread_cond_wait() 函数进行等待
{
    pthread_cond_wait(&cond, &mutex); // 等待条件变量, 此时线程就在这里进行了等待
}                                     // 直到被唤醒, 然后线程才会向下继续向下执行
pthread_mutex_unlock(&mutex);

// 在调用 pthread_cond_wait() 进行等待后, mutex 锁会被释放, 后续可以被其他线程竞争锁
// 当线程被唤醒后, 线程会重新去竞争锁, 当竞争成功后, 才会继续执行, 否则还是会被阻塞

3. 通知条件变量

有两种方法通知正在等待的线程

1. pthread_cond_signal() 函数: 唤醒正在等待的某一个线程

2. pthread_cond_broadcast() 函数: 唤醒所有在等待的线程

pthread_mutex_lock(&mutex);  // 加锁, 并不会阻塞在这里, 因为上面的 wait 等将 mutex 释放
condition = 1;
pthread_cond_signal(&cond);  // 通知单个线程, 然后本线程继续向后执行
// 或者使用函数 pthread_cond_broadcast(&cond);  通知所有线程, 然后继续向后执行
pthread_mutex_unlock(&mutex);  // 解锁

4. 销毁条件变量

调用函数 pthread_cond_destroy() 进行销毁.

pthread_cond_destroy(&cond)

 下面实现一个例子: 

#include<pthread.h>
#include<stdio.h>
#include<unistd.h>
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
int sum = 0;

void* func1(void* arg)
{
    pthread_mutex_lock(&mutex);
    if(sum <= 0)
    {
        printf("线程1进行等待\n");
        pthread_cond_wait(&cond, &mutex);
    }
    printf("线程1被唤醒\n");
    pthread_mutex_unlock(&mutex);
    return NULL;
}

void* func2(void* arg)
{
    sleep(5);
    pthread_mutex_lock(&mutex);
    ++sum;
    pthread_cond_signal(&cond);
    pthread_mutex_unlock(&mutex);
    return NULL;
}

int main()
{
    pthread_t t1;
    pthread_create(&t1, NULL, func1, NULL);
    pthread_t t2;
    pthread_create(&t2, NULL, func2, NULL);
    pthread_join(t1, NULL);
    pthread_join(t2, NULL);
    pthread_mutex_destroy(&mutex);
    pthread_cond_destroy(&cond);
    return 0;
}

运行这段代码, 能观察到屏幕上会先打印出 "线程1进行等待", 当过了5秒之后线程2唤醒线程1, 然后线程1开始执行打印 "线程1被唤醒".

死锁概念

死锁是指多个线程在执行过程中, 因为资源抢夺造成的一种僵局.

发生死锁的四个必要条件:

  1. 互斥请求: 资源在某段时间内只能有一个线程访问
  2. 请求与保持: 一个线程已经获得了最少一个资源, 并且还在请求其他资源, 但是其他资源已经被其他线程持有. 自己不会释放已经获得的资源, 而是一直请求.
  3. 不可抢占: 线程与线程之间不可强制抢夺资源. 一个线程已经获得了资源, 其他线程不能直接抢夺.
  4. 循环等待: 线程都分别获得一些资源, 但是还需要其他线程已获取的资源. 其他线程同理, 也需要其他线程已获取的资源. 线程之间形成循环等待资源的场景.

想要破坏死锁, 那么就只要破坏上面的任意一个条件即可.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值