Linux下的多线程编程和线程同步

本文详细介绍了Linux中的多线程概念,包括进程与线程的区别,资源分配,线程创建与回收,以及线程同步方法(互斥锁、读写锁、条件变量和信号量)。特别关注了如何避免死锁和管理线程资源,以及生产者-消费者模型的应用。
摘要由CSDN通过智能技术生成

Linux多线程

进程与线程的区别

  • 进程有自己独立的地址空间,多个线程公用一个地址空间

    • 线程更加节省系统资源,效率不仅可以保持而且能够更高
    • 在一个地址空间中多个线程独享:每个线程都有属于自己的栈区、寄存器(内核中管理的)
    • 在一个地址空间中多个线程共享:代码段、堆区、全局数据区、打开的文件(文件描述符表)都是线程共享的
  • 线程是程序的最小执行单位,进程是操作系统中最小的资源分配单位

    • 每个进程对应一个虚拟地址空间,一个进程只能抢一个cpu时间片
    • 一个地址空间中可以划分出多个线程,在有效的资源基础上,能够抢更多的cpu时间片
  • cpu的调度和切换:线程的上下文切换比进程要快得多

    (上下文切换:进程/线程分时复用cpu时间片,在切换之前会将上一个任务的状态进行保存,下次切换回这个任务的时候,加载这个状态继续运行,任务从保存到再次加载这个过程就是一次上下文切换)

  • 线程更加廉价,启动速度更快,退出也快,对系统资源的冲击小

控制线程的个数

在处理多任务程序的时候使用多线程比使用多进程要更有优势,但是线程并不是越多越好:

  1. 文件IO操作:文件IO对cpu使用率不高,因此可以分时复用cpu时间片,线程个数 = 2 * cpu核心数
  2. 处理复杂的算法(主要是cpu进行运算、压力大),线程个数 = cpu的核心数

创建线程函数

#include <pthread.h>
int pthread_create(pthread_t* thread, const pthread_attr_t* attr, void *(*start_routine)(void*), void* arg);

thread:传出参数,unsigned long,线程创建成功,会将线程id写入到这个指针指向的内存中

attr:线程的属性,一般情况下使用默认属性即可,写NULL

start_routine:函数指针,创建出的子线程的处理动作,回调函数

arg:作为参数传递到回调函数中

返回值:创建成功返回0,失败返回对应错误号

线程退出函数

使用线程退出函数来让主线程退出、子线程继续运行

void pthread_exit(void* retval);

参数:线程退出时携带的数据,当前子线程的主线程会得到该数据,如果不需要使用,指定为NULL

线程回收

子线程退出时其内核资源主要由主线程回收(用户资源自动释放),回收函数是pthread_join(),这个函数是阻塞函数,如果还有子线程在运行,调用该函数就会阻塞,子线程退出函数解除阻塞进行资源回收,函数被调用一次,只能回收一个子线程,如果有多个子线程则需要循环进行回收。

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

thread:要被回收的子线程的线程id。

retval:二级指针,指向一级指针的地址,这个地址中存储了pthread_exit()传递出的数据,如果不需要可以指定为NULL。

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

线程分离

某些情况下,程序中的主线程有属于自己的业务处理流程,如果让主线程负责子线程的资源回收,调用pthread_join()只要子线程不退出主线程就会一直被阻塞,主要线程的任务也就不能被执行了。

可以使用线程分离函数pthread_detach(),调用后指定的子线程就可以和主线程分离,当子线程退出的时候,其占用的内核资源就被系统其他进程接管回收了,线程分离之后再主线程中使用pthread_join()就回收不到子线程资源了。

int pthread_detach(pthread_t thread);  // thread:子线程的线程id

线程取消

在某些特定情况下(当要被杀死的线程进行了一次系统调用(直接或间接),就被杀死了)在一个线程中杀死另一个线程。

int pthread_cancel(pthread_t thread);

线程同步

概念

当多个线程中的一个线程对内存中的共享资源访问时,其他线程都不可以对这块内存进行操作,需要阻塞等待,直到该线程对这块内存访问完毕,线程堆内存的这种访问方式称之为线程同步。

常用方式有四种:互斥锁、读写锁、条件变量、信号量

互斥锁

Linux中互斥锁类型为pthread_mutex_t

// 初始化互斥锁
int pthread_mutex_init(pthread_mutex_t* mutex, const pthread_mutexattr_t attr);
// 释放互斥锁资源
int pthread_mutex_destroy(pthread_mutex_t* mutex);
  • mutex:互斥锁变量的地址
  • attr:互斥锁的属性,一般使用默认属性即可,这个参数指定为NULL
// 修改互斥锁状态,将其设定为锁定状态,这个状态被写入到参数mutex中
int pthread_mutex_lock(pthread_mutex_t* mutex);

// 尝试加锁
int pthread_mutex_trylock(pthread_mutex_t* mutex);
  • pthread_mutex_lock这个函数被调用,首先会判断参数mutex互斥锁中的状态是不是锁定状态:如果没有被锁定,这个线程可以加锁成功;如果被锁定,其他线程加锁就失败了,这些线程会阻塞在这把锁上。
  • pthread_mutex_trylock函数被调用,如果没有被锁定则加锁成功;如果被锁定,线程不会被阻塞,直接返回错误号。
// 对互斥锁解锁
int pthread_mutex_unlock(pthread_mutex_t* mutex);

如何避免死锁:

  1. 避免多次锁定,多检查
  2. 对共享资源访问完毕后,一定要解锁,或者在加锁时使用trylock
  3. 如果程序中有多把锁,可以控制对锁的访问顺序
  4. 项目程序中可以引入一些专门用于死锁检测的模块

读写锁

是互斥锁的升级版,读写锁是一把锁,类型是pthread_rwlock_t

特定:

  1. 使用读写锁的读锁锁定了临界区,线程对临界区的访问是并行的,读锁是共享的
  2. 使用读写锁的写锁锁定了临界区,线程对临界区的访问时串行的,写锁时独占的
  3. 写锁比读锁的优先级高
  4. 不能对同一把锁同时进行读锁定和写锁定

如果对临界资源操作时涉及大量的读操作,使用读写锁效率高;如果只有少量读操作,读写锁与互斥锁效率差不多

// 初始化读写锁
int pthread_rwlock_init(pthread_rwlock_t* restrict rwlock, const pthread_rwlockattr_t* restrict attr);
// 释放读写锁占用的系统资源
int pthread_rwlock_destory(pthread_rwlock_t* rwlock);
// 加读锁,锁定的是读操作
int pthread_rwlock_rdlock(pthread_rwlock_t* rwlock);
int pthread_rwlock_tryrdlock(pthread_rwlock_t* rwlock);
// 加写锁,锁定的是写操作
int pthread_rwlock_wrlock(pthread_rwlock_t* rwlock);
int pthread_rwlock_trywrlock(pthread_rwlock_t* rwlock);
// 解锁,不管锁定的是什么锁
int pthread_rwlock_unlock(pthread_rwlock_t* rwlock);

条件变量

条件变量主要作用并不是处理线程同步,而是进行线程的阻塞,如果在多线程程序中只是用条件变量无法实现线程同步,必须要配合互斥锁来使用。主要用来处理生产者-消费者模型。

条件变量对应的类型为pthread_cond_t,被条件变量阻塞的线程的现场信息会被记录到这个变量中,以便在解除阻塞的时候使用

// 初始化
int pthread_cond_init(pthread_cond_t* cond, pthread_condattr_t* attr);
// 销毁释放资源
int pthread_cond_destroy(pthread_cond_t* cond);
  • cond:条件变量的地址
  • attr:条件变量属性,一般使用默认属性NULL
// 线程阻塞函数,哪个线程调用这个函数,哪个线程就会被阻塞
int pthread_cond_wait(pthread_cond_t* cond, pthread_mutex_t* mutex);
// 将线程阻塞一定时间长度,时间到达之后线程就解除阻塞了
int pthread_cond_timedwait(pthread_cond_t* cond, pthread_mutex_t* mutex, struct timespec* abstime);

通过pthread_cond_wait原型可以看出,该函数在阻塞线程的时候,需要一个互斥锁参数,互斥锁主要功能是进行线程同步,让线程顺序进入临界区。该函数会对这个互斥锁做以下几件事情:

  1. 在阻塞函数的时候,如果线程已经对互斥锁mutex上锁,那么会将这把锁打开,来避免死锁
  2. 当线程解除阻塞的时候,函数内部会帮助这个线程再次将这个mutex互斥锁锁上,继续向下访问临界区
// 唤醒阻塞在条件变量上的线程,至少有一个被解除阻塞
int pthread_cond_signal(pthread_cond_t* cond);
// 唤醒阻塞在条件变量上的线程,被阻塞的线程会全部解除阻塞
int pthread_cond_broadcast(pthread_cond_t* cond);

信号量

信号量和条件变量一样用于处理生产者和消费者模型,用于阻塞生产者或消费者线程的运行,信号的类型为sem_t,头文件为<semaphore.h>

// 初始化信号量
int sem_init(sem_t* sem, int pshared, unsigned int value);
// 资源释放,线程销毁之后调用这个函数
int sem_destory(sem_t* sem);
  • sem:信号变量地址
  • pshared:
    • 0:线程同步
    • 非0:进程同步
  • value:初始化当前信号量拥有的资源数(>=0)
// 函数被调用,sem中的资源-1,当sem中资源数减为0时,线程也就被阻塞了
int sem_wait(sem_t* sem);
// sem中资源耗尽时,线程不会被阻塞,而是返回错误号
int sem_trywait(sem_t* sem);
// 调用该函数,sem中的资源+1,
int sem_post(sem_t* sem);
// 查看信号量sem中资源数量
int sem_getvalue(sem_t* sem, int* sval);
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值