Linux:线程同步

作者 : admin 本文共4380个字,预计阅读时间需要11分钟 发布时间: 2024-06-16 共1人阅读

Linux:线程同步

    • 线程同步
      • 条件变量 cond

线程同步

同步的含义是:

同步:线程之间按照一定顺序访问资源

线程在占用锁的时候,是不受控制的,这就有可能导致一个竞争能力强的线程,从头到尾都占用一个锁。刚释放这个锁,就又被同一个线程申请走了。

线程是通过条件变量来进行线程同步的

条件变量 cond

条件变量condpthread库中,需要头文件,创建与销毁方式如下:

条件变量的类型是pthread_cond_t,分为全局条件变量局部条件变量,它们的创建方式不同。

全局cond

想要创建一个全局的条件变量很简单,直接定义即可:

pthread_cond_t xxx = PTHREAD_COND_INITIALIZER;

这样就创建了一个名为xxx的变量,类型是pthread_cond_t,即这个变量是一个条件变量全局的条件变量必须用宏PTHREAD_COND_INITIALIZER进行初始化!

另外,全局的条件变量不需要手动销毁

局部cond

局部的条件变量是需要通过接口来初始化与销毁的,接口如下:

pthread_cond_init

pthread_cond_init函数用于初始化一个条件变量,函数原型如下:

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

参数:

  • restrict cond:类型为pthread_cond_t *的指针,指向一个条件变量,对其初始化
  • restrict attr:用于设定该条件变量的属性,一般不用,设为空指针即可

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


pthread_cond_destroy

pthread_cond_destroy函数用于销毁一个条件变量,函数原型如下:

int pthread_cond_destroy(pthread_cond_t *cond);

参数:类型为pthread_cond_t *的指针,指向一个条件变量,销毁该条件变量

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


创建好条件变量后,就要使用这个条件变量,主要是两个操作:等待条件满足唤醒线程

如图所示:

Linux:线程同步插图

现在有三个线程thread-1thread-2thread-3,这三个线程争夺一个临界资源。

thread-1申请到了锁mutex,但是由于我们给这个临界资源添加了条件变量:此时thread-1不能直接访问临界资源,而是进入等待队列,并且释放持有的锁

Linux:线程同步插图(1)

由于锁被释放,后续线程可以继续申请这个锁。于是thread-2thread-3也分别申请到了mutex,通过相同的方式进入了等待队列

Linux:线程同步插图(2)

现在所有线程都在等待队列中,这些线程因为没有满足特定条件,所以不能访问临界资源。但是为什么要进等待队列呢?

因为要保证线程同步,也就是说,一开始谁先访问的临界资源,那么后续条件满足时,就让谁先来访问这个资源

pthread_cond_wait

pthread_cond_wait函数用于让一个线程进入等待队列等待,直到被唤醒,函数原型如下:

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

参数:

  • restrict cond:类型是pthread_cond_t *的指针,即在哪一个条件变量下等待
  • restrict mutex:类型是pthread_mutex_t *的指针,因为在进入等待队列前,线程是持有锁的状态,此处传入锁的指针,就是为了帮助这个线程释放该锁,从而让其他线程也可以申请锁,进入等待队列

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

那么进入等待队列后,又要如何唤醒内部的线程呢?

有两种唤醒方式:唤醒一个线程唤醒所有线程

pthread_cond_signal

pthread_cond_signal用于唤醒等待队列的第一个线程,让其访问临界区的代码,函数原型如下:

int pthread_cond_signal(pthread_cond_t *cond);

参数:cond用于指明一个条件变量,说明要唤醒哪一个条件变量下等待的线程。

比如刚刚三个线程都进入了等待队列

Linux:线程同步插图(3)

当使用pthread_cond_signal唤醒一个线程时:线程重新获得之前释放的锁mutex,随后访问临界区代码

Linux:线程同步插图(4)

当后续条件再次满足,thread-2thread-3也会依次再次获得锁,从而访问到临时资源。

假设现在thread-1访问完毕临界资源后,立马再次申请了锁:

Linux:线程同步插图(5)

由于条件变量的存在,therad-1不能直接访问资源,要去等待队列等待,此时thread-1进入等待队列尾部

Linux:线程同步插图(6)

这样就可以避免一个线程一直占用临界资源,从而完成线程同步了。

pthread_cond_broadcast

pthread_cond_broadcast用于唤醒等待队列中的所有线程,函数原型如下:

int pthread_cond_broadcast(pthread_cond_t *cond);

参数:cond用于指明一个条件变量,说明要唤醒哪一个条件变量下等待的线程。

还是刚才的三个线程都处于等待队列的情况:

Linux:线程同步插图(7)

当我现在用pthread_cond_broadcast唤醒所有线程,此时thread-1thread-2thread-3都被唤醒了,难道他们一起访问临界资源吗?不是的,此时所有被唤醒的线程再次竞争同一把锁,竞争到锁的线程才访问临界资源。当前一个线程访问完毕后,剩下的线程继续竞争,再访问临界资源

讲解完条件变量的接口,我写一个示例帮助大家理解。

class thread
{
public:
    pthread_t _tid;
    string _name;
};

pthread_mutex_t g_mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t g_cond = PTHREAD_COND_INITIALIZER;

void master(string name)
{
    while(true)
    {
        sleep(1);
        pthread_cond_signal(&g_cond);
    }
}

void* server(void* argv)
{
    thread* thd = (thread*)argv;
    
    while(true)
    {
        pthread_mutex_lock(&g_mutex);

        pthread_cond_wait(&g_cond, &g_mutex);

        cout << thd->_name << " is weakup!!!" << endl;

        pthread_mutex_unlock(&g_mutex);
    }

    return nullptr;
}

int main()
{
    vector<thread> threads(5);

    for(int i = 0; i < 5; i++)
    {
        threads[i]._name = "thread-" + to_string(i + 1);
        pthread_create(&threads[i]._tid, nullptr, server, (void*)&threads[i]);
    }

    master("master thread");

    return 0;
}

首先我简单封装了一个线程类:

class thread
{
public:
    pthread_t _tid;
    string _name;
};

其包含两个成员,_tid存储该线程的TID_name存储该线程的名字。

随后定义了两个全局变量,分别是互斥锁g_mutex以及条件变量g_cond

主线程执行函数master

void master(string name)
{
    while(true)
    {
        sleep(1);
        pthread_cond_signal(&g_cond);
    }
}

也就是每秒钟通过ptherad_cond_signal唤醒一个队列中的线程。

main函数中:

int main()
{
    vector<thread> threads(5);

    for(int i = 0; i < 5; i++)
    {
        threads[i]._name = "thread-" + to_string(i + 1);
        pthread_create(&threads[i]._tid, nullptr, server, (void*)&threads[i]);
    }

    master("master thread");

    return 0;
}

先创建了五个线程对象,随后在for循环内部通过pthread_create创建线程,以及给这些线程命名,这些线程都去执行了server函数,而主线程执行master函数。

server函数如下:

void* server(void* argv)
{
    thread* thd = (thread*)argv;
    
    while(true)
    {
        pthread_mutex_lock(&g_mutex);

        pthread_cond_wait(&g_cond, &g_mutex);

        cout << thd->_name << " is weakup!!!" << endl;

        pthread_mutex_unlock(&g_mutex);
    }

    return nullptr;
}

这也是示例中最核心的部分,线程进入了while循环后,先通过pthread_mutex_lock加锁,意图访问临界资源。

但是由于条件变量的限制,其还没有访问到临界资源,就执行了pthread_cond_wait,进入等待队列,并释放掉了自己之前申请的锁。

每秒钟主线程在master中唤醒一个线程,当线程被唤醒后,便重新拿到锁,执行临界资源cout <_name << " is weakup!!!" << endl;,也就是显示器资源,最后释放自己的锁。

注意:server中,是没有任何sleep函数的,但是由于master中限制了每秒钟唤醒一次,所以最后线程会以同步的形式,每隔一秒依次占用显示器,而不会发生某个线程一直输出

输出结果:

Linux:线程同步插图(8)

可以看到,线程按照1 2 3 4 5的顺序依次输出了,这就是线程同步的作用。

我们再看看pthread_cond_broadcast的效果,现在把master改为如下代码:

void master(string name)
{
    while(true)
    {
        sleep(3);
        cout << "-------------" << endl;
        pthread_cond_broadcast(&g_cond);
    }
}

现在主线程每隔三秒唤醒等待队列中的所有线程,为了方便观察,我每次额外输出一条横线-----------

输出结果:

Linux:线程同步插图(9)

由于线程被全部唤醒后,此时它们又要再次竞争锁,每个轮次情况不一样,所以输出的顺序就不一样。比如第一次输出,thread-1竞争力比较强,而第三次输出,thread-2竞争力比较强。但是可以保证的是,每个轮次中1 2 3 4 5都获取到了一次资源,因此也算一种同步


本站无任何商业行为
个人在线分享 » Linux:线程同步
E-->