目录
一、Linux线程介绍
进程与线程
典型的UNIX/Linux进程可以看成只有一个控制线程:一个进程在同一时刻只做一件事情。有了多个控制线程后,在程序设计时可以把进程设计成在同一时刻做不止一件事,每个线程各自处理独立的任务。
所谓线程,就是操作系统所能调度的最小单位。普通的进程,只有一个线程在执行对应的逻辑。我们可以通过多线程编程,使一个进程可以去执行多个不同的任务。相比多进程编程而言,线程享有共享资源,即在进程中出现的全局变量, 每个线程都可以去访问它,与进程共享“4G”内存空间,使得系统资源消耗减少。
进程——资源分配的最小单位,线程——程序执行的最小单位
进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些要求同时进行并且又要共享某些变量的并发操作,通常会选择使用线程而不是进程。这是因为线程之间共享同一进程的内存空间,因此它们可以轻松地访问和修改共享变量,而无需复杂的通信机制。
使用线程的理由
从上面我们知道了进程与线程的区别,其实这些区别也就是我们使用线程的理由。总的来说就是:进程有独立的地址空间,线程没有单独的地址空间(同一进程内的线程共享进程的地址空间)。
使用多线程的理由之一是和进程相比,它是一种非常"节俭"的多任务操作方式。我们知道,在Linux系统下,启动一个新的进程必须分配给它独立的地址空间,建立众多的数据表来维护它的代码段、堆栈段和数据段,这是一种"昂贵"的多任务工作方式。而运行于一个进程中的多个线程,它们彼此之间使用相同的地址空间,共享大部分数据,启动一个线程所花费的空间远远小于启动一个进程所花费的空间,而且,线程间彼此切换所需的时间也远远小于进程间切换所需要的时间。据统计,总的说来,一个进程的开销大约是一个线程开销的30倍左右,当然,在具体的系统上,这个数据可能会有较大的区别。
使用多线程的理由之二是线程间方便的通信机制。对不同进程来说,它们具有独立的数据空间,要进行数据的传递只能通过通信的方式进行,这种方式不仅费时,而且很不方便。线程则不然,由于同一进程下的线程之间共享数据空间,所以一个线程的数据可以直接为其它线程所用,这不仅快捷,而且方便。当然,数据的共享也带来其他一些问题,有的变量不能同时被两个线程所修改,有的子程序中声明为static的数据更有可能给多线程程序带来灾难性的打击,这些正是编写多线程程序时最需要注意的地方。
二、线程的创建等待及退出
线程的创建
#include <pthread.h>
// 返回:若成功返回0,否则返回错误编号
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg);
⚫ 该函数第一个参数为 pthread_t 指针,用来保存新建线程的线程号
⚫ 第二个参数表示了线程的属性,一般传入 NULL 表示默认属性
⚫ 第三个参数是一个函数指针,就是线程执行的函数。这个函数返回值为 void*, 形参为 void*
⚫ 第四个参数则表示为向线程处理函数传入的参数,若不传入,可用 NULL 填充
线程的退出
单个线程可以通过以下三种方式退出,在不终止整个进程的情况下停止它的控制流:
1)线程只是从启动例程中返回,返回值是线程的退出码。
2)线程可以被同一进程中的其他线程取消。
3)线程调用pthread_exit:
#include <pthread.h>
int pthread_exit(void *rval_ptr);
在退出时候可以传递一个 void*类型的数据带给主线程,若选择不传出数据,可将参数填充为 NULL。
线程的等待
#include <pthread.h>
//返回:若成功返回0,否则返回错误编号
int pthread_join(pthread_t thread, void **rval_ptr);
该函数为线程回收函数,默认状态为阻塞状态,直到成功回收线程后才返回。第一个参数为要回收线程的ID号,第二个参数为线程回收后接受线程传出的数据。如果对线程的返回值不感兴趣,可以把 rval_ptr 置为NULL。在这种情况下,调用pthread_join函数将等待指定的线程终止,但并不获得线程的终止状态。
代码示例
以下的代码会结合上面三个函数,看看这些函数是怎么用的
#include <stdio.h>
#include <pthread.h>
void *func1(void *arg)//用户自定义的线程功能函数
{
static int ret = 10;//传出数据前面一定要加static!!!
printf("t1:%lu thread is creat\n",(unsigned long)pthread_self());//打印t1线程的ID号
printf("t1:param is %d\n",*((int *)arg));//先将arg转换为int *型,再用*号取数据
pthread_exit((void *)&ret);
}
int main()
{
int ret;
int param = 100;//传入线程的参数
pthread_t t1;//用于存放线程的ID号
int *pret;//用于存放线程退出时传出的数据
ret = pthread_create(&t1, NULL, func1, (void *)¶m);
if(ret == 0)//创建成功
{
printf("main:creat t1 success\n");
}
printf("main:%lu\n",(unsigned long)pthread_self());//打印主线程的ID号
pthread_join(t1,(void **)&pret);//等待t1线程退出
printf("t1 quit ret=%d\n",*pret);//打印t1线程退出时传出的数据
return 0;
}
运行结果:
注意:因采用 POSIX 线程接口,故在要编译的时候包含 pthread 库,使用 gcc 编译应 gcc xxx.c -lpthread 方可编译多线程程序。
代码示例中有一个上面没讲到的函数:
//获取线程号
#include <pthread.h>
pthread_t pthread_self(void);
//成功:返回线程号
这些函数主要难点就是数据类型的转换以及一些指针的操作,只要把代码示例里面看懂就不是很难理解了,main函数创建线程后,主线程和被创建的线程会同时执行,当main函数执行到join函数时会阻塞,等待t1线程退出。
三、互斥锁
简述
多个线程都要访问某个临界资源,比如某个全局变量时,需要互斥地访问: 我访问时,你不能访问。
初始化互斥量
#include <pthread.h>
int pthread_mutex_init(phtread_mutex_t *mutex, const pthread_mutexattr_t *restrict attr);
该函数初始化一个互斥量,第一个参数是该互斥量指针,第二个参数为控制互斥量的属性,一般为 NULL 。当函数成功后会返回 0,代表初始化互斥量成功。
互斥量加锁/解锁
//互斥量加锁(阻塞)/解锁
#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
//成功:返回 0
lock 函数与 unlock 函数分别为加锁解锁函数,只需要传入已经初始化好的 pthread_mutex_t 互斥量指针。成功后会返回 0 。当某一个线程获得了执行权后,执行 lock 函数,一旦加锁成功后,其余线程遇到 lock 函数时候会发生阻塞,直至获取资源的线程执行 unlock 函数后。unlock 函数会唤醒其他正在等待互斥量的线程。
销毁互斥量
//互斥量销毁
#include <pthread.h>
int pthread_mutex_destory(pthread_mutex_t *mutex);
//成功:返回 0
该函数是用于销毁互斥量的,传入互斥量的指针,就可以完成互斥量的销毁,成功返回 0。
代码示例
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
pthread_mutex_t mutex;//互斥量变量 一般申请全局变量
int Num = 0;//公共临界变量
void *fun1(void *arg)
{
pthread_mutex_lock(&mutex);//加锁 若有线程获得锁,则会阻塞
while(Num < 3)
{
Num++;
printf("fun1:Num = %d\n",Num);
sleep(1);
}
pthread_mutex_unlock(&mutex);//解锁
pthread_exit(NULL);//线程退出 pthread_join 会回收资源
}
void *fun2(void *arg)
{
pthread_mutex_lock(&mutex);//加锁 若有线程获得锁,则会阻塞
while(Num > -3)
{
Num--;
printf("fun2:Num = %d\n",Num);
sleep(1);
}
pthread_mutex_unlock(&mutex);//解锁
pthread_exit(NULL);//线程退出 pthread_join 会回收资源
}
int main()
{
pthread_t tid1,tid2;
pthread_mutex_init(&mutex,NULL);//初始化互斥量
pthread_create(&tid1,NULL,fun1,NULL);//创建线程 1
pthread_create(&tid2,NULL,fun2,NULL);//创建线程 2
pthread_join(tid1,NULL);//阻塞回收线程 1
pthread_join(tid2,NULL);//阻塞回收线程 2
pthread_mutex_destroy(&mutex);//销毁互斥量
return 0;
}
运行结果:
如果不使用互斥锁,线程1和2会同时执行,Num加加减减,谁也不知道最后的结果,有可能永远无法跳出while循环,但加上了互斥锁,保证一方执行完后才轮到另一方执行。
条件变量
简述
条件变量是一种同步机制,用来通知其他线程条件满足了。一般是用来通知对方共享数据的状态信息,因此条件变量是结合互斥量来使用的。条件变量给多个线程提供了一个会合的场所。条件变量与互斥量一起使用时,允许线程以无竞争的方式等待特定的条件发生。(条件变量有什么用看了相关函数后就大概知道了)
创建和销毁条件变量
#include <pthread.h>
// 初始化条件变量
int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr);
//cond_attr 通常为 NULL
// 销毁条件变量
int pthread_cond_destroy(pthread_cond_t *cond);
//这些函数成功时都返回 0
函数的使用方法和互斥锁差不多,要使用前先创建,最后再销毁。
等待条件变量
#include <pthread.h>
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
这需要结合互斥量一起使用,示例代码如下:
pthread_mutex_lock(&g_tMutex);
// 如果条件不满足则,会 unlock g_tMutex
// 条件满足后被唤醒,会 lock g_tMutex
pthread_cond_wait(&g_tConVar, &g_tMutex);
/* 操作临界资源 */
pthread_mutex_unlock(&g_tMutex);
这里的条件不满足表示wait函数没有被唤醒,这涉及到下一个函数,先看上面的代码:线程先lock(持有互斥锁),这时其他线程如果也想获得这个锁就会被阻塞,该线程代码继续执行到pthread_cond_wait 函数时,该线程会释放锁,并进入阻塞状态等待被其他线程唤醒;等其他线程调用唤醒函数后,该线程才会执行wait函数之后的代码,此时该线程持有互斥锁,最后该线程主动释放互斥锁。(一般流程,理清思路就知道条件变量是干嘛用的了)
通知条件变量
#include <pthread.h>
int pthread_cond_signal(pthread_cond_t *cond);
pthread_cond_signal 函数只会唤醒一个等待 cond 条件变量的线程。
条件变量同样需要初始化一个 pthread_cond_t 类型的全局变量,函数的使用方法和互斥锁类似,毕竟它要和互斥锁搭配着使用,所以这里就不给出代码示例了,只要清除调用函数时,线程进入了什么状态,那么不管是互斥锁还是条件变量都不是什么难题了。
补充(什么情况下会造成死锁?)
死锁是多线程或多进程并发编程中常见的问题,它发生在一组线程或进程互相等待彼此持有的资源而无法继续执行的情况。
互斥锁使用不当: 如果多个线程在持有某些资源(如互斥锁)的情况下,又试图获取其他线程已经持有的资源,可能会导致死锁。例如,线程 A 持有锁 L1 并请求锁 L2,而线程 B 持有锁 L2 并请求锁 L1,这种情况可能导致死锁。所以我们编写程序时要注意,不要写成死锁的情况,等以后写一些大型的代码,需要同时操控几个互斥量时,可能会犯这种错误。