Linux线程库
Posted 顾文繁
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux线程库相关的知识,希望对你有一定的参考价值。
线程引言
Linux上最有名的线程库是LinuxThreads和NPTL,它们都是采用1:1的方式实现的。现代Linux上模式运行的线程库是NPTL(Native POSIX Thread Library)。可以使用
getconf GNU_LIBPTHREAD_VERSION
查看当前系统上使用的线程库。
从Linux内核2.6开始,提供了真正的内核线程。新的NPTL线程库应运而生。相对于LinuxThreads,NPTL的主要优势在于:
- 内核线程不再是一个进程,这就避免了很多进程模拟内核线程导致的语义问题。
- 抛弃了管理线程,终止线程、回收线程堆栈等工作可以由内核来完成。
- 由于不存在管理线程,所以一个进程的线程可以运行在不同的CPU上,从而充分利用了多处理器系统的优势。
- 线程的同步由内核来完成,属于不同进程的线程之间也能共享互斥锁,因此可实现跨进程的线程同步。
创建线程和结束线程
创建
在pthread.h的头文件中,创建线程的函数pthread_create定义如下:
#include <pthread.h>
int pthread_create(pthread_t* thread, const pthread_attr_t* attr, void* (*start_routine)(void*), void* arg);
thread参数是新线程的标识符,attr参数用于设置新线程的属性。给他传递NULL表示使用默认线程属性。start_routine和arg分别为回调函数以及传给回调函数的参数。
退出
线程一旦被创建好,内核就可以调度内核线程来执行start_routine回调函数了,线程函数在结束时最好屌用退出函数,以确保安全,干净的删除。
void thread_exit(void* retval);
pthread_exit函数通过retval参数向线程的回收者传递其退出信息。它执行完之后不会返回到调用者,并且不会失败。
回收线程
一个进程中的所有线程都可以使用pthread_join函数来回收其他线程(线程是可回收的情况下)
,即等待其他线程结束。pthread_join定义如下:
pthread_join(pthread_t thread, void** retval);
thread参数是目标线程的标识符,retval参数则是目标线程返回的退出信息。该函数会一直阻塞,直到背回收的线程结束为止。该函数成功时返回0,失败则返回错误码。
终止线程
int pthread_cancel(pthread_t thread)
thread参数是目标线程的标识符,回收成功返回0,失败返回错误码。
接收到取消请求的目标线程可以决定是否允许被取消以及如何取消,这分别由如下两个函数完成:
#include <pthread.h>
int pthread_setcancelstate(int state, int *oldstate);
int pthread_setcanceltype(int type,int* oldtype);
state有两个可选值:
- PTHREAD_CANCEL_ENABLE,允许线程被取消。他是线程被创建时的默认取消状态。
- PTHREAD_CANCEL_DISABLE,禁止线程被取消。这种情况下,如果一个线程收到取消请求,则它会将请求挂起,知道该线程允许被取消。
type参数同样有两个可选值:
- PTHREAD_CANCEL_ASYNCHRONOUS,线程随时都可以被取消。它将使得接受到的取消请求目标线程立即采取行动。
- TRHREAD_CANCEL_DEFERRED,允许目标线程推迟行动,知道它调用取消点函数中的一个:pthread_join,pthread_testcancel,pthread_cond_wait,pthread_cond_timedwait,sem_wait和sigwait。根据POSIX标准,其他可能阻塞的系统调用,比如read,wait可以成为取消点。不过为了安全起见,我们最好在可能会被取消的代码中调用pthread_testcancel函数以设置取消点。
pthread_setcancelstate和pthread_setcanceltype成功时返回0,失败返回错误码。
POSIX信号量
和多进程程序一样,多线程程序也必须考虑同步问题。pthread_join可以看作一种简单的线程同步方式,不过很显然,他无法高效地实现复杂的同步需求,比如控制对共享资源的独占访问,或者在某个条件满足后唤醒一个线程。接下来我们讨论3中专门用于线程同步机制:POSIX信号量、互斥量和条件变量。
POSIX信号量函数的名字都以sem_开头,并不想大多数线程函数那样以pthread_开头,常用的POSIX信号量函数是下面5个:
#include <semaphore.h>
int sem_init(sem_t* sem, int pshared, unsigned int val);
int sem_destory(sem_t* sem);
int sem_wait(sem_t* sem);
int sem_trywait(sem_t* sem);
int sem_post(sem_t* sem);
sem_init用于初始化一个未命名的信号量,pshared参数制定信号量的类型,如果其值为0,就代表这个信号量是当前进程局部信号量,否则该信号量就可以在多个进程之间共享。val制定信号量的初始值。
sem_destory销毁信号量。
sem_wait、sem_trywait函数以原子的操作方式将信号量的值-1.如果信号量的值为0,则sem_wait将被阻塞,直到这个信号量具有非0值。sem_trywait是sem_wait的非阻塞版本,当信号量为非0时,sem_trywait对信号量执行-1,当信号量的值为0时,他将返回-1,并设置errno为EAGAIN
sem_post函数将以原子的方式将信号量的值+1,当信号量的val>0时,其他正在调用sem_wait等待信号量的线程将被唤醒。
互斥锁
互斥锁(也成互斥量)可以用于保护关键代码,以确保其独占的访问,这点有点像一个二进制信号量,当进入关键代码时,我们需要获得互斥锁并将其加锁,这等价于二进制信号量的P操作,当离开关键代码时我们需要对互斥锁解锁,以唤醒其他等待该互斥的线程,这等价于二进制信号量的V操作。
POSIX互斥锁的相关函数只要有5个:
#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t* mutex, const pthread_mutexattr_t* mutexattr);
int pthread_mutex_destroy(pthread_mutex_t* mutex);
int pthread_mutex_lock(pthread_mutex_t* mutex);
int pthread_mutex_trylock(pthread_mutex_t* mutex);
int pthread_mutex_unlock(pthread_mutex_t* mutex);
互斥锁的属性
pthread_mutexattr_t结构体定义了一套完整的互斥锁属性。线程库提供了一系列函数来操作pthread_mutexattr_t类型的变量,以方便我们获取和设置互斥锁属性,下面列出一些主要的函数。
#include <pthread.h>
int pthread_mutexattr_init(pthread_mutexattr_t* attr);
int pthread_mutexattr_destroy(pthread_mutexattr_t* attr);
//获取和设置互斥锁的pshared
int pthread_mutexattr_getpshared(const pthread_mutexattr_t* attr, int* pshared);
int pthread_mutexattr_setpshared(const pthread_mutexattr_t* attr, int pshared);
//获取和设置互斥锁的type属性
int pthread_mutexattr_gettype(const pthread_mutexattr_t* attr, int* type);
int pthread_mutexattr_settype(const pthread_mutexattr_t* attr, int type);
两种常用的属性:pshared,type
pshared:
PTHREAD_PROCESS_SHARED--互斥锁可以被跨进程共享
PTHREAD_PROCESS_PRIVATE--互斥锁只能被初始化线程隶属同一个进程的线程共享。
type:
PTHREAD_MUTEX_NORMAL--普通锁,默认锁
PTHREAD_MUTEX_ERRORCHECK--检测锁
PTHRED_MUTEX_RECURSIVE--嵌套锁
PTHREAD_MUTEX_DEFAULT--默认锁
线程死锁举类
#ifndef TEST_FFMPEG_SISUO_H
#define TEST_FFMPEG_SISUO_H
#include <unistd.h>
#include <pthread.h>
#include <stdio.h>
pthread_mutex_t mutex_a;
pthread_mutex_t mutex_b;
void* another(void* arg){
pthread_mutex_lock(&mutex_b);
printf("in child thread, got mutex b ,waiting for mutex a\\n");
sleep(2);
pthread_mutex_lock(&mutex_a);
pthread_mutex_unlock(&mutex_a);
pthread_mutex_unlock(&mutex_b);
pthread_exit(nullptr);
}
void test_sisuo(){
pthread_t id;
pthread_mutex_init(&mutex_a,NULL);
pthread_mutex_init(&mutex_b,NULL);
pthread_create(&id, NULL, another, NULL);
pthread_mutex_lock(&mutex_a);
printf("in main thread, got mutex a , waiting for mutex b\\n");
sleep(2);
pthread_mutex_lock(&mutex_b);
pthread_mutex_unlock(&mutex_b);
pthread_mutex_unlock(&mutex_a);
//线程同步
pthread_join(id, NULL);
pthread_mutex_destroy(&mutex_a);
pthread_mutex_destroy(&mutex_b);
}
#endif //TEST_FFMPEG_SISUO_H
当执行时,主线程拿到a的锁,开启的线程拿到了b,主线程再申请锁时,锁被开启的子线程拿到了,同步等待,子线程申请a的锁时,又被主线程锁上了,两个线程互相拿到了对方的锁。
用一个形象的例子说明一下,一个人需要使用同时拿到簸箕和扫帚才能工作,假设两个人分别拿到了苕帚和簸箕,两个人同时都准备工作时,都向对方要对方的工具,这就造成了两个都无法工作,造成了死锁。
条件变量
互斥锁是用于同步线程对共享数据的访问,条件变量则是用于在线程之间同步共享数据的值。条件变量提供了一种线程间的通知机制:当某个共享数据达到某个值的时候,唤醒等待这个共享数据的线程。
#include <pthead.h>
int pthread_cond_init(pthread_cond_t* cond, const pthread_condattr_t* cond_attr);
int pthread_cond_destory(pthread_cond_t* cond);
//以广播方式唤醒所有等待目标条件变量的线程
int pthread_cond_broadcast(pthread_cond_t* cond);
int pthread_cond_signal(pthread_cond_t* cond)
// lock->unlock->wait->..lock..unlock
int pthread_cond_wait(pthread_cond_t* cond);
生产者消费者例子
//
// Created by wenfan on 2021/6/29.
//
#ifndef TEST_FFMPEG_CONDITION_VARIABLE_H
#define TEST_FFMPEG_CONDITION_VARIABLE_H
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>
pthread_mutex_t mutex;
pthread_cond_t cond;
typedef struct node{
int num;
struct node* next;
}node_t;
node_t* head = nullptr;
node_t* tmp = nullptr;
void* producer(void* arg){
while (true){
pthread_mutex_lock(&mutex);
tmp = new node;
int rand_num = rand()%100;
if(rand_num == 100)
break;
tmp->next = head;
tmp->num = rand_num;
head = tmp;
std::cout << "生产:" << rand_num << std::endl;
pthread_mutex_unlock(&mutex);
//通知
pthread_cond_signal(&cond);
sleep(10);
}
}
void* consumer(void* arg){
while (true){
pthread_mutex_lock(&mutex);
if(head == nullptr){
pthread_cond_wait(&cond, &mutex);
std::cout << "pthread_cond_wait" << std::endl;
}
tmp = head;
head = tmp->next;
std::cout << "消费:" << tmp->num << std::endl;
delete tmp;
tmp = nullptr;
pthread_mutex_unlock(&mutex);
sleep(rand()%3+1);
}
}
void consumer_producer_test(){
pthread_mutex_init(&mutex, nullptr);
pthread_cond_init(&cond, nullptr);
pthread_t consumer_t, producer_t;
pthread_create(&consumer_t, nullptr, consumer, nullptr);
pthread_create(&producer_t, nullptr, producer, nullptr);
pthread_join(consumer_t, nullptr);
pthread_join(producer_t, nullptr);
}
#endif //TEST_FFMPEG_CONDITION_VARIABLE_H
在这个例子中,当头节点head==nullptr时,释放锁,消费者consumer尽心等待,然后生产者producer获得了锁,生产区块,然后使用pthread_cond_signal进行通知,此时的消费者往下执行(又获得了锁)。
相较于mutex而言,条件变量可以减少无意义的竞争。如直接使用mutex,除了生产者、消费者之间要竞争互斥量以外,消费者之间也需要竞争互斥量,但如果汇聚(链表)中没有数据,消费者之间竞争互斥锁是无意义的。有了条件变量机制以后,只有生产者完成生产,才会引起消费者之间的竞争。提高了程序效率。
以上是关于Linux线程库的主要内容,如果未能解决你的问题,请参考以下文章
newCacheThreadPool()newFixedThreadPool()newScheduledThreadPool()newSingleThreadExecutor()自定义线程池(代码片段