Linux线程操作

由于在 C++ 阶段就已经深刻学习过 线程相关的知识,但是 C 语言的线程操作繁琐且难用,就算以后要用也必然是用封装过的线程,我们需要做这个工作。总的来讲,我们要显示三个内容:银行存钱取钱,阻塞队列,生产者消费者模型。

线程的基本操作

pthread_self–获取线程ID

每个线程都有一个唯一的ID。

在 Linux 中, pthread_t 类型定义为无符号长整型 ( unsigned long ) 。

1
pthread_t pthread_self(void);

pthread_create–创建线程

1
2
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);

thread:pthread_t* 定义,用于接收创建的线程的标识符。

attr:线程属性,通常是 NULL ,采用默认属性。

start_routine:函数指针。

arg:传递给函数指针的形参。如果不传递任何参数,填 NULL。

代码:C 语言线程的基本使用

实际上这并不难,重点还是要演示如果传递更多的参数,已经如何获取线程的返回值。后者需要等到介绍后面的接口才能做,这里重点谈一谈前者。

我们看传递给函数指针的参数只有一个,理论上也只能传递一个。但是 void* 代表可以传递任何类型,结构体是一个类型,并且支持存储多个不同类型的参数,因此C 语言线程如何突破只能传递一个参数的限制?用结构体!

代码:C 语言线程如何突破只能传递一个参数的限制

pthread_exit–终止线程

1
void pthread_exit(void *retval);

我们通过这个函数可以传递线程的返回值,即 retval 参数。然后其他线程就可以通过 pthread_join() 获取该返回值。等到我们讲完 pthread_join() 就实现获取返回值的代码。

让线程终止的方式,还要很多,下面列举:

  1. 进程终止 (比如调用 exit() ,从 main() 函数返回),那么该进程的所有线程都会立即终止。
  2. 从线程的入口函数返回。
  3. 线程调用 pthread_exit() 。
  4. 响应 pthread_cancel() 的取消请求。

pthread_join–连接已终止的线程

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

如果我们 detach 或者 等待 线程终止,主线程可能早早结束,会导致线程没有被执行的机会,或者执行到途中异常终止,主线程结束意味着进程结束,其他线程依赖于该进程,也就随之消亡。

但它还有一个作用,就是获取返回值。

代码地址:C 语言线程获取返回值

pthread_detach–分离线程

1
int pthread_detach(pthread_t thread);

有时,我们并不关心线程的返回状态,反而希望在线程终止时系统能够自动清理并移除它。在这种情况下,我们可以调用 pthread_detach() 将线程标记为处于分离(detached)状态。一旦线程处于分离状态,就不能再使用 pthread_join() 来获取其返回状态了,也无法再回到可连接(joinable)状态了。

pthread_cancel–取消线程(了解)

通常情况下,程序中的多个线程会并发执行,每个线程各司其职。 但有时候,需要取消一个线程的执行。

与 pthread_exit 只能终止本线程不同。pthread_cancel 可以由其他线程调用以显式地向目标线程发起取消请求,且 pthread_cancel 不是强制的,是一种协商机制,目标线程可以选择是否响应这次取消操作。

1
int pthread_cancel(pthread_t thread);

发送取消请求后,pthread_cancel() 函数会立即返回,不会等待目标线程退出 。

目标线程会不会响应取消请求?以及何时响应?这取决于目标线程的两个属性:取消状态和取消类型。

(一)取消状态

目标线程会不会响应取消请求,是由目标线程的取消状态决定的,它有两个取值:

  • PTHREAD_CANCEL_ENABLE :线程可以取消。这是默认值。
  • PTHREAD_CANCEL_DISABLE :线程不可取消。如果此类线程收到取消请求,则会将取消请求挂起,直到线程的取消状态设置为启用

下面是设置取消状态的方法:

1
int pthread_setcancelstate(int state, int *oldstate);

state :取消状态,可取下面两个值:

  • PTHREAD_CANCEL_ENABLE :线程可以取消。这是默认值。
  • PTHREAD_CANCEL_DISABLE :线程不可取消。如果此类线程收到取消请求,则会将取消请求挂起,直到线程的取消状态设置为启用。

oldstate :传出参数,用来保存以前的取消状态。

(二)取消类型

目标线程何时响应取消请求,是由目标线程的取消类型决定的,它也有两个取值:

  • PTHREAD_CANCEL_DEFERRED :延迟响应,挂起取消请求,直到下一个取消点才响应。这是默认值。
  • PTHREAD_CANCEL_ASYNCHRONOUS :异步响应,可以在任意时间点响应取消请求 (未必是立即响应)。异步响应在实际应用中很少使用。

设置线程的取消类型:

1
int pthread_setcanceltype(int type, int *oldtype);

type :取消类型,可取下面两个值:

  • PTHREAD_CANCEL_DEFERRED :延迟响应,挂起取消请求,直到下一个取消点才响应。这是默认值。
  • PTHREAD_CANCEL_ASYNCHRONOUS :异步响应,可以在任意时间点响应取消请求 (未必是立即响应)。异步响应在实际应用中很少使用。

oldtype :传出参数,用来保存以前的取消类型。

(三)取消点

若将线程的取消性状态和类型分别置为 ENABLE 和 DEFERRED(默认状态),那么只有当线程执行到某个取消点 时,取消请求才会起生效。取消点是一组函数,这些函数往往可以让线程陷入无限期的阻塞。

取消点.png

如果线程的取消类型为 DEFERRED ,当线程收到取消请求后,它会在下次抵达取消点时终止。如果该线程尚未分离,其它线程调用 pthread_join() 进行连接,会收到一个特殊的返回值 PTHREAD_CANCELED 。

(四)设置取消点

线程如果有挂起的取消请求,只要调用这个函数,线程就会终止。

1
void pthread_testcancel(void);

如果线程执行的代码中不包含取消点,可以周期性地调用 pthread_testcancel() ,以确保对取消请求做出及时响应。

线程清理函数(了解)

线程收到取消请求后,会执行到下一个取消点终止。如果线程只是草草地直接终止,可能会让程序处于不一致的状态,比如:对共享变量的修改只进行了一半啦,没有释放互斥锁啦…… 这样的错误轻则导致其他线程产生错误的结果、发生死锁,重则让进程直接崩溃。

为了规避这一问题,线程可以设置一个或多个清理函数。当线程响应取消请求时,会自动执行这些清理函数。每个线程都拥有一个清理函数栈。当线程响应取消请求时,会依次执行栈中的清理函数 (从栈顶到栈底)。当执行完所有的清理函数后,线程终止。

1
2
3
4
5
会将函数指针 routine 添加到清理函数栈的栈顶
void pthread_cleanup_push(void (*routine)(void *), void *arg);

从调用线程的清理函数栈的栈顶删除一个清理函数
void pthread_cleanup_pop(int execute);

线程同步

互斥锁

(一)初始化

下面有两种方式:

1
2
3
pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER; // 静态初始化,仅适用于静态变量

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

(二)上锁

1
2
3
int pthread_mutex_lock(pthread_mutex_t* mutex);	
int pthread_mutex_trylock(pthread_mutex_t* mutex);
int pthread_mutex_timedlock(pthread_mutex_t* mutex, const struct timespec* abstime);

如果已经没有上锁,上面就会锁住。如果已经上锁,就会:按顺序注释

  1. 一直阻塞,直到等待的互斥量解锁
  2. 立即失败,并返回状态码 EBUSY
  3. 如果在超时时间内,调用线程没能获得互斥量的所有权,那么函数 pthread_mutex_timedlock() 会返回错误码 ETIMEDOUT

(三)释放锁

1
int pthread_mutex_unlock(pthread_mutex_t *mutex);

(四)销毁

1
int pthread_mutex_destroy(pthread_mutex_t *mutex);

只有当互斥量处于未锁定状态,且后续不再使用它时,才应将其销毁。

(五)实战

代码地址:10个线程从银行取钱

代码地址:两个银行用户互相转账

条件变量

(一)初始化

1
2
pthread_cond_t cond = PTHREAD_COND_INITIALIZER; // 静态初始化,仅适用于静态变量
int pthread_cond_init(pthread_cond_t* cond, const pthread_condattr_t* attr);

(二)等待

1
2
int pthread_cond_wait(pthread_cond_t* cond, pthread_mutex_t* mutex);
int pthread_cond_timedwait(pthread_cond_t* cond, pthread_mutex_t* mutex, const struct timespec* abstime);

pthread_cond_wait() 函数的语义如下:

  1. 释放所持有的互斥量 mutex 。
  2. 陷入阻塞状态。
  3. 当 pthread_cond_wait() 返回时,调用线程一定再一次获取了互斥量 mutex 。

如果不然,就会一直阻塞。而 pthread_cond_timedwait 可以在哪怕是在指定时间内没有满足要求,也会自动解除阻塞。

(三)唤醒

1
2
int pthread_cond_signal(pthread_cond_t *cond);	// 唤醒一个线程
int pthread_cond_broadcast(pthread_cond_t *cond); // 唤醒多个线程

(四)销毁

1
int pthread_cond_destroy(pthread_cond_t *cond);

当后续不再使用条件变量时,才应调用 pthread_cond_destroy() 将其销毁。

(五)实战

代码地址:阻塞队列

代码地址:生产者消费者