Linux——多线程

目录

线程概念

线程控制

线程创建

进程 vs 线程

线程异常

线程等待

线程终止

pthread_cancel

进程替换

线程分离

线程互斥

mutex

mutex接口

mutex的理解

互斥锁的实现

可重入和线程安全

死锁

什么是死锁

死锁产生的必要条件

避免死锁

线程同步

概念

条件变量

条件变量函数


线程概念

        在一个程序加载到内存中时,他就变成了一个进程,会有自己的代码和数据还有内核数据结构,也有虚拟地址到物理地址的映射关系。

        如果我想再创建一个进程,这个进程不想给他创建内核数据结构只有PCB,让这个PCB指向同样的地址空间。就是通过某些手段将当前进程的“资源”通过某些方式划分给不同的PCB。

        我们把上图中的每一个task_struct都可以叫做一个线程(thread)。线程是在进程内部执行的,或者说线程在进程的地址空间内运行,是操作系统调度的基本单位

        上面的这每一个线程都是一个执行流,线程会更轻量化,执行的粒度更细调度轻量化资源占用更少调度成本较低,只要能满足这些它都是线程,不管是在哪个操作系统下。

        进程和线程都要被调度、被创建、维护各种关系等,这两种在概念上都高度重合,那么操作系统也要维护和管理这些线程,就这些操作下来,只是多了一份和进程高度重合的代码来维护线程,所以Linux中没有在内核上去区分进程和线程都用task_struct表示,只不过进程有独立地址空间,线程和进程共享地址空间就行了,用最小的代码实现线程的效果。

        在用户视角看来,进程就是内核数据结构加上进程对应的代码和数据;在内核的视角来看,进程是承担分配系统资源的基本实体

        申请task_struct,申请地址空间,创建页表,开辟内存保存代码和数据,这些都是进程向操作系统申请的,之后的线程就不会再向操作系统申请,而是向进程申请。

        原来我们说的进程是只有一个执行流的进程,后面就可能会遇到内部有多个执行流的进程,原来的task_struct就是进程内部的一个执行流

        在CPU的视角,它不关心是进程还是线程,它只知道task_struct。在Linux下的PCB的量级会小于等于其他操作系统的PCB,如果这个PCB是多线程的一个执行流,那就要比其他OC的PCB量级小,如果只有一个线程,那就是等于。

        所以Linux下的进程统称为轻量级进程

        Linux下没有真正意义上的线程结构,因为没有对应的数据结构,它的进程是用PCB模拟实现的。所以Linux不能直接给我们提供线程的相关接口,只能提供轻量级进程的接口。但是用户有不知道什么是轻量级进程,我只想用进程的时候调用进程的接口,用线程的时候调用线程的接口,所以Linux在用户层实现了一套用户多线程方案,以库的方式提供给用户进行使用,这就是pthread——线程库,也叫他原生线程库


线程控制

线程创建

作用:创建一个新的线程

参数:

  • thread:线程id,是一个输出型参数
  • attr:设置创建线程的属性,默认为nullptr就可以了
  • start_routine:函数指针,线程执行进程代码一部分的入口函数
  • arg:传入入口函数的参数

返回值:创建成功返回0,失败错误码被设置

还有要注意的是,在使用gcc或g++编译的时候要加 -lpthread

#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include <cstdio>
#include <string>

using namespace std;

void* threadRun(void* args)
{
    const string name = (char*)args;
    while (true)
    {
        cout << "新线程: "<< name << ", pid: " << getpid() << endl;
        sleep(1);
    }
}

int main()
{
    pthread_t tid[5]; // 直接创建5个线程
    char name[64];
    for (int i = 0; i < 5; i++)
    {
        snprintf(name, sizeof(name), "thread-%d", i);
        pthread_create(tid + i, nullptr, threadRun, (void*)name); // 创建线程,把name传入函数
        sleep(1);
    }

    while (true) 
    {
        cout << "主线程, pid: " << getpid() << endl;
        sleep(3);
    }

    return 0;
}

        使用ps -aL指令就可以查看系统中的轻量级进程,他们都属于同一个进程,LWP(Light Weight Process)就是轻量级进程,这些编号都是不一样的,有一个PID和LWP是一样的,那就是主线程,所以操作系统识别是LWP,PID不能说明唯一性,原来只有一个轻量级进程的时候LWP和PID是一样的,也没有问题。

进程 vs 线程

        进程中的多个线程是共享同一地址空间的,比如正文代码区、全局数据区和堆区都是共享的,文件描述符表、信号的处理方式、当前工作目录、用户id和所属组id也是共享的。

int g_val = 0;

void* threadRoutine(void* args)
{
    while (true)
    {
        cout << (char*)args << " : " << g_val << " &: " << &g_val << endl;
        sleep(1);
        g_val++;
    }
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRoutine, (void*)"new thread");

    while (true)
    {
        cout << "main thread: " << g_val << " &: " << &g_val << endl;
        sleep(1);
    }

    return 0;
}

        如果使用__thread来修饰全局变量,可以让每一个线程各自拥有一个全局变量 -- 线程的局部存储。

__thread int g_val = 0;

        进程是资源分配的基本单位,而线程是调度的基本单位,也有一部分数据是每一个线程独有的,比如线程id;线程也是要被调度的,寄存器中要存有这个线程的上下文,所以寄存器也是独有的,要调度肯定还有优先级也是独有的;每个线程要调用不同的函数完成各种功能,也定要入栈和出栈,临时变量要保存在栈中,如果栈也共享的,所有的执行流都要访问,另一个执行流可能就会覆盖你的数据,所以栈也是独有的寄存器和栈可以体现出线程的动态属性

线程的优点:

  • 线程之间切换需要操作系统做的工作更少,成本比较低。
  • 线程占用的资源要比进程少,它的资源也是从进程来的。

线程的缺点(对标多进程):

  • 线程间切换并不是没有成本,如果是单核单CPU的情况下创建一个线程是最好的,不会有切换的成本,所以进程不是创建的越多越好。
  • 健壮性会降低,多个线程都是用了全局变量,一个线程修改了就会影响别人。
  • 缺乏访问控制,后续也要有访问控制的方案。
  • 编程难度变高。

当tast_struct要切换的时候,为什么线程要比进程切换的成本低呢?

  1. 如果是同一个进程地址空间和页表不需要切换。
  2. 如果要调度的是另一个进程,就要把上下文、临时数据、页表、地址空间全都要切换
  3. CPU内部是有硬件级别的缓存的(cache),如果一条一条的从内存中读指令,那就会拉低效率,CPU会根据局部性原理预读一些指令。如果进程切换了cache就失效了,新进程来了,只能重新缓存。

线程异常

 还是演示除0错误。

#include <iostream>
#include <unistd.h>
#include <pthread.h>

using namespace std;

void* threadRoutine(void* args)
{
    while (true)
    {
        cout << (char*)args << " running ..." << endl;
        sleep(1);
        int a = 10;
        a /= 0;
    }
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRoutine, (void*)"new thread");

    while (true)
    {
        cout << "main thread running ..." << endl;
        sleep(1);
    }

    return 0;
}

        虽然这里看到主线程先运行,新线程后运行,其实线程的运行顺序是由调度器决定的。一个线程异常了就可能导致整个进程终止。

线程等待

        通过前几章说过的进程等待,线程也是需要等待的,如果主线程不等待,也会引发类似的僵尸问题,导致内存泄漏。

        线程等待用的就是这个接口。

参数:

  • thread:线程id,与创建不同的是不需要取地址
  • retval:线程退出的退出码。

返回值:成功返回0,失败返回错误码

 

void* threadRoutine(void* args)
{
    int i = 5;
    while (true)
    {
        cout << (char*)args << " running ..., exit after " << i << " seconds" << endl;
        sleep(1);
        if (--i == 0) break;
    }
    cout << "new thread quit" << endl;
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRoutine, (void*)"new thread");

    pthread_join(tid, nullptr); // 默认会阻塞等待

    cout << "main thread wait success ... main quit" << endl;

    return 0;
}

        我们已经看到了主线程在等新线程退出,pthead_join还有一个参数是一个void**,新线程执行的函数的返回值是一个void*,所以这第二个参数就是接受函数返回值的。

void* threadRoutine(void* args)
{
    int i = 5;
    while (true)
    {
        cout << (char*)args << " running ..., exit after " << i << " seconds" << endl;
        sleep(1);
        if (--i == 0) break;
    }
    cout << "new thread quit" << endl;

    return (void*)1;
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRoutine, (void*)"new thread");

    void* ret = nullptr;

    pthread_join(tid, &ret); // 默认会阻塞等待

    // 因为这个机器下默认就是64位的,所以不能强转成int
    cout << "main thread wait success ... main quit, new thread quit code: " << (long long)ret << endl;

    return 0;
}

        所以通过这个参数,我们不止可以返回一个数,也可以返回从堆上申请的空间,因为堆是共享的。

void* threadRoutine(void* args)
{
    int i = 5;
    int* data = new int[5];
    while (true)
    {
        cout << (char*)args << " running ..., exit after " << i << " seconds" << endl;
        sleep(1);
        data[i-1] = i;
        if (--i == 0) break;
    }
    cout << "new thread quit" << endl;

    return (void*)data;
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRoutine, (void*)"new thread");

    int* ret = nullptr;

    pthread_join(tid, (void**)&ret); // 默认会阻塞等待

    cout << "main thread wait success ... main quit" << endl;
    for (int i = 0; i < 5; i++)
    {
        cout << ret[i] << endl;
    }

    return 0;
}

线程终止

原来我们使用的exit是终止进程的,那我们现在想要一个线程终止该怎么做呢?

作用:终止线程

参数:retval返回值

void* threadRoutine(void* args)
{
    int i = 5;
    while (true)
    {
        cout << (char*)args << " running ..., exit after " << i << " seconds" << endl;
        sleep(1);
        if (--i == 0) break;
    }
    cout << "new thread quit" << endl;

    pthread_exit((void*)1);
    
}

pthread_cancel

还有一种终止进程的方法就是取消进程。 

参数:要取消的线程的id。

 返回值:成功返回0,失败返回错误码。

void* threadRoutine(void* args)
{
    while (true)
    {
        cout << (char*)args << " running ..." << endl;
        sleep(1);
    }
    cout << "new thread quit" << endl;

    pthread_exit((void*)1);
    
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRoutine, (void*)"new thread");

    int i = 0;
    while (true)
    {
        cout << "main thread running ..." << endl;
        sleep(1);
        i++;
        if (i == 5) break;
    }

    pthread_cancel(tid);
    cout << "pthread_cancel: " << tid << endl;

    void* ret = nullptr;
    pthread_join(tid, (void**)&ret); // 默认会阻塞等待

    cout << "main thread wait success ... main quit, new thread quit code: " << (long long)ret << endl;

    return 0;
}

        线程被取消,join的退出码会被设置为-1。

        使用pthread_cancel时,取消的一定是一个已经跑起来的线程,我觉得不需要了,才取消的。

        一般都是使用主线程去取消新线程,如果主线程被新线程取消了,那么谁来等待这个新线程退出呢,所以一般不这样做。

        我们也看到了pthread_cancel取消的线程id是很长的一串,并不是我们使用ps aL看到的LWP,它本质上是一个地址,因为我们用的不是Linux自带的接口,而是pthread库提供的接口。        

        一个线程也要有自己的属性,它也要被管理起来,操作系统是对轻量级进程的调度,还有内核数据结构的管理,库也要给用户提供线程相应的属性,Linux下使用进程模拟的线程,用户想要知道一个线程的退出结果,线程的参数、栈结构等,这些内核是不管的,所以就要库在用户层来管理这个线程,就是在库中管理相应的结构。

为了更好的让线程找到自己的用户层属性,就把每个线程结构的起始地址设置为tid

        那么把每个线程的栈结构都放在了共享区,那么地址空间中的栈结构还用不用呢?那肯定是用的,主线程用的就是内核级的栈结构新线程用的就是共享区提供的栈结构,这也不会和单进程一个执行流冲突,一个线程那用的就是内核级的栈区。

 

作用:获取线程的tid

// ...
    cout << (char*)args << " running ..., new tid: " << pthread_self() << endl;
// ...
    cout << "main thread running ..., main tid: " << pthread_self() << endl;
// ...

进程替换

        如果我们使用execl进程替换函数,那么整个进程都要被替换,包括每一个线程,所以不管在哪一个线程下调用execl系列的进程进程替换函数,整个进程都会被替换,所以它才被叫做进程替换。

线程分离

        默认情况下,每一个新建的线程都是要被等待的,如果线程退出不使用pthread_join就无法释放资源,造成内存泄漏。但是pthread_join默认是阻塞等待的,没有非阻塞等待的设置,如果我不关心线程返回的是什么,就可以使用线程分离

参数:线程的tid

返回值:成功返回0, 失败返回错误码。

pthread_detach和pthead_join是不能一起使用的,你都已经分离了,我就不能再等了。

void* threadRoutine(void* args)
{
    pthread_detach(pthread_self());

    while (true)
    {
        cout << (char*)args << " running ..., new tid: " << pthread_self() << endl;
        sleep(1);
        break;
    }

    pthread_exit((void*)1);
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRoutine, (void*)"new thread");

    while (true)
    {
        cout << "main thread running ..., main tid: " << pthread_self() << endl;
        sleep(1);
        break;
    }

    int n = pthread_join(tid, nullptr);
    cout << "n: " << n << ", " << strerror(n) << endl; 

    return 0;
}

没有线程分离就正常等待。


线程互斥

下面的这些概念在原来的时候也说过,我们现在再来完善一下:

  • 临界资源:多个线程执行流看到的同一份资源叫做临界资源
  • 临界区:每个线程内部访问临界资源的代码就叫做临界区
  • 互斥:为了保护临界区,多执行流任何时刻只能有一个进程进入临界区,这就叫做互斥
  • 原子性:不会被任何调度机制打断,对于一件事要么做要么不做,没有中间状态就成为原子性

下面就来看一些实例,比如我们模拟一个卖票的程序。

int tickets = 1000; // 定义了1000张票,这个数字不重要

void* getTickets(void* args) // 多个执行流可能同时进入这个函数,这个函数就被重入了
{
    (void)args;
    while (true)
    {
        if (tickets > 0)
        {
            usleep(1000);
            cout << pthread_self() << " : " << tickets << endl;
            tickets--;
        }
        else
        {
            break;
        }
    }
}

int main()
{
    pthread_t t1, t2, t3;
    pthread_create(&t1, nullptr, getTickets, nullptr);
    pthread_create(&t2, nullptr, getTickets, nullptr);
    pthread_create(&t3, nullptr, getTickets, nullptr);

    pthread_join(t1, nullptr);
    pthread_join(t2, nullptr);
    pthread_join(t3, nullptr);

    return 0;
}

        我们看到的是票数出现了-1,这就代表着多卖了一张,这可是一个很严重的错误。我们执行的操作tickets--其实做了三步。

        第一步把内存中的数据读到CPU中,第二步tickets--,第三步放回到内存中。线程1在这个操作执行的过程中因为某些原因发生了线程切换,可能线程2已经把tickets减到0了,这时候你这个线程1被换回来,你才减了一次,又把这个数放到了内存中。

        又或者此时的tickets已经到1了,这时候你这个线程1又被切换了,线程2已经把tickets减到0了,这时候线程1回来了,因为刚才判断时tickets大于0,现在又把tickets--,这就变成了-1。

        所以tickets这个全局变量在并发访问的时候导致了数据不一致的问题。

mutex

        为了解决上面的这种问题就要有一个新的概念就是互斥锁。这是原生线程库提供的一个数据类型。

int tickets = 1000;

pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER; // 这是一个全局的锁

void* getTickets(void* args) // 多个执行流可能同时进入这个函数,这个函数就被重入了
{
    (void)args;
    while (true)
    {
        pthread_mutex_lock(&mtx); // 加锁的同时也要注意解锁,这是一个全局变量
        // 临界区
        if (tickets > 0)
        {
            cout << pthread_self() << " : " << tickets << endl;
            tickets--;
            pthread_mutex_unlock(&mtx);
        }
        else
        {
            pthread_mutex_unlock(&mtx);
            break;
        }
    }
}

int main()
{
    pthread_t t1, t2, t3;
    pthread_create(&t1, nullptr, getTickets, nullptr);
    pthread_create(&t2, nullptr, getTickets, nullptr);
    pthread_create(&t3, nullptr, getTickets, nullptr);

    pthread_join(t1, nullptr);
    pthread_join(t2, nullptr);
    pthread_join(t3, nullptr);

    return 0;
}

        这样我们就看到了tickets变为了1,还可以看到一个现象就是所有的pthread_self打印的tid都是一样的,这是因为选择哪个线程完全是操作系统说了算的,但是我们可以模拟一下买完票的后续操作。

void* getTickets(void* args) // 多个执行流可能同时进入这个函数,这个函数就被重入了
{
    (void)args;
    while (true)
    {
        pthread_mutex_lock(&mtx); // 加锁的同时也要注意解锁,这是一个全局变量
        // 临界区
        if (tickets > 0)
        {
            cout << pthread_self() << " : " << tickets << endl;
            tickets--;
            pthread_mutex_unlock(&mtx);
        }
        else
        {
            pthread_mutex_unlock(&mtx);
            break;
        }
        // 模拟后续动作
        usleep(1000);
    }
}

【注意】:加锁的粒度越小越好,尽量不在加锁和解锁中间放一些无关的代码。

mutex接口

我们再来看一下这些函数。

作用:初始化互斥锁

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

参数:

  • mutex:需要初始化的互斥锁。
  • attr:初始化互斥锁的属性,一般设置为nullptr

返回值:成功返回0,失败返回错误码。

上面的代码我们使用的:

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

调用pthread_mutex_init函数初始化互斥锁叫做动态分配,除此之外,我们还可以用下面这种方式初始化互斥锁,该方式叫做静态分配,这个互斥锁不需要销毁。

 

作用:销毁互斥锁

int pthread_mutex_destroy(pthread_mutex_t *mutex);

参数:要销毁的互斥锁

返回值:成功返回0,失败返回错误码。

 

作用:给临界资源加锁

int pthread_mutex_lock(pthread_mutex_t *mutex);

参数:要使用的互斥锁

返回值:成功返回0,失败返回错误码。

 

作用:给临界资源解锁

int pthread_mutex_unlock(pthread_mutex_t *mutex);

参数:要使用的互斥锁

返回值:成功返回0,失败返回错误码。

下面我们就写一个完整的代码:

int tickets = 1000;

#define THREAD_NUM 3

class ThreadData
{
public:
    ThreadData(const string& s, pthread_mutex_t* pm)
        :tname(s)
        ,pmtx(pm)
    {}
    string tname;
    pthread_mutex_t* pmtx;
};

void* getTickets(void* args) // 多个执行流可能同时进入这个函数,这个函数就被重入了
{
    ThreadData* td = (ThreadData*)args;
    (void)args;
    while (true)
    {
        pthread_mutex_lock(td->pmtx);
        // 临界区
        if (tickets > 0)
        {
            cout << td->tname << " : " << tickets << endl;
            tickets--;
            pthread_mutex_unlock(td->pmtx);
        }
        else
        {
            pthread_mutex_unlock(td->pmtx);
            break;
        }
        // 模拟后续动作
        usleep(1000);
        delete td;
    }
}

int main()
{
    pthread_mutex_t mtx;
    pthread_mutex_init(&mtx, nullptr);

    pthread_t t[THREAD_NUM];
    for (int i = 0; i < THREAD_NUM; i++)
    {
        string name = "thread ";
        name += to_string(i + 1);

        ThreadData* td = new ThreadData(name, &mtx);
        pthread_create(t + i, nullptr, getTickets, (void*)td);
    }

    for (int i = 0; i < THREAD_NUM; i++)
    {
        pthread_join(t[i], nullptr);
    }

    pthread_mutex_destroy(&mtx);

    return 0;
}

mutex的理解

        如果给临界区加锁后,那么线程在临街中是否会切换。那是一定会切换的,因为切不切换是操作系统说了算的。既然它还要切换,会不会出现上述的问题呢?

        虽然被切换了,在访问tickets的时候,这个线程是持有锁的。其他线程想要访问,那就得先申请锁申请锁也不会成功,所以就被阻塞了,这就保证了临界区数据一致性,所以访问临界资源先申请锁,用完后释放锁,这才是正确的编码方式。

        所以对这个持有锁的线程,其他线程就认为这个线程的操作是原子的

        加锁之后就可以保证临界区的代码一定是串行的

        换言之,每个进程都要申请同一个锁,这个锁不也就是共享资源吗,所以也要保证锁的安全,那申请锁和释放锁也必须是原子的。

互斥锁的实现

        从汇编的角度来说,要是只有一条汇编语句,我们就认为该汇编语句执行时是原子的。在汇编语言中有一条swap或exchange指令,用这一条语句将CPU内的寄存器和内存数据进行交换。

        多执行流中,在CPU中所有的寄存器存放的是当前执行流的上下文,这些数据是该执行流私有的,但寄存器的空间是被所有执行流共享的。


可重入和线程安全

  • 现在就可以理解什么是重入了,抢票的函数就是一个可重入的函数
  • 线程安全:多个线程并发访问同一段代码时不会出现不同的结果。

常见的线程不安全情况:

  • 不保护临界资源的函数。

  • 函数状态随着调用发生了变化的函数。

  • 返回指向静态变量指针的函数。

  • 调用线程不安全的函数。

常见的线程安全的情况:

  • 每个线程对全局变量或者静态变量只有读取的权限。
  • 类或者接口对于线程来说都是原子操作,就像只用一条语句完成交换。

常见不可重入的情况:

  • 调用了malloc/free,因为malloc函数是用全局链表来管理堆的。
  • 调用标准IO库函数,函数大多都是不可重入的方式使用全局数据结构。
  • 可重入函数内使用了静态的数据结构。

常见可重入的情况:

  • 不使用全局变量或静态变量。
  • 不使用malloc或者new开辟出的空间。
  • 不调用不可重入函数。
  • 不返回静态或全局数据,所有数据都由函数的调用者提供。
  • 使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据,一开始定义一个临时变量保存数据,函数执行完后再用临时数据恢复。

可重入和线程安全的联系:

  • 函数可重入,那么线程一定是安全的。
  • 函数不可重入,那么就有可能引发线程安全问题。

可重入函数和线程安全的区别:

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

死锁

什么是死锁

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

        别说多个了,一个执行流如果重复申请锁,也会产生死锁,你已经申请了一个锁了,在申请一看al是0,那直接就被阻塞了,但是你还拿着锁呢,所以写代码的时候也要注意。

        多执行流就像下图,锁执行线程表示分配给了他,线程指向锁代表要申请锁,此时线程a持有锁1,线程b持有锁2,线程a还想要锁2,线程b还想要锁1,那这两个线程就都阻塞了,这就叫做死锁。

死锁产生的必要条件

  • 互斥条件:一个资源每次只能被一个执行流使用,使用了互斥锁
  • 请求与保持条件: 一个执行流因请求资源而阻塞时,对已获得的资源保持不放,我已经有了一个锁了,还想再申请另一个锁。
  • 不可抢占条件: 一个执行流已获得的资源,在未使用完之前,不能强行夺取。
  • 循环等待条件: 若干执行流之间形成一种头尾相接的循环等待资源的关系,最后形成一个环。

避免死锁

  • 破坏死锁的四个必要条件
  • 加锁顺序一致
  • 避免锁未释放的场景
  • 资源一次性分配

线程同步

概念

  • 同步: 在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,这就叫做同步。
  • 竞态条件: 因为时序问题,而导致程序异常,称之为竞态条件。

        加锁也是会存在某些问题的,如果个别线程的竞争力特别强,每次都能够申请到锁,但申请到锁之后啥也不做,所以这个线程就一直在申请锁和释放锁,这就可能导致其他线程长时间竞争不到锁,引起饥饿问题,他可以这样做,就是不合理。
        加锁没有错,它能够保证在同一时间段只有一个线程进入临界区,但没有高效的使用这份临界资源。
        现在就规定,当一个线程释放锁后,这个线程不能立马再次申请锁,该线程必须排到这个锁的资源等待队列的最后。

        所以多执行流按照一定的顺序进行对临街资源的访问就叫做线程同步,引入线程同步就是为了解决访问临界资源不合理的问题。

条件变量

        当我们申请临界资源的时候,先要对临界资源是否存在做检测,要检测也是要访问临界资源。所以对临街资源的检测也要在加锁和解锁之间,这就导致了频繁的检测就绪条件,也就要频繁的申请释放锁,为了解决这个问题提出来一下建议:

  1. 不要让线程频繁的检测,要让他等待
  2. 当条件就绪的时候,通知对应的线程,让他进行资源申请和访问。

这就引出了一个概念:条件变量

条件变量函数

这些函数返回值类型都是int,成功返回0,失败返回错误码。

作用:动态分配初始化条件变量。

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

参数:

  • cond:需要初始化的条件变量。
  • attr:初始化条件变量的属性,一般设置为nullptr。

作用:静态分配初始化条件变量,不需要销毁。

pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

作用:销毁条件变量。

int pthread_cond_destroy(pthread_cond_t *cond);

参数:cond:需要销毁的条件变量。

作用:等待条件变量满足

int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);

参数:

  • cond:需要等待的条件变量。
  • mutex:当前临界区对应的互斥锁

        再来说一下这个第二个参数为什么要传入锁,我们在访问临界资源的时候一定是要先检测,如果条件不满足再使用pthread_cond_wait,检测本身也是在访问临界资源,所以都是先加锁,再检测,如果要是检测出条件不满足就要阻塞这个线程,这时是带着锁阻塞的,那么其他线程想要申请锁就不会申请成功,所以这个函数的第二个参数要传入锁,调用的时候就会释放这个锁,别的线程就可以申请锁了。

        之后当线程被唤醒的时候也会自动帮我们获取锁

作用:唤醒等待

int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);
  • pthread_cond_signal函数用于唤醒等待队列中首个线程。
  • pthread_cond_broadcast函数用于唤醒等待队列中的全部线程。

参数:唤醒在cond条件变量下等待的线程。

        下面就简单的演示一下条件变量是怎么用的,但是没有对应的场景还是无法更好的理解,而且使用条件变量的时候是有问题的,后面会有场景的。

#include <iostream>
#include <unistd.h>
#include <pthread.h>

using namespace std;

#define TNUM 4

typedef void(*func_t)(const string& name, pthread_mutex_t* pmtx, pthread_cond_t* pcond);

volatile bool quit = false; // 退出条件

class ThreadData
{
public: 
    ThreadData(const string& name, func_t func, pthread_mutex_t* pmtx, pthread_cond_t* pcond)
        :_name(name)
        ,_func(func)
        ,_pmtx(pmtx)
        ,_pcond(pcond)
    {}
public:
    string _name;           // 线程名
    func_t _func;           // 线程要执行的函数
    pthread_mutex_t* _pmtx; // 互斥锁
    pthread_cond_t* _pcond; // 条件变量
};

void func1(const string& name, pthread_mutex_t* pmtx, pthread_cond_t* pcond)
{
    while (!quit)
    {   
        // 运行到wait的时候,当前线程会立即被阻塞,每个线程都要这样做
        pthread_mutex_lock(pmtx);
        // 这里的等待就是在检测资源是否就绪,它应该在加锁与解锁之间的
        pthread_cond_wait(pcond, pmtx);
        cout << name << " running..." << endl;
        pthread_mutex_unlock(pmtx);
    }
}

void func2(const string& name, pthread_mutex_t* pmtx, pthread_cond_t* pcond)
{
    while (!quit)
    {
        pthread_mutex_lock(pmtx);
        pthread_cond_wait(pcond, pmtx);
        cout << name << " running..." << endl;
        pthread_mutex_unlock(pmtx);
    }
}

void func3(const string& name, pthread_mutex_t* pmtx, pthread_cond_t* pcond)
{
    while (!quit)
    {
        pthread_mutex_lock(pmtx);
        pthread_cond_wait(pcond, pmtx);
        cout << name << " running..." << endl;
        pthread_mutex_unlock(pmtx);
    }
}

void func4(const string& name, pthread_mutex_t* pmtx, pthread_cond_t* pcond)
{
    while (!quit)
    {
        pthread_mutex_lock(pmtx);
        pthread_cond_wait(pcond, pmtx);
        cout << name << " running..." << endl;
        pthread_mutex_unlock(pmtx);
    }
}

void* Entry(void* args)
{
    ThreadData* td = (ThreadData*)args; // td在每一个线程私有的栈结构中保存
    td->_func(td->_name, td->_pmtx, td->_pcond);
    delete td; // 当函数执行完后也要释放td,因为它也是new出来的
}

int main()
{
    pthread_mutex_t mtx; // 互斥锁
    pthread_cond_t cond; // 条件变量

    // 初始化
    pthread_mutex_init(&mtx, nullptr);
    pthread_cond_init(&cond, nullptr);

    pthread_t tids[TNUM]; // 创建多线程
    func_t funcs[TNUM] = {func1, func2, func3, func4}; // 每个线程独有的方法
    for (int i = 0; i < TNUM; i++)
    {
        string name = "Thread ";
        name += to_string(i + 1);
        ThreadData* td = new ThreadData(name, funcs[i], &mtx, &cond);
        pthread_create(tids + i, nullptr, Entry, (void*)td);
    }

    // 让主线程区一个一个唤醒
    int cnt = 10;
    while (cnt--)
    {
        sleep(1);
        cout << "resume thread run code: " << endl;
        pthread_cond_signal(&cond);
        // pthread_cond_broadcast(&cond); // 唤醒所有等待的线程
    }

    cout << "ctrl done" << endl;
    quit = true;
    pthread_cond_broadcast(&cond); // 再唤醒所有线程,检测退出条件

    // 等待线程退出
    for (int i = 0; i < TNUM; i++)
    {
        pthread_join(tids[i], nullptr);
        cout << "thread: " << tids[i] << " quit" << endl;
    }

    // 销毁
    pthread_mutex_destroy(&mtx);
    pthread_cond_destroy(&cond);

    return 0;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/455058.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

论坛管理系统|基于Spring Boot+ Mysql+Java+B/S架构的论坛管理系统设计与实现(可运行源码+数据库+设计文档+部署说明+视频演示)

推荐阅读100套最新项目 最新ssmjava项目文档视频演示可运行源码分享 最新jspjava项目文档视频演示可运行源码分享 最新Spring Boot项目文档视频演示可运行源码分享 目录 目录 前台功能效果图 管理员功能登录前台功能效果图 用户功能模块 系统功能设计 数据库E-R图设计 l…

webpack面试题

1、webpack是干什么的 Webpack是一个现代的JavaScript应用程序的静态模块打包工具。当webpack处理应用程序时&#xff0c;它会在内部构建一个依赖图&#xff0c;此依赖图对应映射到项目所需的每个模块&#xff0c;然后将所有这些模块打包成一个或多个bundle。Webpack的主要功能…

Java初阶数据结构队列的实现

1.队列的概念 1.队列就是相当于排队打饭 2.在排队的时候就有一个队头一个队尾。 3.从队尾进对头出 4.所以他的特点就是先进先出 所以我们可以用链表来实现 单链表实现要队尾进队头出{要有last 尾插头删} 双向链表实现效率高&#xff1a;不管从哪个地方当作队列都是可以的&…

HttpContext请求接收上下文模块设计与实现(http模块四)

目录 类功能 类定义 类实现 编译测试 类功能 类定义 // HttpContext接收请求上下文模块功能设计 typedef enum {RECV_HTTP_ERROR,RECV_HTTP_LINE,RECV_HTTP_HEAD,RECV_HTTP_BODY,RECV_HTTP_OVER } HttpRecvStatu;class HttpContext { private:int _resp_statu; …

Games101笔记-变换

Scale Reflection Shear Rotate 没有额外提示默认绕原点旋转 线性变换 Transiation 不属于线性变换&#xff0c;仿射变换 齐次坐标 二维的点和向量增加一个维度 点加点等于两个点的中点 所有的仿射变换都可以写成齐次坐标的形式 在表示二维情况下的仿射变换时&#…

Linux驱动分离与分层的简介

一. 简介 我们在前面几章编写的设备驱动都非常的简单&#xff0c;都是对 IO 进行最简单的读写操作。 像 I2C 、SPI 、 LCD 等这些复杂外设的驱动就不能这么去写了&#xff0c; Linux 系统要考虑到驱动的可重用性&#xff0c;因 此&#xff0c;提出了驱动的分离与分层这样的软…

Maven: There are test failures.(已解决)

问题解决办法 进行package打包时报错如下&#xff1a; 然后这些并不能看出是测试的哪里的问题&#xff0c;可以点击上一级进行查看更详细的错误&#xff0c;越向上日志越详细&#xff0c;可以看到是52行出了错误&#xff0c; 52对应代码如下&#xff1a; 原因是存在注册的测…

基于FPGA的图像锐化算法(USM)设计

免费获取源码请关注微信号《FPGA学习笔记册》&#xff01; 1.图像锐化算法说明 图像锐化算法在实际的图像处理应用很广泛&#xff0c;例如&#xff1a;医学成像、工业检测和军事领域等&#xff1b;它的作用就是将模糊的图像变的更加清晰。常用的图像锐化算法有拉普拉斯算子、s…

记录一下在Pycharm中虚拟环境的创建

如果在Pycharm中要新建一个虚拟环境&#xff0c;那你可以在Terminal中选择Command Prompt&#xff0c;在这里面执行相关命令 一、安装了Anaconda&#xff0c;创建虚拟环境 当你使用解释器是Anaconda提供的时&#xff0c;你可以使用conda命令执行&#xff0c;见以下操作&#x…

Acwing.4261 孤独的照片(贡献法)

题目 Farmer John 最近购入了 N 头新的奶牛&#xff0c;每头奶牛的品种是更赛牛&#xff08;Guernsey&#xff09;或荷斯坦牛&#xff08;Holstein&#xff09;之一。 奶牛目前排成一排&#xff0c;Farmer John 想要为每个连续不少于三头奶牛的序列拍摄一张照片。 然而&…

华为组网:核心交换机旁挂防火墙,基于ACL重定向配置实验

如图所示&#xff0c;由于业务需要&#xff0c;用户有访问Internet的需求。 用户通过接入层交换机SwitchB和核心层交换机SwitchA以及接入网关Router与Internet进行通信。为了保证数据和网络的安全性&#xff0c;用户希望保证Internet到服务器全部流量的安全性&#xff0c;配置重…

Flask开发类似jenkins构建自动化测试任务工具

1、自动化 某一天你入职了一家高大上的科技公司&#xff0c;开心的做着软件测试的工作&#xff0c;每天点点点&#xff0c;下班就走&#xff0c;晚上陪女朋友玩王者&#xff0c;生活很惬意。 但是美好时光一般不长&#xff0c;这种生活很快被女主管打破。为了提升公司测试效率…

如何“使用Docker快速安装Jenkins,在CentOS7”?

1、运行 docker run -d --namejenkins -p 8080:8080 jenkins/jenkins 2、查看日志 &#xff0c;使用 "docker logs -f jenkins",可以持续刷新日志 docker logs jenkins 3、通过命令查看密码 docker exec -it jenkins cat /var/jenkins_home/secrets/initialAdminP…

用云服务器构建gpt和stable-diffusion大模型

用云服务器构建gpt和stable-diffusion大模型 一、前置知识二、用云端属于自己的聊天chatGLM3step1、项目配置step2、环境配置1、前置知识2、环境配置流程 step3、创建镜像1、前置知识2、创建镜像流程 step4、通过 Gradio 创建ChatGLM交互界面1、前置知识2、创建ChatGLM交互界面…

YOLOv8改进 | 图像去雾 | 特征融合注意网络FFA-Net增强YOLOv8对于模糊图片检测能力(北大和北航联合提出)

一、本文介绍 本文给大家带来的改进机制是由北大和北航联合提出的FFA-net: Feature Fusion Attention Network for Single Image Dehazing图像增强去雾网络&#xff0c;该网络的主要思想是利用特征融合注意力网络&#xff08;Feature Fusion Attention Network&#xff09;直接…

基于单片机的Buck型变换器控制

摘要&#xff1a;对于电子产品而言&#xff0c;必不可少的供电电源&#xff0c;随着人们对电子产品的安全性能要求越来越高&#xff0c;变相的对供电电源提出了新的机遇和挑战。Buck型变换器控制的研究一直是该领域重要的一方面&#xff0c;对于直流斩波电路而言&#xff0c;研…

C#在未安装Halcon环境中调用Halcon的方法

1.1 找到Halcon的dll 将Halcon安装路径下的所有dll复制进一个文件夹内 1.2 放入程序目录下 1.3 设置程序引用目录文件 在App.config中添加如下代码 <runtime><assemblyBinding xmlns"urn:schemas-microsoft-com:asm.v1"><probing privatePath"H…

前端开发小技巧【Vue篇】 - 样式穿透 + 绑定变量

前言 样式穿透 Vue都是通过深度选择器来样式穿透的。当我们在写项目的时候&#xff0c;经常会导入第三方库&#xff0c;有些特殊的情况&#xff0c;就是在导入第三方库后&#xff0c;呈现的样式并不是我们想要的样式&#xff0c;所以我们需要对第三方的样式进行修改&#xff1…

JVM简单调优

jdk自带了许多对jvm进行监控的程序&#xff0c;例如JVisualVM、jstack等等。 现在进行一些简单的对jvm的监控。 我们可以使用JVisualVM来对堆区进行图形化监控。 我们可以在命令行输入jvisualvm&#xff0c;然后就进入了jvisualvm的图形化界面。 然后我们随便执行一个主方法…

选型|匠芯创工业级显示控制MCU

D13x系列微控制器 匠芯创D13x系列是一款基于RISC-V架构的高性能、国产自主、工业级跨界MCU&#xff0c;配备强大的2D图形加速、PNG解码、JPEG编解码引擎&#xff0c;具有丰富的屏接口&#xff0c;具有工业宽温、高可靠性、高开放性&#xff0c;可广泛应用于工业HMI、网关、串口…