1.线程安全

线程安全:多个线程并发同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作,并且没有锁保护的情况下,会出现该问题。

重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数。

常见的线程不安全的情况

1.不保护共享变量的函数

2.函数状态随着被调用,状态发生变化的函数

3.返回指向静态变量指针的函数

4.调用线程不安全函数的函数

常见的线程安全的情况

1.每个线程对全局变量或者静态变量只有读取的权限,没有写入的权限,一般来说这些线程是安全的

2.类或者接口对于线程来说都是原子操作

3.多个线程之间的切换不会导致该接口的执行结果存在二义性

常见不可重入的情况

1.调用了malloc/free函数,因为malloc函数是用全局链表来管理堆的

2.调用了标准I/O库函数,标准I/O库的很多实现都以不可重入的方式使用全局数据结构

3.可重入函数体内使用了静态的数据结构

常见可重入的情况

1.不使用全局变量或静态变量

2.不使用用malloc或者new开辟出的空间

3.不调用不可重入函数

4.不返回静态或全局数据,所有数据都有函数的调用者提供

5.使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据

可重入与线程安全联系

1.函数是可重入的,那就是线程安全的

2.函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题

3.如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的。

可重入与线程安全区别

1.可重入函数是线程安全函数的一种

2.线程安全不一定是可重入的,而可重入函数则一定是线程安全的。

3.如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生死锁,因此是不可重入的。

2.常见锁概念

死锁:死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所站用不会释放的资源而处于的一种永久等待状态。

死锁的四个必要条件

1.互斥条件:一个资源每次只能被一个执行流使用

2.请求与保持条件:一个执行流因为请求资源被阻塞时,对已有的资源保持不放

3.不可抢占:一个执行流已获得的资源,在未使用完之前,不可被强行抢占

4.循环等待:若干个执行流之间形成了一种头尾相接的循环等待资源的关系(A等待B的资源,B等待C的资源,C等待A的资源)。

解决死锁方法:

1.破环上面的4个条件的任意一个即可(对于第一个条件互斥来说,互斥本来就是为了线程安全才有的,所以对于互斥条件一般不选择破环)

2.加锁顺序一致(也就是申请锁的顺序一致,一个执行流拥有锁1之后,再去申请锁2,破环循环等待的条件)

3.避免锁未被释放的场景(例如,对线程函数进行加锁,但是在该函数中又出现了去申请其他锁的行为之前,因为将锁释放了再去申请)。

4.资源一次性分配,尽量资源被线程一次性拥有,不需要多次申请。

使用一些检测死锁的算法(其原理不是破坏造成死锁的四个条件,而是对资源分配进行预判,只允许不会导致死锁的资源分配请求)

(1)死锁检测算法

(2)银行家算法

3.线程同步

同步:在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,叫做同步

竞态条件:因为时序问题,而导致程序异常,我们称之为竞态条件。在线程场景下,这种问题也不难理解

3.1 条件变量

一般配合锁的使用,让线程“等通知”的工具,让线程申请锁具有时序性,用来帮助实现线程同步。

1.问题引入(如何理解条件变量呢?)

#include<iostream>
#include<cstdlib>
#include<cstring>
#include<vector>
#include<unistd.h>
#include<pthread.h>
using namespace std;
#define NUM 8
int ticket=10000;
 
pthread_mutex_t lock=PTHREAD_MUTEX_INITIALIZER; 
struct ThreadData
{
public:
    ThreadData(int num)
    {
        threadname="thread-"+to_string(num);
    }
public:
    string threadname;
};
void*GetTicket(void*args)
{
    ThreadData*td=static_cast<ThreadData*>(args);
    const char*name=td->threadname.c_str();
    while(true)
    {
        pthread_mutex_lock(&lock);
        if(ticket>0)
        {
            printf("who=%s,get a ticket%d\n",name,ticket);
            
            ticket--;
            pthread_mutex_unlock(&lock);
        }
        else
        {
            pthread_mutex_unlock(&lock);
            break;
        }
    }
    delete td;
    return nullptr;
}
int main()
{
    //创建多线程
    vector<pthread_t> tids;
    for(int i=0;i<NUM;i++)
    {
        pthread_t tid;
        ThreadData*td=new ThreadData(i);
        pthread_create(&tid,nullptr,GetTicket,td);
        tids.push_back(tid);
    }
    for(auto i:tids)
    {
        pthread_join(i,nullptr);
    }
    return 0;
}

运行结果:

初步理解:

上面的代码和运行结果是上一节课中,我们通过锁实现了线程互斥,但是我们发现了每次锁释放了之后还是被同一个线程再次给访问到,然后再次申请到了锁。

我们初步可以理解条件变量就是让我们的线程申请锁变得有序性,排队一个一个来,避免线程应一直申请不到锁,而发生的饥饿问题。

进一步理解(举例理解):

在之前的代码中,当一个线程释放锁了,之后这个锁被所有的线程开始竞争,但是由于刚刚释放锁的线程离锁比较近,也就更容易再次申请到锁,导致其他线程的饥饿问题。

现在有了条件变量的存在,其他没有申请到锁的就应该老实的按循序排好队,等待被唤醒。而不是一股脑的去竞争,当第一个线程释放锁了之后,就去按一下铃铛,可以通知一个线程或者通知所有的线程,去唤醒它们,让被唤醒的线程去申请锁,第一个线程就如果还需要申申请锁,就应该老老实实的去队尾排队,这样子就达到了申请临时资源的有序性,所以在上面的例子中,条件变量就至少需要拥有一个铃铛和一个让线程排队的队列。

同样的这个线程可以申请一个锁,也可以申请第二个锁,那么第一个锁对应一个条件变量,第二个锁对应第二个条件变量,那么由于存在多个线程,pthread线程库中势必会存在多个锁,存在多个条件变量,所以pthread线程库就要管理多个条件变量,如何管理?

先描述再组织,即先使用struct结构体描述条件变量的各个属性(其中一定包含等待队列),然后再采用一定的数据结构将条件变量组织起来,从此以后如果对特定条件变量的增删查改就变成了对组织条件变量的数据结构的增删查改。

3.1.1 条件变量接口认识

1.初始化和释放

初始化

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

pthread_cond_t cond = PTHREAD_COND_INITIALIZER;(定义为全局)

参数:
cond:要初始化的条件变量
attr:要设置的属性,默认设为NULL就可以

销毁
int pthread_cond_destroy(pthread_cond_t *cond)

2.等待条件满足

int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
参数:
cond:要在这个条件变量上等待
mutex:传入锁的指针

3.唤醒等待

int pthread_cond_broadcast(pthread_cond_t *cond);(唤醒全部线程)
int pthread_cond_signal(pthread_cond_t *cond);(唤醒单个线程)

只要掌握了条件变量的地址,任意一个线程都可以唤醒。

代码示例1:

创建5个线程,在加锁的情况下,由主线程来一个一个的唤醒阻塞在队列的线程,让它们有顺序的对临界资源n进行访问。

int n=0;
pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond=PTHREAD_COND_INITIALIZER;
void*count(void*arg)
{
    uint64_t number=(uint64_t)arg;
    cout<<"pthread-"<<number<<", create success"<<endl;
    while(true)
    {
        //线程分离
        pthread_detach(pthread_self());

        pthread_mutex_lock(&mutex);

        pthread_cond_wait(&cond,&mutex);

        cout<<"pthread-"<<number<<" n: "<<n++<<endl;

        pthread_mutex_unlock(&mutex);
    }
    return nullptr;
}                                                       
int main()
{
    vector<pthread_t> tids;
    for(int i=0;i<5;i++)
    {
        pthread_t tid;
        pthread_create(&tid,nullptr,count,(void*)i);
        tids.push_back(tid);
        usleep(1000);
    }
    while(1)
    {
        pthread_cond_signal(&cond);
        sleep(1);
    }
    return 0;
} 

运行结果:

可以明显的看到,确实线程是按照顺序的打印出来了n的值。但是这边有两个问题:

1.为什么要条件变量要放到加锁的后面呢(为什么pthread_cond_wait(&cond,&mutex);第二个参数是锁)?

其实第二个参数是锁,是为了去释放锁,没错!!!,当执行pthread_cond_wait(&cond,&mutex);代码时是会释放锁的。

2.里面具体的执行流程是怎么样的?

当第一个线程拥有锁了之后,到达了pthread_cond_wait(&cond,&mutex);代码时,会将锁释放掉,同时自己会阻塞在那里,等待被唤醒,其他阻塞的线程因为锁被释放了,就开始竞争锁,竞争到锁的线程同样释放了锁之后,会阻塞在条件变量,等待被唤醒,这样子,所有的线程也就排好队的阻塞在那里了,之后我们在后续的代码中一个一个去唤醒线程,被唤醒的线程会重新拥有锁,也就使线程有序的访问到临界资源了。

代码示例2:

创建5个线程,在加锁的情况下,由主线程一下子唤醒全部阻塞在队列的线程,让它们有顺序的对临界资源n进行访问。

int n=0;
pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond=PTHREAD_COND_INITIALIZER;
void*count(void*arg)
{
    uint64_t number=(uint64_t)arg;
    cout<<"pthread-"<<number<<", create success"<<endl;
    while(true)
    {
        //线程分离
        pthread_detach(pthread_self());

        pthread_mutex_lock(&mutex);

        pthread_cond_wait(&cond,&mutex);

        cout<<"pthread-"<<number<<" n: "<<n++<<endl;

        pthread_mutex_unlock(&mutex);
    }
    return nullptr;
}                                                       
int main()
{
    vector<pthread_t> tids;
    for(int i=0;i<5;i++)
    {
        pthread_t tid;
        pthread_create(&tid,nullptr,count,(void*)i);
        tids.push_back(tid);
        usleep(1000);
    }
    while(1)
    {
        // pthread_cond_signal(&cond);
        pthread_cond_broadcast(&cond);
        sleep(1);
    }
    return 0;
}      

运行结果:

可以看到在条件变量的等待队列中等待的线程一次性被唤醒了,但是唤醒并不是无序的,而是依次让条件变量下的等待队列的队头出队,直到队列为空。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐