1 线程简介
1.1 进程与线程区别
-
进程是资源(CPU、内存等)分配的基本单位,它是程序执行时的一个实例。线程是一条执行路径,是程序执行时的最小单位,它是进程的一个执行流,是CPU调度和分派的基本单位,一个进程可以由很多个线程组成,线程间共享进程的所有资源,每个线程有自己的堆栈和局部变量。线程由CPU独立调度执行,在多CPU环境下就允许多个线程同时运行。同样多线程也可以实现并发操作,对于单核CPU而言:多线程就是一个CPU在来回的切换,在交替执行。对于多核CPU而言,多线程就是同时有多条执行路径在同时(并行)执行,每个核执行一个线程,多个核就有可能是一块同时执行的。
-
一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程。
-
不同进程间数据很难共享,同一进程下不同线程间数据很易共享。
-
进程要比线程消耗更多的计算机资源,进程创建生成一块新的虚拟内存,而进程下的线程创建,不开辟新的内存,共享进程资源。
-
进程间不会相互影响,一个线程挂掉将导致整个进程挂掉。
ps -aux #查看进程 ps -Lf pid #进程号,查看进程pid中的线程
1.2 线程操作目的
多线程可以提高程序的效率。在cpu允许的范围内雇佣足够多的工人。实现并发编程:多个任务可以同时做,常用与任务之间比较独立,互不影响。
1.3 NPTL线程库
POSIX Thread Library (NPTL)是Linux内核运行使用POSIX线程标准写的程序。可通过以下命令查看:
getconf GNU_LIBPTHREAD_VERSION
1.4 线程共享与独有的资源
共享资源:
-
同一块地址空间
-
文件描述符表()
-
每种信号的处理方式(如:SIG_DFL,SIG_IGN或者自定义的信号优先级)
-
当前工作目录
-
用户(进程)id和组id
独有:
-
线程会产生临时变量,临时变量保存再栈上,所以每个线程都有自己的私有栈结构
-
每个线程都有私有的上下文信息。
-
线程ID
-
一组寄存器的值
-
errno 变量
-
信号屏蔽字以及调度优先级
2 线程的控制
2.1 线程创建
/*
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void*), void *arg);
功能:创建一个新的线程
参数
thread: 传出参数,返回线程ID
attr: 设置线程的属性,attr为NULL表⽰示使⽤用默认属性
start_routine: 是个函数地址,线程启动后要执⾏的函数(函数指针)
arg: 传给线程启动函数的参数
返回值:成功返回0;失败返回错误码
#include <pthread.h>
void pthread_exit(void *retval);
功能:终止线程
参数
retval:是 void* 类型的指针,可以指向任何类型的数据,它指向的数据将作为线程退出时的返回值。默认 NULL 。
返回值:无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(自身)
#include <pthread.h>
pthread_t pthread_self(void);
//获取用户态线程id
*/
#include <stdio.h>
#include <pthread.h>
#include <string.h>
void * call(void *arg) {
printf("child thread: %ld\n",pthread_self());
printf("arg : %d\n",*(int *)arg);
return NULL;
}
int main(){
pthread_t tid;
int #include <stdio.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
void * call(void *arg) {
printf("child thread: %ld\n",pthread_self());
printf("arg : %d\n",*(int *)arg);
sleep(3);
return NULL;
}
int main(){
pthread_t tid;
int num =10;
// 创建子线程,这里main函数为主线程
int ret = pthread_create(&tid, NULL, call,(void *)&num); //
if(ret !=0 ){
// failed to create
char *error = strerror(ret);
printf("error: %s\n",error);
}
// 主线程
printf("main thread: %ld\n",pthread_self());
for(int i =0; i< 5; i++){
printf("main- %d\n",i);
}
//主线程回收子线程资源,3s后回收
int ret1 = pthread_join(tid, NULL);
if(ret1 !=0 ){
// failed to create
char *error = strerror(ret1);
printf("error: %s\n",error);
}
// 退出主线程,且不影响子线程,否则子线程来不及
// 运行,就return 0,程序结束。
pthread_exit(NULL);
// 此时主线程退出后的操作不会运行,如
printf("main thread!\n");
return 0;
}num =10;
// 创建子线程,这里main函数为主线程
int ret = pthread_create(&tid, NULL, call,(void *)&num); //
if(ret !=0 ){
// failed to create
char *error = strerror(ret);
printf("error: %s\n",error);
}
// 主线程
printf("main thread: %ld\n",pthread_self());
for(int i =0; i< 5; i++){
printf("main- %d\n",i);
}
// 退出主线程,且不影响子线程,否则子线程来不及
// 运行,就return 0,程序结束。
pthread_exit(NULL);
// 此时主线程退出后的操作不会运行,如
printf("main thread!\n");
return 0;
}
2.2 线程回收
线程运行结束,回收子线程资源。任何线程都能其他线程资源。
/*
#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
功能:链接终止线程,回收子线程资源。
thread: 线程ID
value_ptr: 它指向一个指针,后者指向线程的返回值
返回值:成功返回0;失败返回错误码
如果新线程创建后,不用pthread_join()等待回收新线程,
那么就会造成内存泄漏,但是当等待新线程时,主线程就会一直阻塞,
影响主线程处理其他链接要求,这时候就需要一种办法让新线程退出后,
自己释放所有资源,因此产生了线程分离。
#include <pthread.h>
int pthread_detach(pthread_t thread);
返回值:成功返回0;失败返回错误码
*/
#include <stdio.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
void * call(void *arg) {
printf("child thread: %ld\n",pthread_self());
printf("arg : %d\n",*(int *)arg);
sleep(3);
return NULL;
}
int main(){
pthread_t tid;
int num =10;
// 创建子线程,这里main函数为主线程
int ret = pthread_create(&tid, NULL, call,(void *)&num); //
if(ret !=0 ){
// failed to create
char *error = strerror(ret);
printf("error: %s\n",error);
}
// 主线程
printf("main thread: %ld\n",pthread_self());
for(int i =0; i< 5; i++){
printf("main- %d\n",i);
}
//主线程回收子线程资源,3s后回收
// int ret1 = pthread_detach(tid);
int ret1 = pthread_join(tid, NULL);
if(ret1 !=0 ){
// failed to create
char *error = strerror(ret1);
printf("error: %s\n",error);
}
printf("回收成功!\n");
// 退出主线程,且不影响子线程,否则子线程来不及
// 运行,就return 0,程序结束。
pthread_exit(NULL);
// 此时主线程退出后的操作不会运行,如
printf("main thread!\n");
return 0;
}
3 线程通信
与进程的通信机制(管道、匿名管道、消息队列、信号量、共享内存、内存映射以及socket等)相比,线程间通信要简单的多。
因为同一进程的不同线程共享同一份全局内存区域,其中包括初始化数据段、未初始化数据段,以及堆内存段,所以线程之间可以方便、快速地共享信息。只需要将数据复制到共享(全局或堆)变量中即可。不过,要避免出现多个线程试图同时修改同一份信息(临界区)。
线程安全就是说多线程访问同一段代码不会产生不确定的结果。编写线程安全的代码依靠 线程同步。如果变量时只读的,多个线程同时读取该变量不会有一致性问题,但是,当一个线程可以修改的变量,其他线程也可以读取或者修改的时候,我们就需要对这些线程进行同步,确保它们在访问变量的存储内容时不会访问到无效的值。
3.1 互斥锁
互斥量本质上说是一把锁,在访问共享资源前对互斥量进行加锁,在访问完成后释放互斥量。对互斥量进行枷锁以后,其他视图再次对互斥量加锁的线程都会被阻塞直到当前线程释放该互斥锁。
/*
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_unlock(pthread_mutex_t *mutex);//解锁互斥
int pthread_mutex_trylock(pthread_mutex_t *mutex);//销毁互斥
*/
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
int tickets = 1000;
pthread_mutex_t mutex;
void *sellticket(void *){
while(1){
//add lock
pthread_mutex_lock(&mutex);
//临界区
if(tickets>0){
usleep(6000);
printf("thread: %ld, tickets: %d \n",pthread_self(), tickets);
tickets--;
}else{
pthread_mutex_unlock(&mutex);
break;
}
pthread_mutex_unlock(&mutex);
}
return NULL;
}
int main(){
//init mutex
pthread_mutex_init(&mutex,NULL);
pthread_t tid1,tid2,tid3;
//创建三个线程
pthread_create(&tid1, NULL,sellticket,NULL);
pthread_create(&tid2, NULL,sellticket,NULL);
pthread_create(&tid3, NULL,sellticket,NULL);
// 回收线程资源
pthread_join(tid1,NULL);
pthread_join(tid2,NULL);
pthread_join(tid3,NULL);
/*
pthread_detach(tid1);
pthread_detach(tid2);
pthread_detach(tid3);
*/
pthread_exit(NULL);
//destroy();
pthread_mutex_destroy(&mutex);
return 0;
}
死锁:是指两个或两个以上的进程(线程)在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。
-
忘记释放锁
-
重复加锁
-
多线程多锁,抢占锁资源。
死锁 & 饥饿:
-
死锁针对多个进(线)程,所有进(线)程处于无限等待状态,程序卡住,死锁一定发生了循环等待。
-
饥饿指某个(些)进(线)程处于忙等或就绪态,由资源分配不公引起,一些进(线)程持续得不到资源。
死锁条件:
(1) 互斥条件:一个资源每次只能被一个进程(线程)使用。 (2) 请求与保持条件:一个进程(线程)因请求资源而阻塞时,对已获得的资源保持不放。 (3) 不剥夺条件 : 此进程(线程)已获得的资源,在末使用完之前,不能强行剥夺。 (4) 循环等待条件 : 多个进程(线程)之间形成一种头尾相接的循环等待资源关系。
死锁处理:
-
预防:静态,事前加约束。
-
避免:动态,实时检测,拒绝不安全的资源请求命令。
-
检测:定时检测,利用率低检测
-
回复:终止,重启,回退,剥夺资源。
3.2 读写锁
写锁与互斥量类似,不过读写锁拥有更高的并行性。读写锁有3种状态:读模式下加锁状态,写模式下加锁状态,不加锁状态。一次只有一个线程可以占有写模式的读写锁,但是多个线程可以同时占有读模式的读写锁。
/*
int pthread_rwlock_init(pthread_rwlock_t *rwlock,
const pthread_rwlockattr_t *rwlockattr);//初始化读写锁
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);//销毁读写锁
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);//读模式锁定读写锁
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);//写模式锁定读写锁
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);//解锁读写锁
3-不定时写线程,独占资源。 5-不定时读线程,可多个共享资源。
*/
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
int num =1;
pthread_rwlock_t rw_lock;
void* write_num(void *arg) {
while(1){
pthread_rwlock_wrlock(&rw_lock);
num++;
printf("writter: %ld, num++: %d\n",pthread_self(),num);
pthread_rwlock_unlock(&rw_lock);
usleep(100);
}
}
void* read_num(void *arg) {
while(1){
pthread_rwlock_rdlock(&rw_lock);
printf("reader: %ld, num: %d\n",pthread_self(),num);
pthread_rwlock_unlock(&rw_lock);
usleep(100);
}
}
int main(){
//init
pthread_rwlock_init(&rw_lock,NULL);
//creat threads
pthread_t wt_tids[3];
pthread_t rd_tids[5];
for(int i =0; i < 3;i++){
pthread_create(&wt_tids[i],NULL,write_num,NULL);
}
for(int i =0; i < 5;i++){
pthread_create(&rd_tids[i],NULL,read_num,NULL);
}
// detach threads
for(int i = 0; i <3;i++){
pthread_detach(wt_tids[i]);
}
for(int i = 0; i <5;i++){
pthread_detach(rd_tids[i]);
}
pthread_exit(NULL);
pthread_rwlock_destroy(&rw_lock);
return 0;
}
3.3 条件变量
条件变量是线程可用的另一种同步机制。互斥量用于上锁,条件变量则用于等待,并且条件变量总是需要与互斥量一起使用,运行线程以无竞争的方式等待特定的条件发生。互斥锁一个明显的缺点是他只有两种状态: 锁定和非锁定。而条件变量通过允许线程阻塞和等待另一个线程发送信号的方法弥补了互斥锁的不足。当条件满足的时候,线程通常解锁并等待该条件发生变化,一旦另一个线程修改了环境变量,就会通知相应的环境变量唤醒一个或者多个被这个条件变量阻塞的线程。这些被唤醒的线程将重新上锁,并测试条件是否满足。
使用 pthread_cond_wait前要先加锁;
pthread_cond_wait内部会解锁,然后等待条件变量被其它线程激活;
pthread_cond_wait被激活后会再自动加锁;
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
pthread_mutex_t count_lock;
pthread_cond_t count_dw;
pthread_cond_t count_up;
unsigned count = 1;
/*
生产者:>=10, wait
容器:资源上限-10
消费者:<=0, wait
*/
// 消费者
void *decrement_count(void *arg)
{ while (1){
usleep(10);
pthread_mutex_lock(&count_lock);
//仓库资源用完,消费者等待。
while(count == 0)
{
printf("仓库资源用完,消费者等待: \n");
pthread_cond_wait(&count_dw, &count_lock);
printf("消费者等待结束! \n");
}
//仓库有剩余空间,通知生产者
if(count == 10){
pthread_cond_signal(&count_up);
}
count --;
printf("消费后: %d\n",count);
pthread_mutex_unlock(&count_lock);
}
return NULL;
}
// 生产者
void *increment_count(void *arg)
{ while(1){
usleep(10);
pthread_mutex_lock(&count_lock);
// 仓库满了,生产者等待
if(count == 10)
{
printf("仓库满了,生产者等待 \n");
pthread_cond_wait(&count_up, &count_lock);
printf("生产者等结束! \n");
}
// 通知消费者,后面资源加一,有存货
if(count == 0){
pthread_cond_signal(&count_dw);
}
count ++;
printf("生产后: %d\n",count);
pthread_mutex_unlock(&count_lock);
}
return NULL;
}
int main(void)
{
pthread_t tid1, tid2;
pthread_mutex_init(&count_lock, NULL);
pthread_cond_init(&count_dw, NULL);
pthread_cond_init(&count_up, NULL);
pthread_create(&tid1, NULL, decrement_count, NULL);
sleep(2);
pthread_create(&tid2, NULL, increment_count, NULL);
//
pthread_detach(tid1);
pthread_detach(tid2);
pthread_exit(NULL);
return 0;
}
3.4 信号量
线程的信号和进程的信号量类似,使用线程的信号量可以高效地完成基于线程的资源计数。信号量实际上是一个非负的整数计数器,用来实现对公共资源的控制。在公共资源增加的时候,信号量就增加;公共资源减少的时候,信号量就减少;只有当信号量的值大于0的时候,才能访问信号量所代表的公共资源。
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
#include <unistd.h>
/*
int sem_init(sem_t *sem, int pshared, unsigned int value);
-sem是要初始化的信号量,
-pshared表示此信号量是在进程间共享还是线程间共享,
-value是信号量的初始值。
int sem_destroy(sem_t *sem);-
-其中sem是要销毁的信号量。
int sem_wait(sem_t *sem);
-等待信号量,如果信号量的值大于0,将信号量的值减1,立即返回。
如果信号量的值为0,则线程阻塞。相当于P操作。成功返回0,失败返回-1。
int sem_post(sem_t *sem);
-释放信号量,让信号量的值加1。相当于V操作。
生产者:>=10, wait
容器:资源上限-10
消费者:<=0, wait
两个信号量:
sem_t psem,csem;
init(psem,0,8);
init(csem,0,0);
producer(){
sem_wait(&psem);//-1
sempost(&csem);//+1,通知消费者
}
customer(){
sem_wait(&csem);//-1
sempost(&psem);//+1,通知生产者
}
*/
pthread_mutex_t count_lock;
sem_t psem;
sem_t csem;
int num =0;
// 消费者
void *customer(void *arg)
{ while (1){
pthread_mutex_lock(&count_lock);
sem_wait(&csem);//资源=-1,若为0,等待。
printf("消费后: %d\n",--num);
pthread_mutex_unlock(&count_lock);
sem_post(&psem);
//usleep(10);
}
return NULL;
}
// 生产者
void *producer(void *arg)
{ while(1){
pthread_mutex_lock(&count_lock);
sem_wait(&psem);//剩余资源空间-1,对应psem+1
printf("生产后: %d\n",++num);
pthread_mutex_unlock(&count_lock);
sem_post(&csem); //消费者资源加一
//usleep(10);
}
return NULL;
}
// 该程序存在死锁
int main(void)
{
pthread_t tid1, tid2;
pthread_mutex_init(&count_lock, NULL);
sem_init(&psem,0,10);// 容器10
sem_init(&csem,0,0);
pthread_create(&tid1, NULL, producer, NULL);
pthread_create(&tid2, NULL, customer, NULL);
//
pthread_detach(tid1);
pthread_detach(tid2);
//sleep(20);
pthread_exit(NULL);
pthread_mutex_destroy(&count_lock);
return 0;
}
note: 互斥锁保证的是数据安全问题。信号量作为线程通信。