目录
1. Linux下线程概念
在传统操作系统中pcb是一个进程,描述一个程序的运行,还有一个tcp描述实现进程。但是在Linux下使用pcb描述实现了程序调度,并且这些pcb共用一个虚拟地址空间,相较于传统的pcb更加轻量化,因此也把linux下的称为轻量级进程。
- 一个进程分为多个线程,每个线程都是一个pcb,多个线程(线程组)是一个进程。
- 进程是资源分配的基本单位
- 线程是cpu调度的基本单位
线程间的独有与共享
- 独有:栈,寄存器,信号屏蔽字,errno,标识符
- 共享:虚拟地址空间(代码段,数据段相同),文件描述符(文件只用打开一次,大家共用),信号处理方式,工作路径,用户ID,组ID
多线程特点
- 线程间通信更加灵活(全局变量,函数传参)
- 线程的创建/销毁成本更低
- 线程间的调度成本更低
多进程的特点
- 具有独立性,因此更加稳定,健壮
共同优点
- CPU密集型程序,IO密集型程序,并行压缩CPU处理/IO等待时间
- 线程并非越多越好,调度线程也需要成本
2.线程控制
POSIX线程库
- 与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以“pthread_”开头的,要使用这些函数库,要通过引入头文<pthread.h>,链接这些线程函数库时要使用编译器命令的“-lpthread”选项
2.1 线程创建
int pthread_create(pthread_t* tid, pthread_attr_t* attr, void* (*thread_routine)(void* arg), void* arg);
- tid:线程在虚拟地址空间开辟的线程空间的首地址,是一个获得型参数,用于获取线程ID,通过这个ID可以找到线程的描述信息,进而访问PCB(轻量级进程完成控制)
- attr:线程属性信息,通常置NULL
- thread_routine:线程入口函数,创建一个线程就是为了运行这个函数,函数运行完毕,则线程退出。
- arg:通过线程入口函数,传递给线程的参数。
- 返回值:0成功,失败返回一个非0值
代码
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>
void* thread_start(void* arg)
{
while(1)
{
printf("线程:%s\n", (char*)arg);
sleep(1);
}
return NULL;
}
int main()
{
pthread_t tid;
char buf[] = "perseverse\n";
int ret = pthread_create(&tid, NULL, thread_start, (void*)buf);
if (ret != 0)
{
printf("thread create error:%d", ret);
return -1;
}
while(1)
{
printf("I am main thread\n");
sleep(1);
}
return 0;
}
makefile
1 create:create.c
2 gcc $^ -o $@ -lpthread #pthread
- 在编译时, 要链接库函数pthread, 不使用l可以增强跨平台性, 两者均可
- 结果
[test@localhost thread]$ ./create
I am main thread
线程:perseverse
I am main thread
线程:perseverse
I am main thread
线程:perseverse
^C
使用ps -ef查看进程信息
[test@localhost ~]$ ps -ef | head -n 1 && ps -ef | grep create
UID PID PPID C STIME TTY TIME CMD
test 5343 2847 0 10:43 pts/0 00:00:00 ./create
使用 ps -efL 查看轻量级进程信息
UID PID PPID LWP C NLWP STIME TTY TIME CMD
test 5343 2847 5343 0 2 10:43 pts/0 00:00:00 ./create
test 5343 2847 5344 0 2 10:43 pts/0 00:00:00 ./create
根据上面的结果, 可以看出, -L的选项是查看轻量级进程信息, 即线程信息
其中
名称 | 解释 |
---|---|
UID | user id 用户ID |
PID | process id 进程ID, 即线程组ID, 是主线程的ID |
LWP | light weight process or thread. 轻量级进程ID, 即线程标识符 |
NLWP | number of LWP in the process. 线程数量 |
PPID | parent processid, 父进程标识符 |
2.2 线程终止
- 普通线程入口函数中的return,(main函数中的return退出的是进程)
- void pthread_exit(void* retval); 退出一个线程,谁调用谁退出。retval线程返回值,通常为NULL
- int pthread_cancel(pthread_t tid); 退出一个指定的线程
- tid 指定的线程id,pthread_t pthread_self(void)获取当前调用栈的线程id, id
- pthread_cancel(pthread_self(NULL));自己退出,违规操作。
注意:
- 线程退出,也不会完全释放资源,需要被其他线程等待
- 主线程退出,其他线程正常运行,不是主流做法
- 主线程退出,不影响整个进程的运行,只有所有线程退出,进程才会退出
2.3 线程等待
- 等待一个线程的退出,获取退出线程的返回值,回收这个线程所占用的资源(一般使用线程分离来结束线程)
- int pthread_join(pthread_t tid, void** retval);
- tid 指定要等待的线程Id, retval 用于获取线程退出的返回值
- void* retval=NULL; pthread_join(tid, &retval); printf("%s", *retval);
- 不是所有的线程都要被等待,一个线程被创建,默认情况下有一个属性joinable;处于joinable属性的线程退出后不会自动释放资源,需要被等待。
2.4 线程分离
- 将线程的属性从joinable设置为detach, 处于detach属性的线程退出后会自动释放资源,不需要被等待。
- int pthread_detach(pthread_t tid); 分离指定的线程,可以在任意位置分离,习惯在将进入线程入口函数开始时
- pthread_detach(pthread_self()); 可以在创建线程之后直接分离
- 等待一个已经分离的线程,则pthread_join会返回错误:这不是一个joinable线程(因为在获取返回值的时候获取不到,detach属性的线程在退出后已经自动释放了资源)
3. 线程安全
- 线程安全的概念:多个线程对临界资源进行争抢访问,而不会造成数据二义或逻辑混乱的情况;主要针对临界资源的访问操作说明。
- 线程安全的实现:同步-->通过条件判断实现访问的合理性;互斥--> 通过同一时间唯一访问实现安全性。
- 互斥的实现:互斥锁。
互斥:我访问临界资源的时候,别人不能访问--->通过互斥锁对临界资源的访问操作进行加锁保护,在加锁期间别人不能访问临界资源。
互斥锁实现互斥的原理:互斥锁本质是一个只有0/1的计数器,对临界资源当前的访问状态进行标记(可访问/不可访问),对临界资源访问之前先通过加锁(通过计数器判断当前状态是否能够访问临界资源,可以访问则将访问状态置为不可访问,然后再去访问临界资源/不可访问则挂起线程,进行等待),对临界资源访问完毕之后进行解锁(将临界资源的访问状态置为可访问)。
- 互斥锁本身也是一个临界资源,但是互斥锁自身计数器的操作保证了原子操作。
- 代码操作:
- 定义互斥锁变量: pthread_mutex_t;
- 初始化互斥锁: pthread_mutex_init() / PTHREAD_MUTEX_INITIALIZER;
- 访问临界资源之前加锁:pthread_mutex_lock(); / pthread_mutex_trylock();
- 访问临界资源完毕后解锁:pthread_mutex_unlock();
- 销毁互斥锁:pthread_mutex_destroy();
使用互斥锁实现黄牛抢票
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
int ticket = 100; // 设100张票, 属于临界资源
pthread_mutex_t mutex;
void *route(void *arg)
{
char *id = (char*)arg;
while (1)
{
pthread_mutex_lock(&mutex);
if ( ticket > 0 )
{
usleep(1000);
printf("%s sells ticket:%d\n", id, ticket);
ticket--;
pthread_mutex_unlock(&mutex);
}
else
{
pthread_mutex_unlock(&mutex);
break;
}
usleep(100);
}
return NULL;
}
int main()
{
// 设四个黄牛
pthread_t t1, t2, t3, t4;
// 互斥锁的初始化
// pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;使用宏初始化
// 使用接口初始化
pthread_mutex_init(&mutex, NULL);
pthread_create(&t1, NULL, route, (void*)"thread 1");
pthread_create(&t2, NULL, route, (void*)"thread 2");
pthread_create(&t3, NULL, route, (void*)"thread 3");
pthread_create(&t4, NULL, route, (void*)"thread 4");
pthread_join(t1, NULL);
pthread_join(t2, NULL);
pthread_join(t3, NULL);
pthread_join(t4, NULL);
pthread_mutex_destroy(&mutex);
return 0;
}
运行结果
[test@localhost thread]$ ./mutex
thread 4 sells ticket:100
thread 1 sells ticket:99
thread 2 sells ticket:98
thread 3 sells ticket:97
thread 4 sells ticket:96
thread 1 sells ticket:95
thread 2 sells ticket:94
thread 3 sells ticket:93
thread 4 sells ticket:92
thread 1 sells ticket:91
thread 2 sells ticket:90
...
...
...
thread 4 sells ticket:10
thread 1 sells ticket:9
thread 2 sells ticket:8
thread 3 sells ticket:7
thread 4 sells ticket:6
thread 1 sells ticket:5
thread 2 sells ticket:4
thread 3 sells ticket:3
thread 4 sells ticket:2
thread 1 sells ticket:1
- NOTE:1、锁尽量只保护对临界资源的访问操作;
- 2、在任意有可能退出线程的地方退出前都要解锁。
- 死锁:多个线程对锁资源进行争抢加锁,但是因为推进顺序不当而造成互相等待,导致流程无法继续推进的情况。
死锁产生的必要条件:
- 互斥条件;
- 不可剥夺条件;
- 请求与保持条件;
- 环路等待条件。
- 预防死锁:破坏死锁产生的必要条件。
- 避免死锁:死锁检测算法/银行家算法。
- 同步的实现:条件变量-->线程在满足资源访问条件的时候才能去访问资源,否则就挂起线程;直到满足条件之后在唤醒线程。
- 条件变量:向外提供了一个使线程等待和唤醒线程的接口+pcb等待队列。因为条件变量只提供了使线程等待和唤醒的接口,因此什么时候让线程等待/唤醒就需要程序员在进程中进行判断。
- 操作代码:
- 定义条件变量: pthread_cond_t;
- 初始化条件变量:pthread_cond_init(pthread_cond_t*, pthread_condattr_t*);/ PTHREAD_COND_INITIALIZER;
- 使线程挂起休眠的接口(条件变量需要搭配互斥锁一起使用,判断条件是否满足的条件本身就是一个临界资源,需要被保护):pthread_cond_wait(pthread_cond_t* , pthread_mutex_t *);--->一直等待别人的唤醒,pthread_cond_timedwait(pthread_cond_t*,pthread_mutex_t*,struct timespec);--->等待指定时间内都没有被唤醒则自动醒来;
- 唤醒线程的接口:pthread_cond_signal(pthread_cond_t*);-->唤醒至少一个等待的线程;pthread_cond_broadcast(pthread_cond_t*);---->唤醒所有等待的线程;
- 销毁条件变量:pthread_cond_destroy(pthread_cond_t*);
Demo:
NOTE:1、条件变量使用中对条件的判断应该使用while循环;2、多种角色线程应该使用多个条件变量。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>
int food = 0;//默认0表示没有食物
pthread_cond_t cook_cond; // 实现线程间对food变量访问的同步操作
pthread_cond_t customer_cond; // 实现线程间对food变量访问的同步操作
pthread_mutex_t mutex; // 保护food变量的访问操作
void *thr_cook(void *arg)
{
while (1)
{
//加锁
pthread_mutex_lock(&mutex);
while (food != 0)
{
//表示有饭,不满足做饭的条件
//让厨师线程等待,等待之前先解锁,被唤醒之后再加锁
//pthread_cond_wait接口中就完成了解锁,休眠,被唤醒后加锁三部操作
//并且解锁和休眠操作是一步完成,保证原子操作
pthread_cond_wait(&cook_cond, &mutex);
}
food = 1; //能够走下来表示没饭food==0, 则做餐,将food修改为1
printf("The chef made the food.\n");
//唤醒顾客吃饭
pthread_cond_signal(&customer_cond);
//解锁
pthread_mutex_unlock(&mutex);
}
return NULL;
}
void *thr_customer(void *arg)
{
while (1)
{
//加锁
pthread_mutex_lock(&mutex);
while (food != 1)
{
//没有饭则等待,等待前先解锁,被唤醒后加锁
pthread_cond_wait(&customer_cond, &mutex);
}
food = 0; // 能够走下来表示有饭food==1, 吃完饭,将food修改为0
printf("Customers have run out of meals.\n");
//唤醒厨师做饭
pthread_cond_signal(&cook_cond);
//解锁
pthread_mutex_unlock(&mutex);
}
return NULL;
}
int main()
{
pthread_t cook_tid[4], customer_tid[4];
int ret, i;
pthread_mutex_init(&mutex, NULL);
pthread_cond_init(&cook_cond, NULL);
pthread_cond_init(&customer_cond, NULL);
for (i = 0; i < 4; i++) {
ret = pthread_create(&cook_tid[i], NULL, thr_cook, NULL);
if (ret != 0) {
printf("pthread_create error\n");
return -1;
}
ret = pthread_create(&customer_tid[i], NULL, thr_customer, NULL);
if (ret != 0) {
printf("pthread_create error\n");
return -1;
}
}
pthread_join(cook_tid[0], NULL);
pthread_join(customer_tid[0], NULL);
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&cook_cond);
pthread_cond_destroy(&customer_cond);
return 0;
}
运行结果
- 生产者与消费者模型
-
典型场景:任务处理中既有数据产出,又有数据处理的场景
-
优点:解耦和,支持忙闲不均,支持并发
-
实现:一个场所(线程安全的缓冲区),两种角色(生产者与消费者),三种关系(实现线程安全)
-
信号量:用于实现进程/线程间同步与互斥(主要用于实现同步),本质是一个计数器+pcb等待队列。
-
同步的实现:通过自身的计数器对资源进行计数,判断进程/线程是否符合访问资源的条件;若符合则访问,若不符合则调用提供的接口使进程/线程阻塞;其他进程/线程促使条件满足之后,唤醒pcb等待队列上的pcb。
-
互斥的实现:保证计数器的计数不大于1(保证资源只有一个),同一时间只有一个进程/线程能访问资源来实现互斥。
-
代码操作:
-
定义信号量:sem_t;
-
初始化信号量:int sem_init(sem_t* sem, int pshared,unsigned int value); 这里,sem:定义的信号量变量;pshared:0->用于线程间/非0->用于进程间;value:初始化信号量的初值->初始资源数量有多少计数就是多少;返回值:成功返回0,失败返回-1;
-
在访问临界资源前,先访问信号量,判断是否能够访问:计数-1; int sem_wait(sem_t* sem); ->通过自身计数判断是否满足访问条件,不满足则一直阻塞线程/进程; int sem_trywait(sem_t* sem); -> 通过自身计数判断是否满足访问条件,不满足则立即报错返回; int sem_timedwait(sem_t* sem,const struct timespec* abs_timeout); ->不满足则等待指定时间,超时后报错返回-ETIMEDOUT;
-
促使访问条件满足+1,唤醒阻塞线程/进程 int sem_post(sem_t* sem); -> 通过信号量唤醒自己阻塞队列上的pcb
-
销毁信号量 int sem_destroy(sem_t* sem);
通过信号量实现一个生产者与消费者模型->线程安全的阻塞队列
#include <iostream>
#include <vector>
#include <pthread.h>
#include <semaphore.h>
#include<cstdio>
#define QUEUE_MAX 5
class RingQueue
{
public:
RingQueue(int maxq = QUEUE_MAX) :_queue(maxq), _capacity(maxq),
_step_read(0), _step_write(0)
{
//sem_init(信号量, 进程/线程标志, 信号量初值)
sem_init(&_lock, 0, 1);//实现互斥锁
sem_init(&_sem_data, 0, 0);//数据空间计数初始为0
sem_init(&_sem_idle, 0, maxq);//空闲空间计数初始为节点个数
}
~RingQueue()
{
sem_destroy(&_lock);
sem_destroy(&_sem_data);
sem_destroy(&_sem_idle);
}
bool Push(int data)
{
//1. 判断是否能够访问资源,不能访问则阻塞
sem_wait(&_sem_idle);//空闲空间计数的判断,空闲空间计数-1
//2. 能访问则加锁,保护访问过程
sem_wait(&_lock);//lock计数不大于1,当前若可以访问则-1,别人就不能访问
//3. 访问资源
_queue[_step_write] = data;
_step_write = (_step_write + 1) % _capacity;//走到最后,从头开始
//4. 解锁
sem_post(&_lock);//lock计数+1,唤醒其它因为加锁阻塞的线程
//5. 入队数据之后,数据空间计数+1,唤醒消费者
sem_post(&_sem_data);
return true;
}
bool Pop(int *data)
{
sem_wait(&_sem_data);//是否有数据
sem_wait(&_lock); // 有数据则加锁保护访问数据的过程
*data = _queue[_step_read]; //获取数据
_step_read = (_step_read + 1) % _capacity;
sem_post(&_lock); // 解锁操作
sem_post(&_sem_idle);//取出数据则空闲空间计数+1,唤醒生产者
return true;
}
private:
std::vector<int> _queue; // 数组, vector需要初始化节点数量
int _capacity; // 队列容量
int _step_read; // 读取数据的位置下标
int _step_write;//写入数据的位置下标
//这个信号量用于实现互斥
sem_t _lock;
//这个信号量用于对空闲空间进行计数
//对于生产者来空闲空间计数>0的时候才能写数据 --- 初始为节点个数
sem_t _sem_idle;
// 这个信号量用于对具有数据的空间进行计数
// 对于消费者来说有数据的空间计数>0的时候才能取出数据 -- 初始为0
sem_t _sem_data;
};
void *thr_productor(void *arg)
{
//这个参数是主线程传递过来的数据
RingQueue *queue = (RingQueue*)arg;//类型强转
int i = 0;
while (1)
{
//生产者不断生产数据
queue->Push(i);//通过Push接口操作queue中的成员变量
printf("productor push data:%d\n", i++);
}
return NULL;
}
void *thr_customer(void *arg)
{
RingQueue *queue = (RingQueue*)arg;
while (1)
{
//消费者不断获取数据进行处理
int data;
queue->Pop(&data);
printf("customer pop data:%d\n", data);
}
return NULL;
}
int main()
{
int ret, i;
pthread_t ptid[4], ctid[4];
RingQueue queue;
for (i = 0; i < 4; i++)
{
ret = pthread_create(&ptid[i], NULL, thr_productor, (void*)&queue);
if (ret != 0)
{
printf("create productor thread error\n");
return -1;
}
ret = pthread_create(&ctid[i], NULL, thr_customer, (void*)&queue);
if (ret != 0)
{
printf("create productor thread error\n");
return -1;
}
}
for (i = 0; i < 4; i++)
{
pthread_join(ptid[i], NULL);
pthread_join(ctid[i], NULL);
}
return 0;
}
信号量和条件变量
- 信号量可以实现互斥量,大部分情况下也可以实现条件变量。甚至使用信号量的实现远比其他实现更容易理解。 然而很多时候使用信号量替换条件变量的可能会降低并发程序的性能。 也就是说, 虽然条件变量使用和理解上复杂, 但是它的并发性高于信号量.
信号量和互斥锁区别
- 1. 互斥量用于线程的互斥,信号量用于线程的同步。这是互斥量和信号量的根本区别,也就是互斥和同步之间的区别。
- 2. 互斥量值只能为0/1,信号量值可以为非负整数。
- 3. 互斥量的加锁和解锁必须由同一线程分别对应使用,信号量可以由一个线程释放,另一个线程得到。