【Linux】多线程基础

线程

  • 线程是轻量级的进程,引入线程前进程是操作系统进行资源分配和调度的基本单位,引入线程后线程称为调度的基本单位,线程不拥有资源共享进程的资源。
  • 父子进程的虚拟地址空间不同,要对父进程的虚拟地址空间的内容进行大量的复制操作,但是线程是直接共享当前进程的虚拟地址空间不会创建新的虚拟地址空间
  • 线程间通信更快速方便,只需要将数据复制到共享(堆、全局)变量中即可交互
  • 线程共享的资源包括进程ID、父进程ID、进程组ID、会话ID、用户ID、文件描述符表、信号处置、虚拟地址空间(除栈和.text文件)
  • 线程非共享资源包括线程ID、信号掩码、线程特有数据、栈以及本地变量等。
  • 使用-l pthread使用线程库pthread才能使用线程相关函数

线程操作函数

创建线程
//主线程创建子线程
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void *), void *arg); 
//成功时返回0。失败时返回非零值。
//thread是一个输出参数,用于存储新创建线程的标识符(线程ID)。
//attr为指向pthread_attr_t结构的指针,用于指定线程的属性,如栈大小、调度策略等。如果为NULL,则使用默认属性。
//start_routine是函数指针,指向新线程开始执行时调用的函数即线程的入口函数
//arg是传递给线程函数的参数。

//具体使用

// 线程的入口函数
void *print_message(void *ptr) {
    char *message;
    message = (char *) ptr; // 将void指针转换为char指针
    printf("%s \n", message);
    pthread_exit(NULL);//退出线程
}

int main(){
    pthread_t tid;//创建存储线程ID的tid
    const char *message = "Hello, World!"; // 传给线程入口函数的参数

    // 创建新线程
    if (pthread_create(&tid, NULL,print_message, (void*) message) != 0) {
        perror("Failed to create thread");
        return 1;
    }
    return 0;
}
退出线程
void pthread_exit(void *retval); 
//使当前线程退出,retval为可选地返回一个退出状态,可以通过pthread_join函数获取这个退出状态,为NULL表示退出状态是不确定的。
//一旦线程调用了pthread_exit,它就不能再恢复执行。
//如果线程是进程中的最后一个可 join 线程(即所有其他线程都已经通过pthread_exit退出并且被join了),并且主线程已经调用了pthread_exit或者main函数返回,那么整个进程将会终止。

//具体使用

pthread_exit(NULL);//默认退出线程
pthread_exit((void*)0x123); //返回一个整数值作为退出状态
//pthread_join获取状态存在status中
 if (pthread_join(tid, &status) != 0) {
        perror("pthread_join");
        return 1;
    }

    printf("status code: %p\n", status);
阻塞回收调用线程
int pthread_join(pthread_t thread, void **retval);
//成功时返回0。如果指定的线程尚未终止,或者线程ID不合法,或者调用线程被信号中断,函数将返回非零值。 
//阻塞回收当前线程,直到指定的线程结束。可选地接收该线程的返回值。
//thread: 要等待的线程的标识符,即之前通过pthread_create创建线程时返回的pthread_t类型的值
//retval: 这是一个输出参数,用于接收被等待线程的返回值(通过pthread_exit传递的void*指针)。如果对返回值不感兴趣,可以传入NULL。
分离线程
int pthread_detach(pthread_t thread); 
//成功时返回0。如果线程ID无效或者调用出现其他错误,函数将返回非零值。
//将线程设置为分离状态,意味着线程结束时自动释放资源,不需要其他线程调用pthread_join。
//thread: 要分离的线程的标识符,即通过pthread_create创建线程时返回的线程ID。
获取当前线程ID
pthread_t pthread_self(void); 返回调用该函数的线程的线程ID。
比较线程
int pthread_equal(pthread_t t1, pthread_t t2);
//如果t1和t2代表同一个线程,则返回非零值(通常为1,表示真)。
如果它们代表不同的线程,则返回0(表示假)

线程同步

  • 线程虽然能通过全局变量来共享信息,但是需要确保多个线程不会修改同一变量。
  • 同一时刻只能被一个线程所访问的资源称为临界资源,访问临界资源的代码即为临界区,应该执行原子操作。
使用互斥量、互斥锁实现同步
//初始化pthread_mutex_t互斥量
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr); 
//初始化互斥量mutex,可选参数attr用于设置互斥量属性。
//成功执行时返回0,出错时返回非零的错误码。
//mutex:指向需要初始化的互斥量的指针。这个变量应该已经被声明并且分配了内存空间。
//attr:一个指向pthread_mutexattr_t结构体的指针,用于设置互斥量的属性。

//加锁
int pthread_mutex_lock(pthread_mutex_t *mutex); 
//阻塞当前线程直到获得互斥量mutex的所有权。
//成功执行时返回0,出错时返回非零的错误码。
//mutex:指向要锁定的互斥量的指针。这个互斥量必须已经通过pthread_mutex_init函数初始化过。

//尝试加锁
int pthread_mutex_trylock(pthread_mutex_t *mutex); 
//尝试立即获取互斥量mutex,如果无法获取(已被其他线程持有),则立即返回失败,而不阻塞。
//成功获取到锁时返回0,如果互斥量已经被锁定,则返回EBUSY,其他错误情况下返回其他非零错误码。

//解锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);
//释放互斥量mutex的所有权。
//成功释放锁时返回0,如果发生错误则返回一个非零的错误码。

//销毁
int pthread_mutex_destroy(pthread_mutex_t *mutex); 
//销毁不再使用的互斥量。
//成功销毁互斥量时返回0,如果发生错误,比如尝试销毁一个已被锁定的互斥量,将返回一个非零的错误码。

//具体使用

int books=1000;//全局共享变量
pthread_mutex_t mutex;//创建共享互斥量
...
void *sellbook(void *str){
while(1){
    pthread_mutex_lock(&mutex);//加锁
    if(books>0){
        usleep(6000);
        printf("%ld 线程正在卖第 %d 本书\n",pthread_self(),books);
        books--;
    }else{
        //卖完直接解锁退出
        pthread_mutex_unlock(&mutex);
        break;
    }
    //没卖完解锁,其他线程可卖
    pthread_mutex_unlock(&mutex);
}
}

int main(){
    pthread_mutex_init(&mutex,NULL);//初始化互斥量

    //创建子线程
    pthread_t tid1,tid2,tid3;
    pthread_create(&tid1,NULL,sellbook,NULL);
    pthread_create(&tid2,NULL,sellbook,NULL);
    pthread_create(&tid3,NULL,sellbook,NULL);

    //阻塞回收子线程资源,等待结束
    pthread_join(tid1,NULL);
    pthread_join(tid2,NULL);
    pthread_join(tid3,NULL);
    /*
    也可使用线程分离
    pthread_detach(tid1);
    pthread_detach(tid2);
    pthread_detach(tid3);
    */

    pthread_exit(NULL);//退出主线程

    //释放互斥量资源
    pthread_mutex_destroy(&mutex);

    return 0;
    
}
...

条件变量
  • 条件变量是线程同步的另一种重要机制,它允许线程在满足特定条件之前挂起(等待),并在条件满足时被唤醒。条件变量常常与互斥锁一起使用,以实现更复杂的线程间同步模式,比如生产者-消费者模型。
//初始化条件变量
int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);
//成功时返回0。如果发生错误(例如,内存不足),则返回一个非零的错误码
//cond: 指向要初始化的条件变量的指针。
//attr: 一个指向条件变量属性对象的指针,如果attr为NULL,则使用默认属性来初始化条件变量。

//等待条件变量
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
//cond:指向要等待的条件变量的指针。
//mutex:指向与条件变量配合使用的、当前已被锁定的互斥锁的指针。调用pthread_cond_wait前,这个锁必须被当前线程持有。
//阻塞时会自动解锁,避免死锁产生,唤醒后会自动加锁

//唤醒单个等待线程
int pthread_cond_signal(pthread_cond_t *cond);
//cond:指向需要被用来唤醒线程的条件变量的指针

//唤醒所有等待线程
int pthread_cond_broadcast(pthread_cond_t *cond);
//cond:指向需要被用来唤醒线程的条件变量的指针

//销毁条件变量
int pthread_cond_destroy(pthread_cond_t *cond);
//cond:指向需要被用来唤醒线程的条件变量的指针
信号量semaphore
  • 信号量代表资源的数量,信号量初始值为1时可以作互斥量使用,信号量中还包含一个阻塞队列,用于P/V操作的阻塞和唤醒。

//初始化信号量sem_t
int sem_init(sem_t *sem, int pshared, unsigned int value);
//sem:指向要初始化的信号量的指针。
//pshared:如果为0,则信号量只能在同一进程中使用;非0值表示可以在不同进程间共享(需要额外的同步措施)。
//value:信号量的初始值。

//等待信号量(P操作)
int sem_wait(sem_t *sem);
//该函数将信号量的值减1。如果结果小于0,则调用线程会被阻塞,直到信号量的值大于等于0为止。

//发信号量(V操作)
int sem_post(sem_t *sem);
//该函数将信号量的值加1。如果加1后信号量的值仍然小于等于0,则说明有阻塞,可能会唤醒一个或多个等待该信号量的线程。

//获取信号量的值
int sem_getvalue(sem_t *sem, int *sval);
//sem:要查询的信号量。
//sval:返回信号量的当前值的地址。

//销毁信号量
int sem_destroy(sem_t *sem);
//释放与信号量关联的资源。注意,一旦销毁,信号量不能再使用。

//具体使用

pthread_mutex_t mutex;//互斥量
sem_t psem;//生产者同步信号量,空缓冲区个数
sem_t csem;//消费者同步信号量,满缓冲区个数

....
//生产者
void * producer(void * arg){
    while(1){
        sem_wait(&psem);//使用空缓冲区,无则阻塞
        pthread_mutex_lock(&mutex);

        //生产
        
        pthread_mutex_unlock(&mutex);
        sem_post(&csem);//提供满缓冲区,唤醒阻塞消费者
    }
}

//消费者
void * consumer(void * arg){
    while(1){
        sem_wait(&csem);//使用满缓冲区,无则阻塞
        pthread_mutex_lock(&mutex);

        //消费
        
        pthread_mutex_unlock(&mutex);
        sem_post(&psem);//提供空缓冲区,唤醒阻塞生产者
    }
}

int main(){

    pthread_mutex_init(&mutex,NULL);//初始化互斥锁
    sem_init(&psem,0,5);//初始化生产者信号量,空缓冲区为5
    sem_init(&csem,0,0);//初始化消费者信号量,满缓冲区为0

    //创建生产者、消费者线程
    pthread_t ptid[5],ctid[5];
    for(int i=0;i<5;i++)
    {
        pthread_create(&ptid[i],NULL,producer,NULL);
        pthread_create(&ctid[i],NULL,consumer,NULL);
    }

    //阻塞回收子线程资源,等待结束
    for(int i=0;i<5;i++)
    {
        pthread_join(ptid[i],NULL);
        pthread_join(ctid[i],NULL);
    /*
    也可使用线程分离
    pthread_detach(ptid[i]);
    pthread_detach(ctid[i]);
    */
    }

    while(1){
        sleep(10);
    }

    //释放信号量
    sem_destroy(&psem);
    sem_destroy(&csem);

    //释放互斥量资源
    pthread_mutex_destroy(&mutex);

    pthread_exit(NULL);//退出主线程

    return 0;

}

读写锁
  • 读写锁(Read-Write Lock)是多线程编程中的一种同步机制,它允许多个读取者同时访问共享资源,但在任何时候最多只允许一个写入者。这种锁机制提高了并发性能,特别适合读多写少的场景。读写锁分为两种状态:读模式和写模式,它有两个基本操作:读取锁定和写入锁定。
  • 读取锁定(共享锁)
    当一个线程获取读取锁时,其他试图获取读取锁的线程也可以成功获取,即多个读取者可以同时访问资源。
    但如果此时有线程正在等待获取写入锁,则所有新的读取请求也会被阻塞,直到写入锁释放。
  • 写入锁定(独占锁)
    当一个线程获取写入锁时,任何其他试图获取读取锁或写入锁的线程都会被阻塞,直到写入锁被释放。写入锁保证了在写操作过程中不会有其他读取者或写入者干扰,确保了数据的一致性。
  • 读写锁也可以通过互斥锁和信号量来实现。需要维护一个写优先的互斥锁以及一个访问共享资源的互斥锁以及表示读者的数量的变量和访问读者数量的互斥锁,只有第一个读者可以进行上锁/P操作,只有最后一个读者可以进行释放锁/V操作。
    pthread_rwlock_rdlock(&rwlock); // 获取读取锁
    // 执行读取操作
    pthread_rwlock_unlock(&rwlock); // 释放读取锁

    pthread_rwlock_wrlock(&rwlock); // 获取写入锁
    // 执行写入操作
    pthread_rwlock_unlock(&rwlock); // 释放写入锁

    pthread_rwlock_init(&rwlock, NULL); // 初始化读写锁
    // 创建线程执行读写操作...
    pthread_rwlock_destroy(&rwlock); // 销毁读写锁
  • 4
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值