线程是CPU调度的基本单位,在Linux中是没有线程的概念的,取而代之的是轻量级进程,也叫做LWP。
进程由PCB和运行中的程序组成,PCB中有各种描述进程的字段,其中有一个指针struct mm_struct *mm指向虚拟地址空间。
进程在创建线程时,其实是创建了一个PCB,并且这个PCB里的mm指针是相同的。
红色的PCB就是用来描述创建出来的线程的。
CPU的调度单位是线程,所以进程创建出来多个线程后,这个“进程内部也就可以并发执行”。
errno
__thread
修饰的静态/全局字段,该字段虽然被创建在代码段,但是每个线程都会拥有自己的独立的实例,存放在私有栈中#include
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg);
通过pthread_create
即可创建一个线程。
#include
#include
void *start_routine(void *arg) {
std::cout << (char *)arg << std::endl;
return nullptr;
}
int main() {
pthread_t tid;
int n = pthread_create(&tid, nullptr, start_routine, (void *)"hello world");
while(1);
return 0;
}
当创建线程时,能够通过第一个输出型参数获取到线程标识符,也可以通过pthread_self
函数获取调用者的线程标识符。
该标识符其实是描述一个线程单位的地址。
虚拟地址空间的共享区会加载原生线程库,而关于线程的一切操作都调用的是该库。当创建线程时,会在共享区中创建线程的TCB,创建多个线程时就会创建多个TCB,这些TCB被某种数据结构管理着。而线程的标识符就是TCB的入口地址。
线程可以通过return
返回,还可以通过pthread_exit
返回,但不可以通过exit
或者是_exit
返回。
因为调用exit
或者是_exit
,OS会向调用进程发送信号,导致整个进程退出,所以为了确保只是当前线程退出,应该使用return
或pthread_exit
。
#include
void pthread_exit(void *retval);
进程在退出时,需要父进程来回收资源,线程在退出时,也需要创建它的线程来进行回收。
可以通过pthread_join
进行等待。
#include
int pthread_join(pthread_t thread, void **retval);
#include
#include
#include
#include
void *start_routine(void *arg) {
int cnt = 3;
while(cnt--) sleep(1);
return (void *)"hello world!";
}
int main() {
pthread_t tid;
pthread_create(&tid, nullptr, start_routine, nullptr);
void *retval = nullptr;
pthread_join(tid, &retval);
std::cout << (char *)retval << std::endl;
return 0;
}
因为这样副线程就能够返回任意的的数据(除了栈上的),比如副线程能够返回new出的一个类或者一个结构体的指针,而外部想要获取这个指针只能通过二级指针。
那么join函数是如何取到该指针的呢?
是因为线程函数的返回值回存放到pthread库中的一个地方,然后join函数去这个地方就能拿到该值。
当主线程不需要关心副线程的返回值的时候,那么主线程可以选择不join等待副线程,而是让副线程自己悄无声息的退出。
#include
int pthread_detach(pthread_t thread);
该函数通过传入线程标识符,可以让该线程与主线程分离,进而当该线程退出时,不需要主线程去join。
该函数可以在主线程中使用pthread_detach(tid)
,也可以在副线程中使用pthread_detach(pthread_self())
当副线程的任务不重要或者是运行到一半不需要了的时候,主线程可以手动中止该副线程。
#include
int pthread_cancel(pthread_t thread);
该函数传入要中止的线程标识符。
多线程是共享虚拟地址空间的,所以多个线程间的数据共享是简单且自然的。最简单的就好比一个全局变量,都是多个线程共享的,那么多个线程同时访问该变量会不会出问题呢?
#include
#include
#include
#include
int gsize = 1000;
void *start_routine(void *arg) {
while (gsize > 0) {
usleep(1000);
--gsize;
}
return nullptr;
}
int main() {
pthread_t tid;
pthread_create(&tid, nullptr, start_routine, nullptr);
while (gsize > 0) {
usleep(1000);
--gsize;
}
pthread_join(tid, nullptr);
std::cout << gsize << std::endl;
return 0;
}
很明显,两个线程的逻辑分别都是当gsize>0时就- -,当gsize == 0时就退出。所以最终的答案应该是0才对,而这里的答案是-1。
那是因为当两个线程同时访问gsize的时候,假设此时gsize为1,而两个线程while判断的时候都是1 > 0,从而都减了两次,所以答案会是-1。
usleep的目的是,加速cpu切片的频率(因为当内核态向用户态进行转换的时候会检测是否需要切片,当合适的时候就切),以便更容易出现实验效果。
多个线程都能访问到的资源叫做临界资源,上面的gsize就是临界资源,而访问临界资源的代码就是临界区,代码中的两个while块。
如何保证临界资源的安全性呢?可以通过互斥锁、信号量等。
要保证临界资源的安全性,只需保证以下几点:
那么要做到这几点,只需要一把互斥锁,在多个线程要进去临界区时就去抢这把锁,谁抢到谁就进,进入临界区的人把门锁住,别人也就进不去了,当临界区里的人出来时,再把门打开,把锁放回去,其它线程再去抢锁。
定义互斥锁
互斥锁就是一个pthread_mutex_t
类型的变量。
定义一个名为mtx
的互斥锁:pthread_mutex_t mtx
初始化互斥锁
int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
局部变量的互斥锁,通过上面的函数进行初始化。
全局变量的互斥锁,通过上面的宏进行初始化。
销毁互斥锁
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);
使用互斥锁将上面代码修正
#include
#include
#include
#include
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
int gsize = 1000;
void *start_routine(void *arg) {
while (true) {
pthread_mutex_lock(&mutex);
if (gsize > 0) {
usleep(1000);
--gsize;
pthread_mutex_unlock(&mutex);
} else {
pthread_mutex_unlock(&mutex);
break;
}
}
return nullptr;
}
int main() {
pthread_t tid;
pthread_create(&tid, nullptr, start_routine, nullptr);
while (true) {
pthread_mutex_lock(&mutex);
if (gsize > 0) {
usleep(1000);
--gsize;
pthread_mutex_unlock(&mutex);
} else {
pthread_mutex_unlock(&mutex);
break;
}
}
pthread_join(tid, nullptr);
std::cout << gsize << std::endl;
pthread_mutex_destroy(&mutex);
return 0;
}
其实互斥锁本身也是一个临界资源,那么互斥锁的安全是如何保证的呢?
因为互斥锁的上锁与解锁是原子性的。
// 伪代码
lock:
movb $0, %al
xchgb %al, mutex
if (al寄存器的内容 > 0) {
return 0;
} else {
阻塞挂起
}
被操作系统唤醒后
goto lock
unlock:
movb $1, mutex
唤醒等待mutex的线程
return 0
先看一下现象
#include
#include
#include
#include
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
int gsize = 1000;
void *start_routine(void *arg) {
while (true) {
pthread_mutex_lock(&mutex);
if (gsize > 0) {
usleep(1000);
std::cout << (char *)arg << "抢到锁了" << std::endl;
--gsize;
pthread_mutex_unlock(&mutex);
} else {
pthread_mutex_unlock(&mutex);
break;
}
}
return nullptr;
}
int main() {
pthread_t tid;
pthread_create(&tid, nullptr, start_routine, nullptr);
while (true) {
pthread_mutex_lock(&mutex);
if (gsize > 0) {
usleep(1000);
std::cout << (char *)arg << "抢到锁了" << std::endl;
--gsize;
pthread_mutex_unlock(&mutex);
} else {
pthread_mutex_unlock(&mutex);
break;
}
}
pthread_join(tid, nullptr);
std::cout << gsize << std::endl;
pthread_mutex_destroy(&mutex);
return 0;
}
可以发现,我们虽然是两个线程在共同竞争同一把锁,但总是副线程能抢到,而主线程抢不到。
这是因为,当多个线程因为竞争锁而被阻塞时,被唤醒的顺序是不确定的,当副线程释放锁时,副线程也就有更大的概率能抢上锁(因为锁总是在副线程手中,副线程刚释放,那么在众多竞争锁的线程中,副线程是距离这把锁最近的线程)。那么这就会导致其它线程总是在竞争锁而竞争不到从而一直在阻塞。这就是互斥锁导致的线程饥饿问题。
那么要解决这一问题,就必须让竞争锁的线程按照一定的顺序进行排队。那么就出现了条件变量。
条件变量能够让因为申请互斥锁而导致阻塞的线程按照申请锁的顺序进行排队,这些线程被唤醒时也是按照这个排队顺序被唤醒的。也就能够解决上面的线程饥饿问题,因为当释放锁的线程再次去申请锁时,必须先申请条件变量。
定义条件变量
条件变量就是一个pthread_cond_t
类型的变量。
定义一个名为cond
的条件变量:pthread_cond_t cond
初始化条件变量
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
局部变量的条件变量,通过上面的函数进行初始化。
全局变量的条件变量,通过上面的宏进行初始化。
销毁条件变量
int pthread_cond_destroy(pthread_cond_t *cond);
申请条件变量
#include
int pthread_cond_timedwait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex,const struct timespec *restrict abstime);
int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
TIMEOUT
唤醒条件变量
#include
int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);
使用条件变量量修改上面代码
#include
#include
#include
#include
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
int gsize = 1000;
void *start_routine(void *arg) {
while (true) {
pthread_mutex_lock(&mutex);
pthread_cond_wait(&cond, &mutex);
if (gsize > 0) {
usleep(1000);
std::cout << (char *)arg << "抢到锁了" << std::endl;
--gsize;
pthread_mutex_unlock(&mutex);
} else {
pthread_mutex_unlock(&mutex);
break;
}
}
return nullptr;
}
int main() {
pthread_t tid1, tid2;
pthread_create(&tid1, nullptr, start_routine, (void *)"副线程1号");
pthread_create(&tid2, nullptr, start_routine, (void *)"副线程2号");
while (true) {
pthread_cond_signal(&cond);
sleep(1);
}
pthread_join(tid1, nullptr);
pthread_join(tid2, nullptr);
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&cond);
return 0;
}
一个条件变量对应一个等待对列,每个调用wait函数的线程都会在这个队列上。
当调用signal或broadcast时,会唤醒这个条件变量对应的队列上的线程。
像单纯的互斥锁,只能保证一个整体的互斥,但有时,我们并不是要一个整体互斥,比如电影院买票,一个大厅有100个座位,我们用互斥锁对整个大厅进行封锁是不合适的,这时就可以用信号量了。
定义信号量
信号量是一个sem_t
类型的变量,定义一个名为sem
的信号量:sem_t sem
初始化信号量
#include
int sem_init(sem_t *sem, int pshared, unsigned int value);
销毁信号量
#include
int sem_destroy(sem_t *sem);
申请资源
#include
int sem_wait(sem_t *sem);
int sem_trywait(sem_t *sem);
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
该类函数用于申请资源,每次调用这个函数,都会使得信号量值减1,若当信号量为0时还调用该函数则会阻塞挂起。
EAGAIN
ETIMEDOUT
该操作也经常被称为P操作
释放资源
#include
int sem_post(sem_t *sem);
信号量加1,若信号量大于0了,则会唤醒阻塞的线程。
该操作也经常被称为V操作
wait(s) {
while (s <= 0) {
阻塞挂起
}
--s
}
post(s) {
++s
}
A线程
lock(mutex1)
lock(mutex2)
// 业务逻辑
unlock(mutex2)
unlock(mutex1)
B线程
lock(mutex2)
lock(mutex1)
// 业务逻辑
unlock(mutex1)
unlock(mutex2)
若A线程在获取了1锁后被切走了,此时B线程又获取了2锁,那么此时就形成了死锁。A线程想继续获取2锁但因为B线程已经获取了所以A线程被挂起,B线程想继续获取1锁但因为A线程已经获取了所以B线程也被挂起,那么至此A、B两个线程都被挂起,除非有外界干预,否则这两个线程将一直保持挂起状态。
死锁的四个必要条件
如何避免死锁
简单理解
生活中处处存在生产者消费者模型的例子
这就是生活中最常见最典型的例子了。
在日常生活中,我们也很少会直接去供货商那买东西,因为麻烦,为了方便所以我们总是去超市买东西。供货商也不会直接在马路上拉个人就卖给他,因为这样卖出去的量太少了。所以,在日常生活中,若是生产者和消费者直接接触,那么经济交易的效率就会大大降低。所以就诞生了超市这个角色,供货商把大量的产品批发给超市,而不用担心自己的产品滞销,老百姓之间去超市买东西,既简单便捷,而且能够买到产品的种类也很多。这就使得生产者和消费者之间的联系变得非常微弱。
在计算机中也需要这样的生产者消费者模型
发布任务的线程就如同生产者,执行任务的线程就如同消费者。若是这两种线程直接接触,那么效率就会大大降低,若是在这两种线程之间多一个缓冲区,这个缓冲区就如同超市,生产者把发布的任务放缓冲区里,消费者去缓冲区里拿要执行的任务,这样就能够实现两种线程之间的解藕。
生产者消费者模型的优点
#include
#include
#include
const static size_t gcap = 10;
template
class BlockQueue {
public:
BlockQueue() : cap_(gcap) {
pthread_mutex_init(&mutex_, nullptr);
pthread_cond_init(&producer_cond_, nullptr);
pthread_cond_init(&consumer_cond_, nullptr);
}
~BlockQueue() {
pthread_mutex_destroy(&mutex_);
pthread_cond_destroy(&producer_cond_);
pthread_cond_destroy(&producer_cond_);
}
void push(const T &in) {
pthread_mutex_lock(&mutex_);
while (is_full()) {
pthread_cond_wait(&producer_cond_, &mutex_);
}
bq_.push(in);
pthread_cond_signal(&consumer_cond_);
pthread_mutex_unlock(&mutex_);
}
void pop(T *out) {
pthread_mutex_lock(&mutex_);
while (is_empty()) {
pthread_cond_wait(&consumer_cond_, &mutex_);
}
*out = bq_.front();
bq_.pop();
pthread_cond_signal(&producer_cond_);
pthread_mutex_unlock(&mutex_);
}
private:
bool is_empty() { return bq_.size() == 0; }
bool is_full() { return bq_.size() == cap_; }
private:
std::queue bq_;
size_t cap_;
pthread_mutex_t mutex_;
pthread_cond_t producer_cond_;
pthread_cond_t consumer_cond_;
};
#include
#include
#include
#include
const static size_t gcap = 10;
template
class RingQueue {
public:
RingQueue() : cap_(cap), space_step_(0), data_step_(0) {
rq_.resize(cap_);
pthread_mutex_init(&mutex_, nullptr);
sem_init(space_sem_, 0, cap_);
sem_init(data_sem_, 0, 0);
pthread_mutex_init(&producer_mutex_, nullptr);
pthread_mutex_init(&consumer_mutex_, nullptr);
}
~RingQueue() {
pthread_mutex_destroy(&mutex_);
sem_destroy(&space_sem_);
sem_destroy(&data_sem_);
pthread_mutex_destroy(&producer_mutex_);
pthread_mutex_destroy(&consumer_mutex_);
}
void push(const T& in) {
P(&space_sem_);
pthread_mutex_lock(&producer_mutex_);
rq_[space_step_++] = in;
space_step_ %= cap_;
pthread_mutex_unlock(&producer_mutex_);
V(&data_sem_);
}
void pop(T *out) {
P(&data_sem_);
pthread_mutex_lock(&consumer_mutex_);
*out = rq_[data_step_++];
data_step_ %= cap_;
pthread_mutex_unlock(&consumer_mutex_);
V(&space_sem_);
}
private:
void P(sem_t *sem) { sem_wait(sem); }
void V(sem_t *sem) { sem_post(sem); }
private:
std::vector rq_;
size_t cap_;
pthread_mutex_t mutex_;
sem_t space_sem_;
sem_t data_sem_;
int space_step_;
int data_step_;
pthread_mutex_t producer_mutex_;
pthread_mutex_t consumer_mutex_;
};