【C++】Linux多线程开发学习笔记


在Linux内核的眼中,线程就是一个轻量级的进程。
进程是CPU分配资源的最小单位,线程是操作系统调度执行的最小单位。
如果直接是c语言写线程的话,需要对线程这个扩展库进行链接,即编译的时候加上 -lpthread

1 查看某个进程的线程号

ps -LF pidid(要查看的进程号)

2 创建线程

一般情况下,main函数所在的线程称为主线程,其余创建的线程称为子线程。
函数功能就是创建一个子线程。

#include <pthread.h>
int pthread_create(pthread_t *thread,const pthread_attr_t *attr,void *(*start_routine) (void *),void *arg);
// 返回值:0为成功;若失败,则返回错误号,并且thread将不被指定
  • thread 传出参数(看到带指针就应该明白),线程创建成功后,子线程的线程ID被写到该变量中。
  • attr表示线程参数,用来设置线程属性。一般使用默认值NULL
  • start_routine为一个函数指针。这个函数是子线程需要处理的逻辑代码。入口函数的返回值必须为void*,且入口函数必须为static(该函数,要求为静态函数。如果处理线程函数为类成员函数时,需要将其设置为静态成员函数。)。
  • arg:给三个参数传参使用的。arg指向一个全局或者堆变量,也可以设置为Null,如果需要传递多个参数,可以将arg指向一个结构。
    【如何获取错误号的信息】
    char * strerror(int errnum);
    【执行子线程的细节】 如果仅仅是创建一个子进程,并且万一子线程还没执行代码呢,主线程结束了,那么子线程也会跟着结束。

3 终止线程

函数功能:终止一个线程,在哪个线程中调用,就表示终止哪个线程。

#include <pthread.h>
void pthread_exit(void *retval);
//没有返回值
  • retval:需要传递一个指针,作为一个返回值,可以通过调用pthread_join()来获取。(就和exit括号中的返回值似的,注意返回的retval不要是局部变量)

调用pthread_exit()相当于在新线程函数start()中执行return,不同之处在于:

  • Linux主线程里使用pthread_exit(val)结束时,只会使主线程结束,而由主线程创建的子线程并不会因此结束,他们继续执行。
  • Linux主线程使用return结束时,那么子线程也就结束了。

4 获得当前线程的线程ID

pthread_t pthread_self(void);
//哪个线程调用,就返回哪个线程的ID

5 连接已终止的线程

该函数的主要用途为:以阻塞的方式等待由thread标识的线程终止,并且调用一次,只能回收一个子线程。

代码中如果没有pthread_join 主线程会很快结束从而使整个进程结束,从而使创建的线程没有机会开始执行就结束了。加入pthread_join后,主线程会一直等待直到等待的线程结束自己才结束,使创建的线程有机会执行。
另外,将好像进程里的wait(),如果子线程没人管,就会变成僵尸线程,因此为了防止那种情况,也需要用到join来回收子线程的资源。joinable(即上面提到的非可分离线程)的线程必须用pthread_join()函数来释放线程所占用的资源,如果没有执行这个函数,那么线程的资源永远得不到释放。

#include <pthread.h>
int pthread_join(pthread_t thread,void **retval);
//返回值:返回0表示成功,返回错误号
  • 第一个参数:需要回收的子线程的id。
  • 第二个参数:接收子线程退出时的返回值(pthread_exit返回的值)。该线程结束时的返回值。这里之所以用双指针,是因为正常应为void *retval,但是为了能够形成引用(即内部修改了,然后能够返回出来),所以需要再加一层指针,因此是双指针。

6 分离线程

该函数的主要用途为:将某个线程分离。

线程的分离状态决定一个线程以什么样的方式来终止自己。线程的默认属性是非分离状态,这种情况下,原有的线程等待创建的线程结束。只有当pthread_join()函数返回时,创建的线程才算终止,才能释放自己占用的系统资源。而分离线程不是这样子的,它没有被其他的线程所等待,自己运行结束了,线程也就终止了,马上释放系统资源。程序员应该根据自己的需要,选择适当的分离状态。

#include <pthread.h>
int pthread_detach(pthread_t thread);
//返回值:如果成功就返回0,否则返回错误码
  • thread: 需要分离的线程ID

【注意】

  1. 一旦线程处于分离状态,就不能使用pthread_join获取其状态,也无法返回“可连接”状态。
  2. 不可以多次分离,否则会产生不可预料的行为。

7 线程取消

给线程发送一个取消的请求,让线程中止。但是不是立马取消,而是当子线程执行到一个取消点(系统规定好的一些系统调用,可以粗略的理解为从用户区向内核区切换的位置),线程才会终止。

int pthread_cancel(pthread_t thread);

8 线程同步

当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作,其他线程才能对该内存地址进行操作,而其他线程则处于等待状态。

虽然线程同步会一定程度上影响性能,但总体的影响不大。因为其只影响临界区(指访问某一共享资源的代码片段,并且这段代码的执行应为原子操作)

为避免线程更新共享变量时出现问题,可以使用互斥量(mutex 是mutual exclusion的缩写)来确保同时仅有一个线程可以访问某项共享资源。可以使用互斥量来保证对任意共享资源的原子访问。一旦线程锁定互斥量,随即成为该互斥量的所有者,只有所有者才能给互斥量解锁
在这里插入图片描述

8.1 互斥锁

下面系列函数都包含在头文件#include <pthread.h>中。

8.1.1 初始化互斥锁

pthread_mutex_init(pthread_mutex_t mutex,const pthread_mutexattr_t attr);
//成功返回0,失败返回errno
  • mutex:需要初始化的互斥锁
  • attr指定了新建互斥锁的属性。一般传递NULL即可,如果参数attr为空,则使用默认的互斥锁属性,默认属性为快速互斥锁 。其共有四个类型:
    • PTHREAD_MUTEX_NORMAL 普通锁(默认)。当线程加锁以后,其余请求锁的线程将形成一个等待队列,并在解锁后先进先出原则获得锁。
    • PTHREAD_MUTEX_ERRORCHECK 检错锁,如果同一个线程请求同一个锁,则返回EDEADLK,否则与普通锁类型动作相同。这样就保证当不允许多次加锁时不会出现嵌套情况下的死锁。
    • PTHREAD_MUTEX_RECURSIVE 递归锁,允许同一个线程对同一个锁成功获得多次,并通过多次 unlock 解锁。
    • PTHREAD_MUTEX_DEFAULT 适应锁,动作最简单的锁类型,仅等待解锁后重新竞争,没有等待队列。

8.1.2 释放互斥锁资源

与pthread_mutex_init函数相配对的是pthread_mutex_destroy函数,当使用完互斥锁后将锁销毁。

pthread_mutex_destroy(pthread_mutex_t* mutex);
//使用完锁之后释放锁,常用于递归锁的时候
//成功返回0,失败返回errno

8.1.3 加锁

当pthread_mutex_lock()返回时,该互斥锁已被锁定。线程调用该函数让互斥锁上锁,如果该互斥锁已被另一个线程锁定和拥有,则调用该线程将阻塞,直到该互斥锁变为可用为止。

pthread_mutex_lock(pthread_mutex_t mutex);//加锁
pthread_mutex_trylock(*pthread_mutex_t *mutex);
//加锁,但是上面方法不一样的是当锁已经在使用的时候,返回为EBUSY,而不是挂起等待
//成功返回0.失败返回错误信息

8.1.4 解锁

pthread_mutex_unlock是可以解除锁定 mutex 所指向的互斥锁的函数。

pthread_mutex_unlock(pthread_mutex_t *mutex);//释放锁
//成功返回0.失败返回错误信息

8.2 死锁

两个或两个以上的进程在执行过程中,因争夺共享资源而造成的一种互相等待的现象若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁。
【产生死锁的几个场景】

  • 忘记释放锁
  • 重复加锁
  • 多线程多锁,抢占锁资源(因为线程解不开锁,会在锁处阻塞等待,都互相等着对方解锁,因此形成死锁)

8.3 读写锁

在对数据的读写操作中,更多的是读操作,写操作较少,例如对数据库数据的读写应用为了满足当前能够允许多个读出,但只允许一个写入的需求,线程提供了读写锁来实现。
【读写锁的特点】

  • 如果有其它线程读数据,则允许其它线程执行读操作,但不允许写操作。
  • 如果有其它线程写数据,则其它线程都不允许读、写操作。
  • 写是独占的. 写的优先级高(如果三个线程,一个正在读,一个加写锁,一个加读锁,那么等第一个读完后,先上写锁)。

8.3.1 读写锁的类型

pthread_rwlock_t

8.3.2 初始化读写锁

pthread_rwlock_init(pthread_rwlock_tmutex rwlock,const pthread_rwlockattr_t attr);
//成功返回0,失败返回errno
  • rwlock:需要初始化的读写锁
  • attr指定了新建读写锁的属性。一般传递NULL即可。

8.3.3 释放读写锁资源

当使用完读写锁后将锁销毁。

pthread_rwlock_destroy(pthread_rwlock_t* rwlock);
//使用完锁之后释放锁,常用于递归锁的时候
//成功返回0,失败返回errno

8.3.4 加锁

当加锁函数返回时,该读锁或者写锁已被锁定。线程调用该函数让读写锁上锁,如果该读写锁已被另一个线程锁定和拥有,则调用该线程将按照章节开头的介绍来决定是否阻塞或执行,直到该互斥锁变为可用为止。

pthread_rwlock_rdlock(pthread_rwlock_t* rwlock);//加读锁
pthread_rwlock_wrlock(pthread_rwlock_t* rwlock);//加写锁
pthread_rwlock_tryrdlock(pthread_rwlock_t* rwlock);
pthread_rwlock_trywrlock(pthread_rwlock_t* rwlock);
//加锁,但是上面方法不一样的是当锁已经在使用的时候,返回为EBUSY,而不是挂起等待
//成功返回0.失败返回错误信息

8.3.5 解锁

pthread_rwlock_unlock是可以解除锁定 rwlock所指向的读写锁的函数。

pthread_rwlock_unlock(pthread_rwlock_t* rwlock);//释放锁
//成功返回0.失败返回错误信息

9 条件变量

条件变量并不能保证数据不混乱,换言之,它不能替换掉互斥锁的作用。它只能是配合锁实现更高效的线程同步,它有两个作用:

  • 条件满足后,引起阻塞
  • 条件满足后,解除阻塞

9.1 条件变量的类型

pthread_cond_t

9.1 初始化条件变量

初始化条件变量的函数。

int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr);
//函数成功返回0;任何其他返回值都表示错误。
//参数 2 条件变量的属性,一般传 NULL

结构pthread_condattr_t是条件变量的属性结构,和互斥锁一样我们可以用它来设置条件变量是进程内可用还是进程间可用,默认值是PTHREAD_ PROCESS_PRIVATE,即此条件变量被同一进程内的各个线程使用;如果选择为PTHREAD_PROCESS_SHARED则为多个进程间各线程公用。

9.2 释放条件变量

销毁条件变量的函数。

 int pthread_cond_destroy(pthread_cond_t *cond);
 //函数成功返回0;任何其他返回值都表示错误。

需要注意的是只有在没有线程在该条件变量上等待时,才可以注销条件变量,否则会返回EBUSY。

9.3 等待条件变量(无限期阻塞)

条件变量是利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作:一个线程等待"条件变量的条件成立"而挂起;另一个线程使"条件成立"(给出条件成立信号)。为了防止竞争,条件变量的使用总是和一个互斥锁结合在一起
需要注意的是:被阻塞的线程可以被pthread_cond_signal函数,pthread_cond_broadcast函数唤醒,也可能在被信号中断后被唤醒。也就是说pthread_cond_wait函数的返回并不意味着条件的值一定发生了变化,必须重新检查条件的值。因此常常结合while使用

 int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
  //函数成功返回0;任何其他返回值都表示错误。
  //函数将解锁mutex参数指向的互斥锁,并使当前线程阻塞在cond参数指向的条件变量上。

该函数调用时需要传入 mutex参数(加锁的互斥锁) ,函数执行时,先把调用线程放入条件变量的请求队列,然后将互斥锁mutex解锁,当函数成功返回为0时,互斥锁会再次被锁上. 也就是说函数内部会有一次解锁和加锁操作.

9.4 等待条件变量(带时长的阻塞)

函数到了一定的时间,即使条件未发生也会解除阻塞。这个时间由参数abstime指定。函数返回时,相应的互斥锁往往是锁定的,即使是函数出错返回。

 int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abstime);
// cond是指向pthread_cond_t结构的指针,mutex是互斥锁的标识符,abstime为指向timespec结构体的指针。

计时等待方式如果在给定时刻前条件没有满足,则返回ETIMEDOUT,结束等待,其中abstime以与time()系统调用相同意义的绝对时间形式出现,0表示格林尼治时间1970年1月1日0时0分0秒。

9.5 唤醒等待的一个或多个线程

激活一个等待该条件的线程

int pthread_cond_signal(pthread_cond_t *cond);
//函数成功返回0;任何其他返回值都表示错误。

唤醒阻塞在条件变量上的所有线程的顺序由调度策略决定,如果线程的调度策略是SCHED_OTHER类型的,系统将根据线程的优先级唤醒线程。

9.6 唤醒所有的线程

函数以广播的方式唤醒所有被pthread_cond_wait函数阻塞在某个条件变量上的线程,参数cond被用来指定这个条件变量。当没有线程阻塞在这个条件变量上时,pthread_cond_broadcast函数无效。

int pthread_cond_broadcast(pthread_cond_t *cond);
//函数成功返回0;任何其他返回值都表示错误。

由于pthread_cond_broadcast函数唤醒所有阻塞在某个条件变量上的线程,这些线程被唤醒后将再次竞争相应的互斥锁,所以必须小心使用pthread_cond_broadcast函数

10 信号量

信号量的原理就是:灯亮就是可用,灯灭就是不可用。它和条件变量的作用一样,都不能代替锁的使用,都是跟互斥锁一起使用。
就是给它一个初始值n,可以理解为n个灯泡,每用一个,就减少一个,当用完了的时候,就会阻塞。

10.1 信号量的类型

sem_t

10.2 初始化信号

该函数用于初始化信号量:

int sem_init(sem_t *sem, int pshared, unsigned int value);
//返回值:创建成功返回0,失败返回-1
  • sem:指向信号量结构的一个指针
  • pshared:不为0时此sem信号量在进程间共享,为0时当前进程的所有线程共享
  • value:信号量的初始值

10.3 信号量减一(无限期阻塞)

sem_wait 是一个阻塞的函数,测试所指定信号量的值,它的操作是原子的。若 s e m v a l u e > 0 sem_{value} > 0 semvalue>0,则该信号量值减去 1 并立即返回。若 s e m v a l u e = 0 sem_{value} = 0 semvalue=0,则阻塞直到 s e m v a l u e > 0 sem_{value} > 0 semvalue>0,此时立即减去 1,然后返回。

int sem_wait(sem_t *sem); 
int sem_trywait(sem_t *sem);
//操作成功返回0,失败则返回-1

下面的函数sem_trywait是非阻塞的函数,它会尝试获取获取 s e m v a l u e sem_{value} semvalue值,如果 s e m v a l u e = 0 sem_{value} = 0 semvalue=0,不是阻塞住,而是直接返回一个错误 EAGAIN。

10.4 信号量减一(带时长的阻塞)

如果判断为0,则阻塞timeout长的时间。

int sem timedwait(sem t *sem,const struct timespec *abs timeout

10.5 信号值加一

把指定的信号量 sem 的值加 1,唤醒正在等待该信号量的任意线程

int sem_post(sem_t *sem);
//返回值:操作成功返回0,失败则返回-1

10.6 释放信号

释放信号量自己占用的一切资源 (被注销的信号量sem要求:没有线程在等待该信号量了)

int sem_destroy(sem_t * sem)
//成功则返回 0,失败返回 -1

10.7 获得目前的信号量

int sem_getvalue( sem_t *sem,int *sval);

此处的sval是传出参数,其值会被赋予目前的信号量!

10.8 消费者生产者模型详解

对于生产者和消费者模型来说:

  • 首先应该有两个线程(对应生产者和消费者),其次应该有两个信号量(对应生产者的生产上限和消费者的消费下限——也是对应着总资源的上限和下限)
  • 生产者的信号量初始值为n(上限),每次执行应该消耗1个生产者的信号量,生成1个消费者的信号量
  • 消费者的信号量初始值为0,每次执行应该消耗1个消费者的信号量,生成一个生产者的信号量

11 引用

本笔记是针对牛客的高境老师的《第三章Linux多线程开发》内容书写的。默默感谢一下高老师,确实很细致。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值