多线程编程

142 阅读4分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

个人博客:www.cgsx.online/

多线程编程

线程

线程是比进程更小的单位,是完成一个独立任务的完整执行序列,是一个可调度的实体。

线程分为内核线程和用户线程:

  • 内核线程运行在内核空间,由内核调度。
  • 用户线程运行在用户空间,由线程库调度。

未命名文件 (36).png

线程有三种实现方式:

  1. 完全在用户空间实现

    即多线程只在用户空间实现,由线程库管理,内核只看得见一个进程,看不见用户空间的多个线程。这些执行线程共享该进程的时间片。

未命名文件 (37).png

**优点**就是创建和调度进程不需要内核的干预,速度非常快,且不占用额外的内存资源,但**缺点**是对于多处理系统,多个进程无法运行在不同的CPU上。

0. 完全由内核调度

未命名文件 (38).png

与完全在用户空间实现相反,调度任务交给内核,运行在用户空间的线程库无须管理任务。

0. 双层调度

未命名文件 (39).png

前两种方式的混合体,结合了前两种方式的优点:1)不会消耗过多的内核资源;2)切换速度快;3)充分利用多处理器的优势。

为什么要使用线程?

多进程模型的缺陷:

  1. 创建多进程带来更大的开销
  2. 数据交换需要IPC技术
  3. 上下文切换耗时

多线程模型优点:

  1. 上下文切换不需要切换数据区和堆区
  2. 利用数据区和堆区交换数据

一个进程有数据区,堆区和栈区,一个进程可以包含多个线程,其中多个线程共享数据区,堆区。

未命名文件 (31).png

线程创建与运行

线程具有单独的执行流,需要单独定义线程的main函数,还要请求操作系统在单独的执行流中执行该函数。

#include <pthread.h>int pthread_create(
    pthread_t* restrict thread, const pthread_attr_t* restrict attr, 
    void* (*strat_routine)(void*), void* restrict arg
);
  • thread 保存新创建线程ID的变量地址值
  • attr 传递线程属性的参数,传递NULL表示默认属性
  • start_routine 线程main函数的函数指针,执行的函数地址值
  • arg 调用函数的传递参数变量地址值

使用线程时需要注意执行流,当进程终止时,线程没有执行完也会一起终止。

未命名文件 (32).png

所以,当线程在执行时,一定要保证进程不能在线程执行完毕前终止。

我们可以使用pthread_join,使得调用该函数的进程进入等待状态,等待线程执行完毕。

#include<pthread.h>int pthread_join(pthread_t thread, void** status);
  • thread 参数ID的线程终止后,从该函数返回
  • status 线程的main函数返回值的指针变量地址值

未命名文件 (33).png

线程同步问题

多个线程访问同一变量,需要进行同步,使得最后的结果正确。

同步主要解决两个问题:

  • 同时访问同一内存空间时发生的情况。
  • 需要指定访问同一内存空间的线程执行顺序的情况。

互斥量

互斥量是一种锁机制,为了保护临界区。

#include <pthread.h>int pthread_mutex_init(pthread_mutex_t * mutex, const pthread_mutexatr_t * attr);
int pthread_mutex_destroy(pthread_mutex_t* mutex);
/**
* mutex 保存互斥量的变量地址值
* attr  即将创建的互斥量的属性
**/

创建互斥量后可以通过开锁解锁操作对临界区进行保护,加锁后只允许有一个线程对其进行操作。

#include <pthread.h>int pthread_mutex_lock(pthread_mutex_t* mutex);
int pthread_mutex_trylock(pthread_mutex_t* mutex);
int pthread_mutex_unlock(pthread_mutex_t* mutex);

未命名文件 (35).png

信号量

信号量与互斥量的作用一样,但不同的是,互斥量只有有锁与没锁两种状态,即0和1,和信号量是一个整数值,可以设置竞争资源的多少。

#include <semaphore.h>int sem_init(sem_t* sem, int pshared, unsigned int value);
int sem_destroy(sem_t* sem);
/**
* sem 信号量变量地址
* pshared 信号量属性,0为同一进程内部使用的信号量
* value 信号量初始值
**/

创建信号量后,通过两个函数对信号量进行增1减1

#include <semaphore.h>int sem_post(sem_t *sem);  // +1
int sem_wait(sem_t *sem);  // -1
int sem_trywait(sem_t *sem); // -1,非阻塞版本

条件变量

条件变量提供了一种线程间的通知机制,当某个共享数据达到某个值的时候,唤醒等待这个共享数据的线程。

相关函数如下

#include <pthread.h>int pthread_cond_init(pthread_cond_t* cond, const pthread_condattr_t* cond_attr);  // 创建
int pthread_con_destroy(pthread_cond_t* cond);  // 销毁
int pthread_cond_broadcast(pthread_cond_t* cond);  // 唤醒所有等待目标条件变量的线程
int pthread_cond_signal(pthread_con_t* cond);  // 唤醒一个等待目标条件变量的线程
int pthread_cond_wait(pthread_cond_t* cond, pthread_mutex_t* mutex);  // 等待目标条件
/**
* cond 指向要操作的目标变量
* cond_attr 指定条件变量的属性
* mutex 互斥锁,保证pthread_cond_wait的原子性
**/

调用wait,会先把线程放入条件变量的等待队列,然后将互斥锁mutex解锁。要加锁的目的是防止在wait过程中signal与broadcast会修改条件变量,导致线程一直被挂起。

未命名文件 (40).png