【Linux】线程安全与线程同步

一、线程安全
1、概念
我们这里通过理解重入与线程安全的关系来理解线程安全
线程安全即多个线程并发同一段代码时,不会出现不同的结果
重入即同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,一个函数在重入的情况下运行结果不会出现任何问题,这样的函数称为可重入函数,否则,就是不可重入函数
2、常见线程情况
常见线程不安全情况
不保护共享变量的函数
函数状态随着被调用,状态发生变化的函数
返回指向静态变量指针的函数
调用线程不安全函数的函数
常见线程安全情况
每个线程对全局变量或静态变量只有读权限没有写权限
类或接口对于线程来说是原子操作
多个线程之间的切换不会导致该接口的执行结果存在二义性
3、常见重入情况
常见不可重入情况
调用了malloc或new函数,因为malloc函数是用全局链表来管理堆的
调用了标准IO库函数,标准IO库中很多实现都以不可重入的方式使用全局数据结构
可重入函数体内使用了静态的数据结构
常见可重入情况
不使用全局和静态变量
不使用malloc或new出来的空间
不调用不可重入函数
不返回静态或全局数据,所有数据都由函数的调用者提供
使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据
4、可重入与线程安全
联系
函数可重入就代表着线程安全
函数不可重入,那就不能由多个线程使用,有可能引发线程安全问题
如果一个函数中有全局变量,这么这个函数既是不可重入的又不是线程安全的
区别
可重入函数是线程安全函数的一种
线程安全不一定是可重入的,但可重入的一定是线程安全的
如果将对临界资源的访问加上锁,那么这个函数是线程安全的,但如果这个重入函数的锁还没释放则会产生死锁,因此是不可重入的
5、死锁
(一)概念
死锁是指在一组进程或线程中的各个进程或线程均占有不会释放的资源,但因互相申请被其他进程或线程所占用的不会释放的资源而处于的一种永久等待的状态
死锁都是人为产生的,我们可以规避掉的
(二)死锁的四个必要条件
互斥条件:一个资源只能被一个执行流使用
请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放
不剥夺条件:一个执行流已获得的资源在未使用完之前。不能强行剥夺
循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系
(三)避免死锁的方法
当有死锁的时候,必然是满足上面这四个条件的,但满足上面四个条件不一定形成死锁,我们只要破坏上面其中任何一条条件就可以避免死锁
加锁顺序一致
避免锁未释放的场景
资源一次性分配
二、线程同步
1、概念
在纯互斥的场景下,由于我们的锁只有少量个,多个线程同时竞争锁,但是得到锁的只有一小部分线程,剩下的线程就会因为等待,产生 “线程饥饿” 问题,线程饥饿本质上就是抢夺不到锁的线程,即抢夺不到资源的线程在等待锁的释放,为了避免这里的饥饿的问题,我们就通过线程同步来在保证数据安全的前提下,让线程按照顺序访问临界资源
2、条件变量
(一)概念
当一个线程互斥的访问某个变量时,它可能在其他线程改变状态之前什么也做不了,比如一个线程访问队列时,发现队列为空,那么它只能等待,直到其他进程将一个节点添加到队列当中,这个时候我们就可以利用条件变量来规避这种情况
(二)调用函数
(1)初始化条件变量
#include <pthread.h>
int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
返回值:成功返回0,失败返回非0错误码
cond:指向要初始化的条件变量的指针,pthread_cond_t 是一个表示条件变量的数据类型
attr:指向条件变量属性对象的指针,传入NULL表示使用默认属性
(2)销毁条件变量
#include <pthread.h>
int pthread_cond_destroy(pthread_cond_t *cond);
返回值:成功返回0,失败返回非0错误码
cond:指向要销毁的条件变量的指针
(3)等待条件被满足
#include <pthread.h>
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
返回值:成功返回0,失败返回非0错误码
cond:指向要操作的条件变量的指针,条件变量用于线程之间的等待和通知机制
mutex:指向互斥锁的指针,互斥锁用于保护共享资源,确保线程安全
调用该函数时,线程会自动释放互斥锁mutex,以便其他线程可以获取锁,当收到信号被唤醒后,线程会重新尝试获取互斥锁
(4)唤醒等待线程
#include <pthread.h>
//唤醒一个等待线程
int pthread_cond_signal(pthread_cond_t *cond);
//唤起所有等待线程
int pthread_cond_broadcast(pthread_cond_t *cond);
返回值:成功返回0,失败返回非0错误码
cond:指向要操作的条件变量的指针,条件变量是一种用于线程同步的机制,允许线程在某个条件不满足时阻塞,直到其他线程通知该条件已经满足
如果一个线程执行 pthread_cond_broadcast,它会将所有等待该条件变量的线程全部唤醒,若执行 pthread_cond_signal,则只会唤醒至少一个等待该条件变量的线程,而非只唤醒当前线程
(三)样例
#include <iostream>
#include <pthread.h>
#include <vector>
#include <unistd.h>
using namespace std;
#define NUM 4
int cnt = 0;
//条件变量函数的用法几乎与锁函数的用法完全等同
//定义全局锁和全局条件变量
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
void *Count(void *args)
{
    pthread_detach(pthread_self()); // 线程分离,跑完就不管了,不在乎它的返回值
// Linux是64位机,指针是8字节,uint是unsigned long long int
    uint64_t num = (uint64_t)args;
    cout << "Thread " << num << " is creat success" << endl;
    usleep(100000);
    while (true)
    {
        pthread_mutex_lock(&lock);
        //这里pthread_cond_wait要在临界区的原因是:
        //因为 pthread_cond_wait 是让线程去等待,等待的原因一定是临界资源不就绪
        //而临界资源是否就绪,是通过判断得来的,判断也是访问临界资源,所以判断必须在加锁之后
        pthread_cond_wait(&cond, &lock);
        //线程在此处进入等待状态,等待条件变量 cond 发出信号
        cout << "Thread " << num << " is running... cnt: " << cnt << endl;
        cnt++;
        usleep(10000);
        pthread_mutex_unlock(&lock);
    }
}
int main()
{
    for (uint64_t i = 1; i <= NUM; i++)
    {
        pthread_t tid;
        //这里的第四个参数,如果想要与新线程共享这个参数的话,可以设为(void*)&i,进行传址调用
        //我们这里要传值调用,不能让它用i
        pthread_create(&tid, nullptr, Count, (void *)i);
        usleep(1000);
    }
//指定唤醒线程来访问临界资源
    while (true)
    {
        sleep(1);
        pthread_cond_signal(&cond); // 唤醒一个线程
        cout << "signal one thread..." << endl;
    }
    return 0;
}
今日分享就到这了~
————————————————
                            版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/s_little_monster/article/details/145822440
阅读剩余
THE END