Linux线程同步

Linux线程同步

  • 1. 线程同步
    • 1.1 线程同步
    • 1.2 线程互斥
    • 1.3 同步机制
  • 2. 条件变量
    • 2.1 条件变量的基本概念
    • 2.2 条件变量函数
    • 2.3 条件变量的使用
  • 3. POSIX信号量
    • 3.1 信号量的基本概念
    • 3.2 POSIX信号量有关函数
    • 3.3 信号量的使用
    • 3.4 条件变量和信号量的区别

1. 线程同步

在多道程序环境下, 系统中各进程以不可预测的速度向前推进, 进程的异步性会造成了结果的不可再现性。 为防止这种现象, 异步的进程间推进受到二种限制:

  1. 间接相互制约关系( 资源共享关系/竞争关系)
    多个线程因为使用共享资源而产生竞争关系, 在抢占使用资源时可能导致使用失败, 都达不到目的, 因而要有信息交换以保证各得其所。
  2. 直接相互制约关系( 相互合作关系)
    一组线程协同完成一个任务, 它们之间是合作关系。 这种情况需要对于相互合作的多个线程的执行次序进行协调, 这样就必须交换信息, 以免出现时间上的差错。

1.1 线程同步

线程的同步指系统中多个线程中发生的事件存在某种时序关系,需要相互合作, 共同完成一项任务。 具体说, 一个线程运行到某一点时要求另一伙伴线程为它提供消息, 在未获得消息之前, 该线程处于等待状态, 获得消息后被唤醒进入就绪态。

1.2 线程互斥

线程的互斥当某一进程访问某一资源时, 不允许别的线程同时访问, 这种限制称为互斥, 即多个线程在访问某些资源( 如临界资源) 时, 也要有一种执行次序上的协调, 当一个线程访问完毕, 另一个线程才能访问。 所以就其本质来讲,互斥仍是一种同步。

临界资源:多线程执行流共享的资源就叫做临界资源。
临界区:每个线程内部,访问临界资源的代码,就叫做临界区。
原子性:不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成。

使用临界区的原则:
有空让进: 当无线程在临界区时, 相应的临界资源处于空闲状态, 因而允许一个请求进入临界区的线程立即进入自己的临界区;
无空等待: 当已有线程进入自己的临界区时, 即相应的临界资源正被访问, 因而其它试图进入临界区的线程必须等待, 以保证进程互斥地访问临界资源;
有限等待: 任一线程进入临界区的要求应在有限的时间内得到满足;
让权等待: 处于等待状态的线程应放弃占用CPU, 以使其他线程有机会得到CPU的使用权。
多中择一: 当没有线程在临界区, 而同时有多个线程要求进入自己的临界区, 只能让其中之一进入临界区, 其他进程必须等待。

1.3 同步机制

1.用于保证多个线程在执行次序上的协调关系的相应机制称为进程同步机制。
2.一个访问临界资源的线程采用进程同步机制后描述如下:
begin remainder section 1; 剩余区1
进入区( entry section)
critical section ; 临界区
退出区( exit section)
remainder section 2 ; 剩余区2
end
线程同步机制在临界区前加上进入区, 它负责对欲访问的临界资源状态进行检查, 以决定是允许该线程进入临界区还是等待。 同时在临界区后加上退出区, 它负责释放临界资源以便其它等待该临界资源的进程使用。
3.解决线程互斥的同步机制有软件方法、 硬件方法、 信号量机制和管程等。

  1. 完全利用软件方法, 有很大局限性( 如不适于多线程) , 现在已很少采用。
  2. 硬件方法解决线程互斥:
    可以利用某些硬件指令- - 机器指令,用于保证两个动作的原子性, 如在一个取指令周期中对一个存储器单元的读和写或者读和测试。 由于这些动作在一个指令周期中执行, 他们不会受到其他指令的干扰。

硬件方法的优点
– 适用于任意数目的进程, 在单处理器或多处理器上
– 简单, 容易验证其正确性
– 可以支持进程内存在多个临界区, 只需为每个临界区设立一个布尔变量
硬件方法的缺点
– 当有进程在临界区内时, 其他想进入临界区的进程必须不断地进行测试, 处于一种忙等待状态, 要耗费CPU时间, 不能实现“ 让权等待”。
– 可能"饥饿": 从等待进程中随机选择一个进入临界区, 有的进程可能一直选不上。

2. 条件变量

2.1 条件变量的基本概念

条件变量是一种线程间通信的机制,它允许一个线程等待另一个线程满足某个条件。当一个线程需要等待某个条件时,它可以调用条件变量的wait()方法,这将导致该线程被阻塞,直到另一个线程调用条件变量的signal()方法或broadcast()方法来唤醒它。当一个线程调用signal()方法时,它将唤醒等待该条件变量的一个线程;而当一个线程调用broadcast()方法时,它将唤醒所有等待该条件变量的线程。

在使用条件变量时,通常需要与互斥锁一起使用,以确保线程在等待条件变量时不会占用CPU资源。当一个线程需要等待某个条件时,它会首先获取互斥锁,然后再调用条件变量的wait()方法。当另一个线程满足了该条件时,它会获取互斥锁,并调用条件变量的signal()方法或broadcast()方法来唤醒等待该条件变量的线程。

2.2 条件变量函数

1.初始化函数

int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrictattr);
参数: cond:要初始化的条件变量
attr:NULL

2.销毁

int pthread_cond_destroy(pthread_cond_t *cond)

3.等待条件满足

int pthread_cond_wait(pthread_cond_t restrict cond,pthread_mutex_trestrict mutex);
参数: cond:要在这个条件变量上等待
mutex:互斥量

4.唤醒等待

int pthread_cond_broadcast(pthread_cond_t *cond); //唤醒所有等待该条件变量的线程
int pthread_cond_signal(pthread_cond_t *cond); //唤醒等待该条件变量的线程中的某一个

2.3 条件变量的使用

我们用5个线程来看现象,5个线程执行自己的函数,其中线程访问临界资源,访问临界资源需要保证资源安全,所以需要加锁。然后再模拟等待某种条件,可以调用条件变量的wait()方法,这将导致该线程被阻塞,直到另一个线程调用条件变量的signal()方法或broadcast()方法来唤醒它。

#include 
#include 
#include 
using namespace std;


const int num = 5;

pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

void *threadrun(void *args)
{
    string name = static_cast<char*>(args);
    
    while (true)
    {
        pthread_mutex_lock(&mutex);
        pthread_cond_wait(&cond, &mutex); // pthread_cond_wait,调用的时候,会自动释放锁
        //...临界资源
        //...临界资源
        cout << name << " running" << endl;
        pthread_mutex_unlock(&mutex);
    }
}
int main()
{
    pthread_t tids[num];
    for (int i = 0; i < num; ++i)
    {
        char *name = new char[64];
        snprintf(name, 64, "thread-%d", i);
        pthread_create(&tids[i], nullptr, threadrun, name);
    }

    sleep(3);

    while (true)
    {
        cout << "main thread wakeup thread..." << endl;
        //pthread_cond_signal(&cond); //一个一个的唤醒
    	pthread_cond_broadcast(&cond); //唤醒所有等待该条件变量的线程
    	sleep(1);
    }
    for (int i = 0; i < num; ++i)
    {
        pthread_join(tids[i], nullptr);
    }
    pthread_cond_destroy(&cond);

    return 0;
}

由下图可以看到,pthread_cond_signal可以将等待中的线程一个一个的唤醒,而pthread_cond_broadcast唤醒所有等待该条件变量的线程。
可以用该命令查看线程信息:while : ; do ps -aL | head -1 && ps -aL | grep threadcond; sleep 1; done 。ps -aL :查看所有的线程信息;head -1:查看的是每列的属性,如下图为:PID LWP TTU TIME CMD;grep threadcond:获取查看线程的线程名。
Linux线程同步_第1张图片
Linux线程同步_第2张图片
条件变量,允许多线程在cond中队列式等待(就是一种顺序)。

3. POSIX信号量

3.1 信号量的基本概念

信号量是一种同步对象,用于保持在0至指定最大值之间的一个计数值。当线程完成一次对该信号量对象的等待(wait)时,该计数值减一;当线程完成一次对信号量对象的释放(release)时,计数值加一。信号量的概念是由荷兰计算机科学家艾兹赫尔·戴克斯特拉发明的。信号量可以用于进程间的互斥操作,保证只有一个进程可以访问某个资源;也可以用于进行进程间的同步操作,当一个进程完成某个任务后,可以通知另一个等待的进程开始执行。

信号量具有两种操作动作,称为V(signal())P(wait())(即部分参考书常称的“PV操作”)。V操作会增加信号量S的数值,P操作会减少它。工作方式:初始化,给予它一个非负数的整数值。执行P(wait()),信号量S的值将被减少。企图进入临界区段的行程,需要先执行P(wait())。当信号量S减为负值时,行程会被阻塞,不能继续;当信号量S不为负值时,行程可以获准进入临界区段。执行V(signal()),信号量S的值会被增加。结束离开临界区段的行程,将会执行V(signal())。当信号量S不为负值时,先前被阻塞的其他行程将可获准进入临界区段。
具体来说,信号量代表可用资源实体的数量,是一种用于控制对共享资源的访问的机制,它可以用来保证同一时刻只有一个线程访问共享资源。当一个线程需要访问共享资源时,它必须先获取信号量,是一种资源的预定机制。如果信号量的值为1,则该线程可以访问共享资源,并将信号量的值减1。当该线程完成对共享资源的访问后,它必须释放信号量,使其值加1。如果有其他线程正在等待该信号量,则其中一个线程将被唤醒并继续执行。

3.2 POSIX信号量有关函数

1.初始化信号量
sem_init函数是一个POSIX信号量函数,用于初始化未命名的信号量。它的原型如下:

#include
int sem_init(sem_t *sem, int pshared, unsigned int value);
sem:指向要初始化的信号量的指针。
pshared:指定信号量的类型。如果pshared的值为0,则信号量是当前进程的局部信号量;否则,其他进程可以共享这个信号量。
value:信号量的初始值。
该函数成功时返回0,失败时返回-1。

2.销毁信号量
sem_destroy函数是一个POSIX信号量函数,用于销毁一个未命名的信号量。它的原型如下:

#include
int sem_destroy(sem_t *sem);
sem:指向要销毁的信号量的指针。
该函数成功时返回0,失败时返回-1,并设置errno为相应的错误码。

  1. 该函数应该与sem_init成对调用,只有通过sem_init初始化的信号量才能用sem_destroy销毁。
  2. 销毁一个有其他进程或线程正在等待的信号量会导致未定义的行为。
  3. 使用一个已经销毁的信号量会产生未定义的结果,除非该信号量重新用sem_init初始化。

3.等待信号量
sem_wait函数是一个用于等待信号量的函数,它的原型如下:

#include
int sem_wait(sem_t *sem);
sem:指向要等待的信号量的指针。
该函数会将信号量的值减一,如果信号量的值大于零,则立即返回;如果信号量的值为零,则阻塞当前线程或进程,直到信号量的值变为正数为止。
该函数通常用于实现进程或线程间的同步,例如互斥锁或条件变量等。
该函数成功时返回0,失败时返回-1,并设置errno为相应的错误码。

4.发布信号量
sem_post函数是一个POSIX信号量函数,用于以原子操作的方式将信号量的值加1。它的原型如下:

#include
int sem_post(sem_t *sem);
sem:指向要增加的信号量的指针。
该函数通常用于释放资源或唤醒等待的进程或线程。相当于V操作。
与sem_wait一样,sem指向的对象是由sem_init或sem_open调用初始化的信号量。
调用成功时返回0,失败时返回-1,并设置errno为相应的错误码。

3.3 信号量的使用

用3个线程来模拟抢占资源,首先有100个资源,每个线程抢占这100个资源,抢占完后,线程等待,主线程释放资源,然后多线程又继续抢资源。

#include 
#include 
#include 
#include 
using namespace std;


const int num = 3;
int resource = 100;

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
sem_t sem;

void* threadrun(void *args)
{
    string name = static_cast<char*>(args);
    while (true)
    {
    	usleep(200);
        sem_wait(&sem); //resource为0,则阻塞当前线程或进程,直到信号量的值变为正数为止。
        pthread_mutex_lock(&mutex);
        cout << name << ":" << resource-- << endl;
        pthread_mutex_unlock(&mutex);
    }
    return nullptr;
}
int main()
{
    pthread_t tids[num];
    sem_init(&sem, 0, resource); //如果pshared的值为0,则信号量是当前进程的局部信号量;否则,其他进程可以共享这个信号量。
    for (int i = 0; i < num; ++i)
    {
        char *name = new char[64];
        snprintf(name, 64, "thread-%d", i + 1);
        pthread_create(&tids[i], nullptr, threadrun, name);
    }
    sleep(3);
    while (true)
    {
        sem_post(&sem);
        resource++; //资源+1
        sleep(1);
    }
    pthread_mutex_destroy(&mutex);
    sem_destroy(&sem);
    return 0;
}

从上述代码可以看到,使用信号量就不需要对于临界资源进行判断,因为信号量本身就是一个计数器,是一种资源的预定机制,因为对临界资源进行判断,需要进入临界区,所以需要加锁后判断,而信号量就不需要在加锁后判断。所以打印结果应该是多线程将100个资源很快抢占完,然后又随着主线程释放资源又进行抢占资源。
Linux线程同步_第3张图片

3.4 条件变量和信号量的区别

条件变量和信号量的区别

  1. 条件变量和信号量都是用于线程同步的机制,但是它们的作用不同。条件变量是一种线程间通信的机制,它允许一个线程等待另一个线程满足某个条件。而信号量则是一种用于控制对共享资源的访问的机制,它可以用来保证同一时刻只有一个线程访问共享资源。
  2. 条件变量通常与互斥锁一起使用,以确保线程在等待条件变量时不会占用CPU资源。当条件变量满足时,等待该条件变量的线程将被唤醒并继续执行。
  3. 信号量通常用于控制对共享资源的访问。当一个线程需要访问共享资源时,它必须先获取信号量。如果信号量的值为1,则该线程可以访问共享资源,并将信号量的值减1。当该线程完成对共享资源的访问后,它必须释放信号量,使其值加1。如果有其他线程正在等待该信号量,则其中一个线程将被唤醒并继续执行。

你可能感兴趣的:(linux)