C++网络编程入门学习(三)-- 多线程的创建与同步

news2024/10/12 0:27:11

C++网络编程入门学习(三)-- 多线程的创建与同步

  • 多线程的创建与同步
    • 线程的创建
    • 线程退出函数
    • 线程回收函数
    • 线程分离
    • 线程同步
      • 线程同步的方式
    • 互斥锁
      • 互斥锁的相关函数
      • 互斥锁的使用示例代码
    • 死锁
      • 死锁的四个必要条件
      • 造成死锁的场景
      • 避免死锁的方法
    • 读写锁
      • 读写锁的使用示例
    • 条件变量
      • 1. 条件变量的概念
      • 2. 条件变量的工作原理
      • 3. 常用函数
    • 信号量

多线程的创建与同步

线程的创建

示例代码

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<pthread.h>

void* callback(void* arg) {
    for(int i = 0; i < 5; ++i) {
        printf("subthread: i =  %d\n", i);
    }
    printf("subthread: %ld\n", pthread_self());
    return NULL;
}

int main() {
    pthread_t tid; // 定义一个pthread_t类型的变量来存储线程ID
    pthread_create(&tid, NULL, callback, NULL); // 创建子线程
    for(int i = 0; i < 5; ++i) {
        printf("main-thread: i = %d\n", i);
    }
    printf("main-thread: %ld\n", pthread_self()); // 打印主线程的ID
    sleep(3); // 为了保证子线程能够执行,防止主线程抢占时间片执行完后销毁,导致子线程无法执行
    return 0;
}

pthread_create 函数的四个参数讲解:

  1. pthread_t *thread
    • 这是一个指向 pthread_t 类型的指针,用于存储新创建线程的ID。通过这个ID,你可以后续对该线程进行操作(如等待线程结束、取消线程等)。
  2. const pthread_attr_t *attr
    • 这个参数用来设置线程的属性。如果传入 NULL,线程将使用默认属性(如默认栈大小、调度策略等)。如果你想设置特定的线程属性,可以创建一个 pthread_attr_t 对象并传入。
  3. void *(*start_routine)(void *)
    • 这是一个指向函数的指针,表示线程执行的起始函数。该函数的返回类型必须是 void*,并且只能接受一个 void* 类型的参数。在线程执行完毕后,可以通过该函数返回的指针来获取结果。
  4. void *arg
    • 这个参数是传递给线程起始函数的参数。你可以将任何数据结构的指针传递给该参数。在线程内部,你需要将其转换回原始类型来使用。在这个例子中,传入的是 NULL,表示不需要传递任何参数。

线程退出函数

pthread_exit 函数

定义

pthread_exit 是一个用于显式终止调用线程的函数,其原型如下:

void pthread_exit(void *retval);

参数

  • retval:传递给等待该线程结束的其他线程的返回值,可以是任何类型的指针(如 void*)。

特性

  • 当调用 pthread_exit 时,线程会立即停止执行,释放它所占用的资源(如线程栈),并将 retval 返回给其他线程。
  • 其他线程可以通过 pthread_join 函数来获取该返回值。
  • 调用 pthread_exit 不会影响其他线程的运行,程序会继续运行。

线程回收函数

pthread_join 是 POSIX 线程库中的一个函数,主要用于使调用线程(通常是主线程)等待指定的子线程终止。通过这个函数,主线程可以同步等待子线程的完成并获得子线程的返回值。它通常用于线程间的协调,以确保主线程不会在子线程执行完毕之前退出。

int pthread_join(pthread_t thread, void **retval);

参数解析

  1. pthread_t thread
    • 线程标识符,用于指定要等待的线程。它通常是在线程创建时,通过 pthread_create 返回的 pthread_t 类型变量。
  2. void **retval
    • 这是一个二级指针,用于接收线程的退出状态(即子线程的返回值)。如果你不关心线程的返回值,可以将其设置为 NULL
    • 子线程可以通过 pthread_exitreturn 返回某个值,该值会被存储在 retval 指向的指针中。

返回值

  • 成功时返回 0
  • 失败时返回错误码,常见错误包括:
    • ESRCH: 指定的线程不存在。
    • EINVAL: 线程不可被等待,或者线程已经处于分离状态(detached)。
    • EDEADLK: 导致死锁的情况,例如一个线程试图等待自己。

pthread_join 的主要功能:

  1. 等待子线程结束
    • 主线程调用 pthread_join 后,会阻塞执行,直到指定的子线程结束。它确保主线程不会在子线程结束之前退出或执行其他操作。
  2. 获取子线程的返回值
    • 通过 pthread_join 的第二个参数 retval,主线程可以获得子线程通过 pthread_exitreturn 返回的结果。这对于需要从子线程获取处理结果的场景非常有用。
  3. 防止僵尸线程
    • 如果主线程不等待子线程结束而直接退出,子线程可能成为“僵尸线程”,即其资源没有被释放。使用 pthread_join 可以确保子线程终止后其资源被回收,避免资源泄漏。

示例代码

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<pthread.h>

// 定义一个结构体类型Test,包含两个整数成员
struct Test {
    int num;  
    int age;  
};

// 子线程的回调函数,接受一个void*类型的参数
void* callback(void* arg) {
    for(int i = 0; i < 5; ++i) {
        printf("subthread: i =  %d\n", i); 
    }
    
    printf("subthread: %ld\n", pthread_self());

    // 将传入的参数arg转换为Test结构体指针类型
    struct Test *t = (struct Test*)arg;

    // 修改结构体中的成员值
    t->num = 100;
    t->age = 6;

    pthread_exit(t);  // 通过pthread_exit函数将结构体指针t作为线程的返回值

    return NULL;
}

int main() {
    pthread_t tid; // 定义一个pthread_t类型变量来存储线程ID
    struct Test t; // 定义一个Test结构体变量t,用于传递给子线程

    // 创建子线程,执行callback函数,并将结构体t的地址作为参数传递给子线程
    pthread_create(&tid, NULL, callback, &t);

    // 打印主线程的线程ID
    printf("main-thread: %ld\n", pthread_self());
    
    void *ptr; // 定义一个void*类型指针,用于接收子线程的返回值

    pthread_join(tid, &ptr); // 主线程等待子线程结束,并通过ptr获取子线程返回的值(t的地址)

    // 打印主线程中结构体t的成员值,注意此时t的值已经被子线程修改
    printf("num = %d, age = %d\n", t.num, t.age);

    return 0;
}

线程分离

pthread_detach 函数用于将一个线程的状态设置为分离状态,使得该线程在终止时可以自动释放所有与其相关的资源,而不需要其他线程调用 pthread_join 来显式回收它的资源。

pthread_detach 函数的原型

int pthread_detach(pthread_t thread);
  • thread:要分离的线程的线程 ID。
  • 返回值:如果成功,返回 0;如果失败,返回一个错误码。

pthread_detach 的用途

当你确定不需要主线程或者其他线程通过 pthread_join 来等待某个线程结束时,可以使用 pthread_detach 将该线程分离。这种操作对于那些不需要返回结果的线程尤其有用,因为它避免了内存泄漏的问题——线程终止后,操作系统会自动回收它的资源。

线程同步

假设有4个线程A、B、C、D,当前一个线程A对内存中的共享资源进行访问的时候,其他线程B, C, D都不可以对这块内存进行操作,直到线程A对这块内存访问完毕为止,B,C,D中的一个才能访问这块内存,剩余的两个需要继续阻塞等待,以此类推,直至所有的线程都对这块内存操作完毕。 线程对内存的这种访问方式就称之为线程同步,通过对概念的介绍,我们可以了解到所谓的同步并不是多个线程同时对内存进行访问,而是按照先后顺序依次进行的

线程同步的方式

  1. 互斥锁(Mutex)
    • 定义:互斥锁是一种常用的同步机制,用于保护共享资源,确保同一时间只有一个线程可以访问该资源。
    • 使用:通过 pthread_mutex_lock 加锁,pthread_mutex_unlock 解锁。只有持有锁的线程才能访问临界区。
  2. 条件变量(Condition Variable)
    • 定义:条件变量是一种同步机制,允许线程在某些条件满足时被唤醒。
    • 使用:结合互斥锁使用,通过 pthread_cond_wait 阻塞线程,pthread_cond_signalpthread_cond_broadcast 唤醒一个或多个等待的线程。
  3. 读写锁(Read-Write Lock)
    • 定义:读写锁允许多个线程同时读取资源,但在写入时,必须独占该资源。
    • 使用:通过 pthread_rwlock_rdlock 获取读锁,pthread_rwlock_wrlock 获取写锁,pthread_rwlock_unlock 解锁。
  4. 信号量(Semaphore)
    • 定义:信号量是一种计数器,用于控制对共享资源的访问。
    • 使用:信号量的值表示可以访问资源的线程数量。使用 sem_wait 减少信号量值,使用 sem_post 增加信号量值。

互斥锁

互斥锁(mutex)是一种同步原语,用于控制对共享资源的访问,确保在任何时刻只有一个线程可以访问该资源,以防止数据竞争和不一致性。互斥锁是多线程编程中非常重要的工具。

互斥锁的基本概念

  1. 锁定和解锁
    • 线程在访问共享资源之前会请求互斥锁,成功获取后才能继续执行。
    • 访问完共享资源后,线程必须释放互斥锁,以便其他线程可以获得访问权限。
  2. 避免竞争条件
    • 通过互斥锁,可以防止多个线程同时修改共享资源,从而避免数据损坏或不一致的情况。
  3. 死锁风险
    • 如果多个线程相互等待对方释放锁,可能会导致死锁,因此需要小心设计互斥锁的使用。

pthread_mutex_t 类型

pthread_mutex_t 是 POSIX 线程库中用于定义互斥锁的类型。它是一个结构体,用于存储锁的状态和相关信息。

在创建的锁对象中保存了当前这把锁的状态信息:锁定还是打开,如果是锁定状态还记录了给这把锁加锁的线程信息(线程ID)。一个互斥锁变量只能被一个线程锁定,被锁定之后其他线程再对互斥锁变量加锁就会被阻塞,直到这把互斥锁被解锁,被阻塞的线程才能被解除阻塞。一般情况下,每一个共享资源对应一个把互斥锁,锁的个数和线程的个数无关

互斥锁的相关函数

  1. 初始化

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

    参数说明

    1. pthread_mutex_t \*mutex
      • 指向要初始化的互斥锁的指针。
      • 在调用此函数之前,该互斥锁必须已经被声明(通常是定义为 pthread_mutex_t 类型)。
    2. const pthread_mutexattr_t \*attr
      • 指向互斥锁属性对象的指针,可以用来设置互斥锁的行为。
      • 如果设置为 NULL,将使用默认属性。
      • 属性对象可用于控制互斥锁的类型(如普通互斥锁、递归互斥锁等),但是如果不需要特殊的锁类型,通常直接传入 NULL

    返回值

    • 返回 0 表示成功。
    • 返回错误码表示初始化失败,常见的错误包括:
      • EINVAL:提供的属性参数无效。
      • ENOMEM:内存不足,无法分配互斥锁的内部结构。
  2. 修改互斥锁的状态, 将其设定为锁定状态, 这个状态被写入到参数 mutex 中

    int pthread_mutex_lock(pthread_mutex_t *mutex);
    

    参数说明

    • pthread_mutex_t *mutex
      • 指向要加锁的互斥锁的指针。
      • 这个互斥锁必须已经被初始化(通常通过 pthread_mutex_init 函数)。

    返回值

    • 返回 0 表示成功加锁。
    • 返回错误码表示加锁失败,常见的错误包括:
      • EINVAL:提供的互斥锁无效,可能是未初始化或已经被销毁。
      • EDEADLK:死锁检测,调用线程已经持有该互斥锁,再次尝试加锁将导致死锁。
  3. 尝试加锁

    pthread_mutex_trylock 函数用于尝试对互斥锁进行加锁,但与 pthread_mutex_lock 不同的是,pthread_mutex_trylock 如果互斥锁已经被其他线程占用,则不会阻塞当前线程,而是立即返回一个错误码。

    int pthread_mutex_trylock(pthread_mutex_t *mutex);
    

    参数说明

    • pthread_mutex_t *mutex
      • 指向要加锁的互斥锁的指针。
      • 这个互斥锁必须已经被初始化。

    返回值

    • 返回 0 表示成功加锁。
    • 返回错误码表示加锁失败,常见的错误包括:
      • EINVAL:提供的互斥锁无效,可能是未初始化或已经被销毁。
      • EBUSY:互斥锁当前已被其他线程占用,调用线程未能获得锁。
  4. 对互斥锁解锁

    int pthread_mutex_unlock(pthread_mutex_t *mutex);
    

    参数说明

    • pthread_mutex_t *mutex
      • 指向要解锁的互斥锁的指针。
      • 该互斥锁必须已经被初始化,并且当前线程必须持有该锁。

    返回值

    • 返回 0 表示成功解锁。
    • 返回错误码表示解锁失败,常见的错误包括:
      • EINVAL:提供的互斥锁无效,可能是未初始化或已经被销毁。
      • EPERM:调用线程没有持有该互斥锁,试图解锁未被自己加锁的互斥锁。

    不是所有的线程都可以对互斥锁解锁,哪个线程加的锁, 哪个线程才能解锁成功。

  5. 释放互斥锁资源

    int pthread_mutex_destroy(pthread_mutex_t *mutex);
    

    参数说明

    • pthread_mutex_t *mutex
      • 指向要销毁的互斥锁的指针。
      • 该互斥锁必须已经被初始化,并且在销毁之前,所有的线程都应当释放对它的持有。

    返回值

    • 返回 0 表示成功销毁互斥锁。
    • 返回错误码表示销毁失败,常见的错误包括:
      • EINVAL:提供的互斥锁无效,可能是未初始化或已经被销毁。
      • EBUSY:互斥锁仍然在使用中,可能有线程仍然持有该锁。

互斥锁的使用示例代码

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <pthread.h>

#define MAX 100
// 全局变量
int number;

// 全局变量, 多个线程共享
pthread_mutex_t mutex; // 创建互斥锁

// 线程处理函数
void* funcA_num(void* arg)
{
    for(int i=0; i<MAX; ++i)
    {
        // 如果线程A加锁成功, 不阻塞
        // 如果B加锁成功, 线程A阻塞
        pthread_mutex_lock(&mutex);
        int cur = number;
        cur++;
        usleep(10);
        number = cur;
        pthread_mutex_unlock(&mutex);
        printf("Thread A, id = %lu, number = %d\n", pthread_self(), number);
    }

    return NULL;
}

void* funcB_num(void* arg)
{
    for(int i=0; i<MAX; ++i)
    {
        // a加锁成功, b线程访问这把锁的时候是锁定的
        // 线程B先阻塞, a线程解锁之后阻塞解除
        // 线程B加锁成功了
        pthread_mutex_lock(&mutex);
        int cur = number;
        cur++;
        number = cur;
        pthread_mutex_unlock(&mutex);
        printf("Thread B, id = %lu, number = %d\n", pthread_self(), number);
        usleep(5);
    }

    return NULL;
}

int main(int argc, const char* argv[])
{
    pthread_t p1, p2;

    pthread_mutex_init(&mutex, NULL); // 初始化互斥锁

    // 创建两个子线程
    pthread_create(&p1, NULL, funcA_num, NULL);
    pthread_create(&p2, NULL, funcB_num, NULL);

    // 阻塞,资源回收
    pthread_join(p1, NULL);
    pthread_join(p2, NULL);

    // 销毁互斥锁
    // 线程销毁之后, 再去释放互斥锁
    pthread_mutex_destroy(&mutex);

    return 0;
}

死锁

死锁是指两个或多个线程在执行过程中,因为争夺资源而造成的一种相互等待的状态,使得它们无法继续执行。简单来说,死锁发生时,每个线程都在等待对方释放资源,导致程序无法进行。

死锁的四个必要条件

  1. 互斥条件
    • 至少有一个资源是被非共享的,即每次只能被一个线程占用。
  2. 保持并等待条件
    • 一个线程至少持有一个资源,并等待获取其他资源。
  3. 不剥夺条件
    • 已分配给线程的资源在未使用完之前,不能被其他线程强制夺取。
  4. 循环等待条件
    • 存在一个线程等待链,形成闭环,即线程A等待线程B持有的资源,线程B等待线程C持有的资源,…,最终线程Z又等待线程A持有的资源。

造成死锁的场景

  1. 资源竞争
    • 两个或多个线程同时竞争多个资源。当每个线程持有一个资源并请求另一个时,可能导致死锁。
  2. 顺序资源请求
    • 如果线程在请求资源时总是以不同的顺序请求,可能导致死锁。例如,线程1请求资源A和B,而线程2请求资源B和A。
  3. 嵌套锁
    • 当一个线程已经持有一个锁并试图获取另一个锁,而另一个线程已经持有第二个锁并试图获取第一个锁时,可能会导致死锁。
  4. 线程优先级问题
    • 在优先级较高的线程等待优先级较低的线程释放资源时,可能导致死锁,尤其在实时系统中更为常见。
  5. 长时间持有锁
    • 如果线程在持有锁的情况下进行长时间的计算或等待,可能会导致其他线程长时间等待,从而增加死锁的可能性。

避免死锁的方法

  1. 资源请求顺序
    • 所有线程以相同的顺序请求资源,避免形成循环等待。
  2. 限制资源数量
    • 限制每个线程所持有的资源数量,避免持有过多资源。
  3. 超时机制
    • 在请求资源时设置超时,如果请求资源失败,则释放已经持有的资源。
  4. 使用死锁检测机制
    • 定期检测系统状态,如果发现死锁,可以采取措施,如终止某些线程以打破死锁。
  5. 避免长时间持有锁
    • 尽量缩小临界区的范围,减少持有锁的时间。

读写锁

读写锁是一种特殊的同步机制,允许多个线程同时读取共享资源,但在写入时必须独占该资源。这种机制对于读多写少的场景非常有效,可以提高并发性能。

读写锁是一把锁,锁的类型为pthread_rwlock_t,有了类型之后就可以创建一把互斥锁了:

pthread_rwlock_t rwlock;
  • 读锁(Read Lock)
    • 允许多个线程同时读取资源。
    • 当一个线程持有读锁时,其他线程也可以获取读锁,但不能获取写锁。
  • 写锁(Write Lock)
    • 只允许一个线程对资源进行写操作。
    • 当一个线程持有写锁时,其他任何线程(无论是读锁还是写锁)都不能访问该资源。
  1. 初始化读写锁
int pthread_rwlock_init(pthread_rwlock_t *rwlock, const pthread_rwlockattr_t *attr);
  • 参数:
    • rwlock: 读写锁的地址,传出参数
    • attr: 读写锁属性,一般使用默认属性,指定为NULL
  1. 在程序中对读写锁加读锁, 锁定的是读操作
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);

rwlock:指向一个已经初始化的读写锁对象的指针。调用此函数的线程需要传入正确的锁对象。

  1. 如果加读锁失败, 不会阻塞当前线程, 直接返回错误号
// 这个函数可以有效的避免死锁
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);

pthread_rwlock_tryrdlock 函数用于尝试获取读写锁的读锁。与 pthread_rwlock_rdlock 不同的是,pthread_rwlock_tryrdlock 是非阻塞的,如果无法立即获取读锁,它将返回一个错误,而不会阻塞当前线程。

  1. 在程序中对读写锁加写锁, 锁定的是写操作
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);

pthread_rwlock_wrlock 函数用于获取读写锁的写锁。写锁是独占的,意味着在一个线程持有写锁的情况下,其他线程无法获取读锁或写锁。这种机制保证了对共享资源的安全写入,避免了数据竞争和不一致性。

  1. 如果加写锁失败, 不会阻塞当前线程, 直接返回错误号
// 这个函数可以有效的避免死锁
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);

调用这个函数,如果读写锁是打开的,那么加锁成功;如果读写锁已经锁定了读操作或者锁定了写操作,调用这个函数加锁失败,但是线程不会阻塞,可以在程序中对函数返回值进行判断,添加加锁失败之后的处理动作。

  1. 解锁
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

读写锁的使用示例

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>

#define Max 50

int number = 0; // 全局变量

pthread_rwlock_t rwlock; // 定义读写锁

// 写的线程的处理函数
void* writeNum(void* arg)
{
    for(int i = 0; i < Max; ++i)
    {
        pthread_rwlock_wrlock(&rwlock);
        int cur = number;
        cur ++;
        number = cur;
        printf("++写操作完毕, number : %d, tid = %ld\n", number, pthread_self());
        pthread_rwlock_unlock(&rwlock);
        // 添加sleep目的是要看到多个线程交替工作
        usleep(rand() % 100);
    }

    return NULL;
}

// 读线程的处理函数
// 多个线程可以如果处理动作相同, 可以使用相同的处理函数
// 每个线程中的栈资源是独享
void* readNum(void* arg)
{
    for(int i = 0; i < Max; ++i)
    {
        pthread_rwlock_rdlock(&rwlock);
        printf("--全局变量number = %d, tid = %ld\n", number, pthread_self());
        pthread_rwlock_unlock(&rwlock);
        usleep(rand() % 100);
    }
    return NULL;
}

int main() {
    
    pthread_rwlock_init(&rwlock, NULL); // 初始化读写锁

    // 3个写线程, 5个读的线程
    pthread_t wtid[3];
    pthread_t rtid[5];
    for(int i = 0; i < 3; ++i)
        pthread_create(&wtid[i], NULL, writeNum, NULL);

    for(int i = 0; i < 5; ++i)
        pthread_create(&rtid[i], NULL, readNum, NULL);

    // 释放资源
    for(int i = 0; i < 3; ++i)
        pthread_join(wtid[i], NULL);

    for(int i = 0; i < 5; ++i)
        pthread_join(rtid[i], NULL);

    pthread_rwlock_destroy(&rwlock); // 销毁读写锁

    return 0;
}

条件变量

条件变量(Condition Variables)是用于线程同步的一种机制,它允许线程在某些条件满足前进入等待状态,避免忙等待。线程可以通过条件变量进行等待,直到另一个线程发出信号通知条件发生变化时被唤醒。这种机制常与互斥锁结合使用,以保护共享资源的访问。

条件变量和互斥锁虽然都能阻塞线程,但它们的作用和使用场景有所不同。为了更好地理解它们的区别,下面将从功能使用场景行为等方面进行对比讲解,并通过例子说明。

  1. 功能对比
  • 互斥锁(Mutex)
    • 用于确保同一时刻只有一个线程访问共享资源,从而保护共享数据的完整性,防止数据竞争。
    • 互斥锁通过阻塞线程来防止其他线程在某个线程访问资源时也去操作该资源。
  • 条件变量(Condition Variable)
    • 条件变量用于让线程等待某个条件的发生,而不是立即访问资源。它让线程在条件不满足时进入等待状态,直到条件满足后被唤醒。
    • 条件变量通常配合互斥锁使用,控制多个线程根据特定条件进行同步操作。
  1. 使用场景
  • 互斥锁的使用场景
    • 适用于保护临界区的场景,多个线程共享某个资源,且同一时刻只能有一个线程访问或修改这个资源。例如,多个线程同时写入一个文件。
  • 条件变量的使用场景
    • 适用于等待某个条件发生的场景,线程需要根据某种状态或条件来决定何时继续执行。例如,生产者-消费者模式中,消费者线程需要等待生产者线程生成数据。
  1. 行为对比
  • 互斥锁的行为
    • 当一个线程持有互斥锁时,其他尝试获取该锁的线程会被阻塞,直到持锁的线程释放锁为止。
    • 互斥锁通常不会主动“唤醒”其他线程,除非锁被释放,其他线程才能竞争获取锁。
  • 条件变量的行为
    • 条件变量使线程进入等待状态,直到某个线程发出信号(signal或broadcast)来通知条件发生变化,唤醒等待的线程。
    • 条件变量依赖互斥锁保护共享数据,确保线程在被唤醒后能安全地重新检查条件。

1. 条件变量的概念

  • 等待队列:条件变量维护一个等待队列,任何一个线程可以等待某个条件的发生,直到条件被满足时,被唤醒。
  • 信号:一个线程在某个条件满足时发出信号,唤醒正在等待该条件的其他线程。
  • 互斥锁:条件变量一般和互斥锁结合使用,互斥锁保护共享数据的完整性,而条件变量则让线程有条件地等待某个状态的改变。

2. 条件变量的工作原理

当某个线程等待某个条件变量时:

  • 它会释放与该条件变量关联的互斥锁,并进入等待状态。
  • 当条件满足时,另一个线程发出信号,唤醒这个等待的线程。
  • 被唤醒的线程在重新获得互斥锁后,继续执行。

3. 常用函数

pthread_cond_init:初始化条件变量

int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);
  • cond:指向条件变量的指针。
  • attr:条件变量的属性,通常设置为 NULL 使用默认属性。

pthread_cond_wait:等待条件变量

int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
  • cond:指向条件变量的指针。
  • mutex:指向与条件变量关联的互斥锁的指针。
  • 该函数会自动释放互斥锁并使线程进入等待状态,直到条件变量被唤醒。

pthread_cond_signal:发送条件变量信号(唤醒一个线程)

int pthread_cond_signal(pthread_cond_t *cond);
  • cond:指向条件变量的指针。
  • 该函数会唤醒一个等待该条件变量的线程。

pthread_cond_broadcast:广播条件变量信号(唤醒所有等待的线程)

int pthread_cond_broadcast(pthread_cond_t *cond);
  • 该函数唤醒所有等待该条件变量的线程。

pthread_cond_destroy:销毁条件变量

int pthread_cond_destroy(pthread_cond_t *cond);
  • cond:指向条件变量的指针。
  • 用于销毁不再需要的条件变量。

信号量

信号量(Semaphore) 是一种用于实现线程同步的机制,类似于互斥锁,但它不仅仅限制单个线程对共享资源的访问,还允许多个线程同时访问一定数量的资源。

信号量的概念

信号量可以被看作是一个计数器,它表示可用资源的数量。根据使用场景,信号量可以分为以下两种:

  1. 计数信号量(Counting Semaphore):允许多个线程同时访问资源。计数器的值可以大于1,表示有多少个线程可以访问共享资源。
  2. 二进制信号量(Binary Semaphore):相当于互斥锁,计数器只有0和1两个值,0表示资源被占用,1表示资源可用。

信号量的两个主要操作

  • P操作(wait):也称为“测试操作”或“减操作”。线程试图获取资源时,执行P操作。信号量的值减1。如果结果是负数,则线程被阻塞,等待资源可用。
  • V操作(signal):也称为“释放操作”或“加操作”。线程释放资源时,执行V操作。信号量的值加1,如果有其他线程在等待资源,则唤醒一个等待的线程。

信号量的基本用法

信号量适合于控制多个线程对一定数量的资源的访问,或用于线程之间的协作。例如,如果有5个线程要访问3个共享资源,信号量可以限制同时只有3个线程进入临界区。

信号量的简单例子

下面是一个使用信号量的示例代码,它演示了如何控制多个线程访问一个有限的资源。

#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
#include <unistd.h>

sem_t sem;  // 定义一个信号量

void* thread_func(void* arg) {
    int num = *((int*)arg);  // 获取线程编号
    sem_wait(&sem);          // P操作,尝试进入临界区
    printf("Thread %d: Entering critical section\n", num);
    sleep(2);  // 模拟线程正在使用资源
    printf("Thread %d: Leaving critical section\n", num);
    sem_post(&sem);          // V操作,离开临界区,释放资源
    return NULL;
}

int main() {
    pthread_t threads[5];  // 创建5个线程
    int thread_num[5];     // 用于给每个线程分配编号

    sem_init(&sem, 0, 3);  // 初始化信号量,允许最多3个线程同时进入临界区

    // 创建5个线程
    for (int i = 0; i < 5; ++i) {
        thread_num[i] = i;
        pthread_create(&threads[i], NULL, thread_func, &thread_num[i]);
    }

    // 等待5个线程执行完毕
    for (int i = 0; i < 5; ++i) {
        pthread_join(threads[i], NULL);
    }

    sem_destroy(&sem);  // 销毁信号量
    return 0;
}

代码说明

  1. sem_t sem; 定义了一个信号量。
  2. sem_init(&sem, 0, 3); 初始化信号量,并将计数器设置为3,表示最多允许3个线程进入临界区。
  3. sem_wait(&sem); 在每个线程中执行P操作,尝试获取信号量。如果信号量大于0,线程进入临界区,信号量减1;否则,线程阻塞,等待其他线程释放资源。
  4. sem_post(&sem); 执行V操作,释放信号量,信号量加1。如果有其他线程在等待,它们可以继续进入临界区。

信号量与互斥锁的区别

  • 互斥锁:通常只允许一个线程访问临界区,保证线程对共享资源的独占访问。
  • 信号量:可以允许多个线程同时访问资源,控制线程的数量,适用于资源有限的场景。

信号量的应用场景

  • 资源管理:信号量可以用来控制对有限资源的访问,比如数据库连接池。
  • 生产者-消费者问题:信号量可以协调生产者和消费者之间的操作,确保生产者生产的资源数量和消费者消耗的数量保持平衡。

通过信号量,可以有效地管理多线程程序中的资源竞争,防止资源争夺导致的冲突。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

图像增强——传统算法伽马校正实现暗光增强(附Python代码)

&#x1f4aa; 专业从事且热爱图像处理&#xff0c;图像处理专栏更新如下&#x1f447;&#xff1a; &#x1f4dd;《图像去噪》 &#x1f4dd;《超分辨率重建》 &#x1f4dd;《语义分割》 &#x1f4dd;《风格迁移》 &#x1f4dd;《目标检测》 &#x1f4dd;《图像增强》 &a…

OpenSearch迁移方案

一、背景 因业务需要迁移Opensearch 集群&#xff0c;当前集群数据量高达21TB&#xff0c;采用常规工具进行迁移估计不可取&#xff0c;需要使用对象存储做中转&#xff0c;进行OpenSearch数据迁移。 二、OpenSearch迁移方案 前期进行OpenSearch数据迁移调研 序号方案诠释备…

java项目之科研工作量管理系统的设计与实现源码(springboot+vue+mysql)

风定落花生&#xff0c;歌声逐流水&#xff0c;大家好我是风歌&#xff0c;混迹在java圈的辛苦码农。今天要和大家聊的是一款基于springboot的科研工作量管理系统的设计与实现。项目源码以及部署相关请联系风歌&#xff0c;文末附上联系信息 。 项目简介&#xff1a; 科研工作…

介绍Java

Java简介 Java是一门由Sun公司&#xff08;现被Oracle收购&#xff09;在1995年开发的计算机编程语言&#xff0c;其主力开发人员是James Gosling&#xff0c;被称为Java之父。Java在被命名为“Java”之前&#xff0c;实际上叫做Oak&#xff0c;这个名字源于James Gosling望向…

Basic Pentesting_ 2靶机渗透

项目地址 plain https://download.vulnhub.com/basicpentesting/basic_pentesting_2.tar.gz 修改静态ip 开机按e 输入rw signie init/bin/bash ctrlx 进入编辑这个文件 vi /etc/network/interfaces修改网卡为ens33 保存退出 实验过程 开启靶机虚拟机 ![](https://img-bl…

paimon,基础查询语句测试

基础设置 -- 创建catalog/加载catalog&#xff0c;如果这个catalog已经存在就不会创建&#xff0c;自动加载元数据信息CREATE CATALOG fs_paimon_catalog WITH ( type paimon, warehouse hdfs://wsl01:8020/paimon/catalog ); -- 使用catalog use catalog fs_paimon_catalog…

Java中二维数组-杨辉三角

使用二维数组打印一个10行杨辉三角 1 1 1 1 2 1 1 3 3 1 1 4 6 4 1 1 5 10 10 5 1 1&#xff09;第一行有1个元素&#xff0c;第n行有n个元素 2&#xff09;每一行的第一个元素和最后一个元素都是1 3&#xff09;从第三行开始&#xff0c;对于非第一个元素和最后一个元素的元素…

差分注意力,负注意力的引入

文章目录 Differential Transformer差分注意力&#xff0c;负注意力的引入相关链接介绍初始化函数多头差分注意力 Differential Transformer差分注意力&#xff0c;负注意力的引入 相关链接 ai-algorithms/README.md at main Jaykef/ai-algorithms (github.com) unilm/Diff…

response和验证码、文件下载操作

目录 Response对象 案例&#xff1a; 1、完成重定向 2、服务器输出字符输出流到浏览器 3、服务器输出字节输出流到浏览器 4、验证码 ServletContext对象 Response对象 功能&#xff1a;设置响应消息 1、设置响应行 格式&#xff1a;HTTP/1.1 200 ok 设置状态码 se…

RabbitMQ 高级特性——死信队列

文章目录 前言死信队列什么是死信常见面试题死信队列的概念&#xff1a;死信的来源&#xff08;造成死信的原因有哪些&#xff09;死信队列的应用场景 前言 前面我们学习了为消息和队列设置 TTL 过期时间&#xff0c;这样可以保证消息的积压&#xff0c;那么对于这些过期了的消…

【更新】上市公司企业机构投资者实地调研数据(2013-2023年)

一、测算方式&#xff1a; 参考《会计研究》逯东&#xff08;2019&#xff09;老师的做法&#xff0c;考虑投资者实地调研的频率和可能性&#xff0c;设立了下述变量来衡量上市公司接待投资者调研情况: 首先&#xff0c;使用年度范围内接待投资者调研的总次数 ( Visitnmb) 作为…

卸载PLSQL及标准卸载流程

目录 1. 卸载PLSQL2. 删除注册表3. 删除数据信息 1. 卸载PLSQL 等待进度条走完 2. 删除注册表 regedit 右击删除 3. 删除数据信息 由于AppData是隐藏文件&#xff0c;需要勾选隐藏的项目。 重启电脑&#xff0c;PLSQL就卸载成功了。

低代码工单管理app评测,功能与效率解析

预计到2030年&#xff0c;低代码平台市场将达1870亿美元。ZohoCreator助力企业构建定制化软件应用&#xff0c;以建筑行业工作订单管理app为例&#xff0c;简化流程&#xff0c;提升管理效率&#xff0c;降低成本。其用户友好界面、自动化管理、跨平台使用及全面报告功能受企业…

项目优化内容及实战

文章目录 事前思考Prometheus 普罗米修斯概述架构安装及使用 Grafana可视化数据库读写分离实战1-PrometheusGrafanaspringboot 事前思考 需要了解清楚&#xff1a;需要从哪些角度去分析实现&#xff1f;使用了缓存&#xff0c;就需要把缓存命中率数据进行收集&#xff1b;使用…

企业在隔离网环境下如何进行安全又稳定的跨网文件交换?

在数字化时代&#xff0c;企业的数据流通如同血液一般重要。然而&#xff0c;当企业内部实施了隔离网环境&#xff0c;跨网文件交换就成了一个棘手的问题。今天我们将探讨在隔离网环境下&#xff0c;企业面临的跨网文件交换挑战&#xff0c;以及如何通过合规的跨网文件交换系统…

数字电路——触发器1(RS和钟控触发器)

触发器&#xff1a;能够存储一位二进制信息的基本单元电路称触发器(Flip-Flop) 特点&#xff1a; 具有两个能自行保持的稳定状态&#xff0c;用来表示逻辑状态的“0”或“1”。具有一对互补输出。有一组控制(激励、驱动)输入。或许有定时(时钟)端CP(Clock Pulse)。在输入信号…

PostgreSQL 16.4安装以及集群部署

1. 环境准备 1.1 主机环境 主机 IP: 192.24.215.121操作系统: CentOS 9PostgreSQL 版本: 16.4 1.2 从机环境 从机 IP: 192.24.215.122操作系统: CentOS 9PostgreSQL 版本: 16.4 2. 安装 PostgreSQL 16.4 在主从两台机器上都需要安装 PostgreSQL 16.4。 2.1 添加 Postgre…

银行卡基础信息查询 API 对接说明

本文将介绍一种 银行卡基础信息查询 API 对接说明&#xff0c;它可用于银行卡基础信息查询。 接下来介绍下 银行卡基础信息查询 API 的对接说明。 申请流程 要使用 API&#xff0c;需要先到 银行卡基础信息查询 API 对应页面申请对应的服务&#xff0c;进入页面之后&#xf…

Python自定义异常类:实际应用示例之最佳实践

Python自定义异常类&#xff1a;实际应用示例之最佳实践 前言 在软件开发中&#xff0c;合理处理异常是保证程序稳定性的重要环节。虽然 Python 内置了丰富的异常类型&#xff0c;但在处理复杂业务逻辑时&#xff0c;自定义异常类能够使代码更加清晰且具备可扩展性。 本文将…

一个架构师的职业素养:四种常用的权限模型

你好,我是看山。 本文收录在《一个架构师的职业素养》专栏。日拱一卒,功不唐捐。 今天咱们一起聊聊权限系统。 以大家熟知的电商场景举例: 用户可以分为普通用户、VIP用户:我们需要控制不同角色用户的访问范围。比如,京东的PLUS会员,可以进入会员专区,而且能够使用礼金…