1. 基本概念
1.1. 进程与线程的概念
典型的UNIX/linux进程可以看成是只有一个控制线程,一个进程在同一时刻只做一件事情,有了多个控制线程后,在程序设计时可以把进程设计成在同一时刻做不止一件事,每个线程各自处理独立的任务。
一个进程至少包含一个线程
进程 进程是程序执行时的一个实例,即它是程序已经执行到何种程度的数据结构的汇集。从内核的观点看,进程的目的就是担当分配系统资源(CPU时间、内存等)的基本单位。
前边说过由用户所编辑指令的c文件在没有运行起来的时候是一个程序,然后经过编译变成一个可执行文件(.exe / a.out),最终将可执行文件运行起来就变成了一个进程。进程本身占用系统资源,当启动了进程以后,在用户空间中会占用数据段、代码段和堆栈段。所以进程对资源开销比较大。
线程 线程是进程的一个执行流,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。一个进程由几个线程组成(拥有很多相对独立的执行流的用户程序共享应用程序的大部分数据结构),线程与同属一个进程的其他的线程共享进程所拥有的全部资源。
1.2. 进程与线程的区别
进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些要求同时进行并且又要共享某些变量的并发操作,最好用线程。
举一个例子:12306在节假日的期间访问人数变多,所有的用户查询车次等操作会向12306这个系统发送请求,那么服务器相应完这个请求会给用户返回一个界面。如果服务器端对每一个请求都启动一个新的进程去处理的话,那么这个系统的开销是很庞大的。而且进程间相互切换也同样比较耗费资源,如果在用户非常庞大的情况下,使用进程去处理大并发的请求,那么这个设计是十分不合理的,容易导致系统的瘫痪。那么就引入线程来处理这种大并发的请求。
1.3. 使用线程的好处:
使用多线程的理由之一是和进程相比,它是一种非常"节俭"的多任务操作方式。我们知道,在Linux系统下,启动一个新的进程必须分配给它独立的地址空间,建立众多的数据表来维护它的代码段、堆栈段和数据段,这是一种"昂贵"的多任务工作方式。而运行于一个进程中的多个线程,它们彼此之间使用相同的地址空间,共享大部分数据,启动一个线程所花费的空间远远小于启动一个进程所花费的空间,而且,线程间彼此切换所需的时间也远远小于进程间切换所需要的时间。据统计,总的说来,一个进程的开销大约是一个线程开销的30倍左右,当然,在具体的系统上,这个数据可能会有较大的区别。
使用多线程的理由之二是线程间方便的通信机制。对不同进程来说,它们具有独立的数据空间,要进行数据的传递只能通过通信的方式进行,这种方式不仅费时,而且很不方便。线程则不然,由于同一进程下的线程之间共享数据空间,所以一个线程的数据可以直接为其它线程所用,这不仅快捷,而且方便。当然,数据的共享也带来其他一些问题,有的变量不能同时被两个线程所修改,有的子程序中声明为static的数据更有可能给多线程程序带来灾难性的打击,这些正是编写多线程程序时最需要注意的地方。
除了以上所说的优点外,不和进程比较,多线程程序作为一种多任务、并发的工作方式,当然有以下的优点:
提高应用程序响应。这对图形界面的程序尤其有意义,当一个操作耗时很长时,整个系统都会等待这个操作,此时程序不会响应键盘、鼠标、菜单的操作,而使用多线程技术,将耗时长的操作(time consuming)置于一个新的线程,可以避免这种尴尬的情况。
使多CPU系统更加有效。操作系统会保证当线程数不大于CPU数目时,不同的线程运行于不同的CPU上。
改善程序结构。一个既长又复杂的进程可以考虑分为多个线程,成为几个独立或半独立的运行部分,这样的程序会利于理解和修改。
1.4. 线程的概念
要点:进程是资源管理的最小单位,线程是程序执行的最小单位。
举个例子:现在假如创建了一个word,然后对新建的word进行编辑,保存等操作。如果这些操作都去启动一个进程去做的话,那显然不太现实,资源开销太大。实际上在word编辑的过程中是启动了多个线程去处理这些操作,进程只有新建word的这一个进程。
由此可见:线程是属于进程的,一个进程默认至少有一个线程,后续有多少个进程取决于用户后续的操作。可以说调用进程就工作,就是在启动线程去工作的。一个线程就是进程里的一个工作流,一个进程可以有多个工作流,所以线程是程序执行的最小单位。
每个进程都有自己的数据段、代码段和堆栈段,线程隶属于某个进程,它包含独立的栈和CPU寄存器状态,但是它共享这个进程所有的用户空间,因为它本身属于这个进程。包括打开的文件、内存页面、信号标识及动态分配内存等。
对于线程的资源开销几乎可以忽略不计,可以减小进程上下文切换的开销。
2. 线程相关函数
多线程开发在linux平台上已经有成熟的 pthread 库支持。其涉及的多线程开发的最基本概念主要包含三点:线程,互斥锁,条件。
线程操作有:创建,退出,等待。
互斥锁操作有:创建,销毁,加锁和解锁。
条件操作有 :创建,销毁,触发,广播和等待。
其他的一些线程扩展概念,如信号灯等,都可以通过上面的三个基本元素的基本操作封装出来。详细请见下表:
2.1. 线程创建函数pthread_create()原型和头文件:
/*
Linux下 man pthread_create查看手册
*/
#include <pthread.h> //头文件
int pthread_create(pthread_t *restrict tidp, const pthread_attr_t *restrict attr,
void *(*start_rtn)(void *), void *restrict arg);
int 函数返回值,成功pthread_create()返回0;当出现错误时,它返回一个错误编号,并且*thread的内容是未定义的
tidp 指向线程标识符的指针
attr 用来设置线程属性,如果没有则填NULL。
start_rtn 表示线程运行函数的地址
arg 线程运行函数的参数,如果没有参数则填NULL
- 函数详解:
当 pthread_create成功返回时,由tidp指向的内存单元被设置为新创建线程的线程ID。attr参数用于定制各种不同的线程属性,暂可以把它设置为NULL,以创建默认属性的线程。
新创建的线程从start_rtn函数的地址开始运行,该函数只有一个无类型指针参数arg。如果需要向start_rtn函数传递的参数不止一个,那么需要把这些参数放到一个结构中,然后把这个结构的地址作为arg参数传入。
2.2. 线程创建函数pthread_create()用法案例:
#include <stdio.h>
#include <pthread.h>
/*
* 主线程创建一个新线程th1,执行func1函数。
* 在func1中,打印新线程的ID和传递的数据(100)。
* 主线程打印其自己的ID,然后等待th1结束。
*/
void* func1(void *arg)
{
printf("th1:%ld是新线程ID\n",(unsigned long)pthread_self()); //获取新线程的ID
printf("th1:data = %d\n",*((int *)arg)); // 转换成int data
}
int main()
{
int ret;
pthread_t th1; //线程标识符
int data = 100;
// int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
// void *(*start_routine) (void *), void *arg);
//创建了一个th1线程 //转换成无类型的地址
ret = pthread_create(&th1, NULL, func1, (void*)&data);
if(ret == 0){
printf("main:创建th1线程成功\n");
}
//获取主线程的ID
printf("main:线程ID = %ld\n",(unsigned long)pthread_self());
// int pthread_join(pthread_t thread, void **retval);
pthread_join(th1,NULL);
//等待th1线程终止,但是不获取th1线程终止状态
return 0;
}
2.3. 线程退出函数pthread_exit()原型和头文件:
单个线程可以通过以下三种方式退出,在不终止整个进程的情况下停止它的控制流:
- 线程只是从启动例程中返回,返回值是线程的退出码。
- 线程可以被同一进程中的其他线程取消。
- 线程调用pthread_exit。
/*
Linux下 man pthread_exit查看手册
*/
#include <pthread.h>
int pthread_exit(void *retval);
int 函数返回值,成功pthread_exit()返回0;当出现错误时,它返回一个错误编号
void *retval 表示线程退出状态,是一个无类型指针,与传给启动例程的单个参数类似。进程中的其他线程可以通过调用pthread_join函 数访问到这个指针。
函数详解:
在线程中禁止调用exit函数,否则会导致整个进程退出,取而代之的是调用pthread_exit函数,这个函数是使一个线程退出,如果主线程调用pthread_exit函数也不会使整个进程退出,不影响其他线程的执行。 需要注意一点,pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了,栈空间就会被回收。 如果在主线程中使用pthread_exit函数,会导致子线程还在,内存无法被回收,成为僵尸进程。引入pthread_join函数。
2.4. 线程等待函数pthread_join()原型和头文件:
/*
Linux下 man pthread_join查看手册
*/
#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
int 函数返回值,如果执行成功pthread_join返回0,当出现错误时,它返回一个错误编号
pthread_t thread 线程ID
void **retval 存储线程结束状态,整个指针和pthread_exit的参数是同一块内存地址
- 函数详解:
调用这个函数的线程将一直阻塞,直到指定的线程调用pthread_exit、从启动例程中返回或者被取消。
可以通过调用pthread_join自动把线程置于分离状态,这样资源就可以恢复。如果线程已经处于分离状态,pthread_join调用就会失败,返回EINVAL。
如果对线程的返回值不感兴趣,可以把rval_ptr置为NULL。在这种情况下,调用pthread_join函数将等待指定的线程终止,但并不获得线程的终止状态。
2.5. 线程脱离函数pthread_detach()原型和头文件:
/*
Linux下 man pthread_detach查看手册
*/
#include <pthread.h>
int pthread_detach(pthread_t thread);
int 函数返回值,如果成功pthread_detach返回0,当出现错误时,它返回一个错误编号
pthread_t thread 需要脱离的线程(标识符)
- 函数详解:
一个线程或者是可汇合(joinable,默认值),或者是脱离的(detached)。 当一个可汇合的线程终止时: 它的线程ID和退出状态将留存到另一个线程对它调用pthread_join。 当一个脱离的线程终止时:像守护进程一样,当它们终止时,所有相关的资源都被释放,我们不能等待它们终止。 如果一个线程需要知道另一线程什么时候终止,那就最好保持第二个线程的可汇合状态。
2.6. 获取线程ID函数pthread_self()原型和头文件:
/*
Linux下 man pthread_self查看手册
*/
#include <pthread.h>
pthread_t pthread_self(void);
pthread_t 函数返回值,返回调用线程的ID
- 函数详解:
对于线程ID比较,为了可移植操作,我们不能简单地把线程ID当作整数来处理,因为不同系统对线程ID的定义可能不一样。我们应该要用pthread_self函数获取一个线程的ID 此函数可与pthread_detach函数配合使用,想让自己脱离的线程使用,就如以下语句:
pthread_detach(pthread_self());
2.7. 线程ID比较函数pthread_equal()原型和头文件:
/*
Linux下 man pthread_equal查看手册
*/
#include <pthread.h>
int pthread_equal(pthread_t tid1, pthread_t tid2);
int 函数返回值,如果两个线程id相等,pthread_equal()返回一个非零值;否则返回0
pthread_t tid1 需要进行比较的第一个线程ID
pthread_t tid2 需要进行比较的第二个线程ID
2.8. 子线程退出主线程获取子线程的退出状态(一):
#include <stdio.h>
#include <pthread.h>
/*
* 主线程创建一个新线程th1,执行func1函数。
* 在func1中,打印新线程的ID和传递的数据(100)。
* 主线程打印其自己的ID,然后等待th1结束。
*/
void* func1(void *arg)
{
static int ret = 10;
printf("th1:%ld是新线程ID\n",(unsigned long)pthread_self()); //获取新线程的ID
printf("th1:data = %d\n",*((int *)arg)); // 转换成int data
pthread_exit((void*)&ret);
}
int main()
{
int ret;
pthread_t th1; //线程标识符
int data = 100;
int *pret = NULL;
// int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
// void *(*start_routine) (void *), void *arg);
//创建了一个th1线程 //转换成无类型的地址
ret = pthread_create(&th1, NULL, func1, (void*)&data);
if(ret == 0){
printf("main:创建th1线程成功\n");
}
//获取主线程的ID
printf("main:线程ID = %ld\n",(unsigned long)pthread_self());
// int pthread_join(pthread_t thread, void **retval);
pthread_join(th1,(void**)&pret);
//等待th1线程终止,但是不获取th1线程终止状态
//
printf("mat : t1 quit :%d \n",*pret);
return 0;
}
static char *str = "th1 is run out";
是为了确保这个字符串的值在整个程序运行期间都有效。当线程退出时,返回这个字符串的地址是安全的,因为它的值不会消失。static
让一个变量的值在整个程序运行期间保持有效,即使它在函数内部定义。这样在多线程或函数间调用时,变量的值不会丢失
2.9. 子线程退出主线程获取子线程的退出状态(二):
void* func1(void *arg)
{
static char *str = "th1 is run out"; //必须使用static修饰,否则值会发生错误
// static int ret = 10;
printf("th1:%ld是新线程ID\n",(unsigned long)pthread_self()); //获取新线程的ID
printf("th1:data = %d\n",*((int *)arg)); // 转换成int data
pthread_exit((void*)str);
}
int main()
{
int ret;
pthread_t th1; //线程标识符
int data = 100;
char *pret = NULL;
// int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
// void *(*start_routine) (void *), void *arg);
//创建了一个th1线程 //转换成无类型的地址
ret = pthread_create(&th1, NULL, func1, (void*)&data);
if(ret == 0){
printf("main:创建th1线程成功\n");
}
//获取主线程的ID
printf("main:线程ID = %ld\n",(unsigned long)pthread_self());
// int pthread_join(pthread_t thread, void **retval);
pthread_join(th1,(void**)&pret);
//等待th1线程终止,但是不获取th1线程终止状态
//
printf("mat : t1 quit :%s \n",pret);
return 0;
}
2.10. 传递给线程运行函数的参数为一个结构体:
void* func1(void *arg)
{
static char *str = "th1 is run out"; //必须使用static修饰,否则值会发生错误
// static int ret = 10;
struct Test *p =(struct Test *)arg
printf("th1:%ld是新线程ID\n",(unsigned long)pthread_self()); //获取新线程的ID
printf("th1:年龄 = %d\n",p->data);
printf("th1:姓名 = %s\n",p->str);
pthread_exit((void*)str);
}
int main()
{
int ret;
pthread_t th1; //线程标识符
struct Test data = {100,"小刘"};
char *pret = NULL;
// int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
// void *(*start_routine) (void *), void *arg);
//创建了一个th1线程 //转换成无类型的地址
ret = pthread_create(&th1, NULL, func1, (void*)&data);
if(ret == 0){
printf("main:创建th1线程成功\n");
}
//获取主线程的ID
printf("main:线程ID = %ld\n",(unsigned long)pthread_self());
// int pthread_join(pthread_t thread, void **retval);
pthread_join(th1,(void**)&pret);
//等待th1线程终止,但是不获取th1线程终止状态
//
printf("mat : t1 quit :%s \n",pret);
return 0;
}
3. 互斥锁相关函数
3.1.1. 示例:线程的共享资源
- 双线程
/*
* 主线程创建一个新线程th1,执行func1函数。
* 在func1中,打印新线程的ID和传递的数据(100)。
* 主线程打印其自己的ID,然后等待th1结束。
*/
void* func1(void *arg)
{
static int ret = 10;
printf("th1:%ld是新线程ID\n",(unsigned long)pthread_self()); //获取新线程的ID
printf("th1:data = %d\n",*((int *)arg)); // 转换成int data
pthread_exit((void*)&ret);
}
void* func2(void *arg)
{
static int ret = 10;
printf("th2:%ld是新线程ID\n",(unsigned long)pthread_self()); //获取新线程的ID
printf("th2:data = %d\n",*((int *)arg)); // 转换成int data
pthread_exit((void*)&ret);
}
int main()
{
int ret;
pthread_t th1; //线程标识符
pthread_t th2; //线程标识符
int data1 = 100;
int data2 = 100;
int *pret = NULL;
// int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
// void *(*start_routine) (void *), void *arg);
//创建了一个th1线程 //转换成无类型的地址
ret = pthread_create(&th1, NULL, func1, (void*)&data1);
if(ret == 0){
printf("main:创建th1线程成功\n");
}
ret = pthread_create(&th2, NULL, func2, (void*)&data2);
if(ret == 0){
printf("main:创建th2线程成功\n");
}
//获取主线程的ID
printf("main:线程ID = %ld\n",(unsigned long)pthread_self());
// int pthread_join(pthread_t thread, void **retval);
pthread_join(th1,(void**)&pret);
//等待th1线程终止,但是不获取th1线程终止状态
//
//
printf("mat : t1 quit :%d \n",*pret);
printf("mat : t2 quit :%d \n",*pret);
return 0;
}
注意:由于两个线程使用的是同一个内存,没有使用先后执行函数,线程有着竞争的关系,所以运行之后每次的结果顺序是不一样的。
/*
* 主线程创建一个新线程th1,执行func1函数。
* 在func1中,打印新线程的ID和传递的数据(100)。
* 主线程打印其自己的ID,然后等待th1结束。
*/
int g_data =0;
void* func1(void *arg)
{
static int ret = 10;
printf("th1:%ld是新线程ID\n",(unsigned long)pthread_self()); //获取新线程的ID
printf("th1:data = %d\n",*((int *)arg)); // 转换成int data
while(1){
printf("t1:%d\n",g_data++);
sleep(1);
}
}
void* func2(void *arg)
{
static int ret = 10;
printf("th2:%ld是新线程ID\n",(unsigned long)pthread_self()); //获取新线程的ID
printf("th2:data = %d\n",*((int *)arg)); // 转换成int data
while(1){
printf("t2:%d\n",g_data++);
sleep(1);
}
}
int main()
{
int ret;
pthread_t th1; //线程标识符
pthread_t th2; //线程标识符
int data1 = 100;
int data2 = 100;
int *pret = NULL;
// int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
// void *(*start_routine) (void *), void *arg);
//创建了一个th1线程 //转换成无类型的地址
ret = pthread_create(&th1, NULL, func1, (void*)&data1);
if(ret == 0){
printf("main:创建th1线程成功\n");
}
ret = pthread_create(&th2, NULL, func2, (void*)&data2);
if(ret == 0){
printf("main:创建th2线程成功\n");
}
//获取主线程的ID
printf("main:线程ID = %ld\n",(unsigned long)pthread_self());
while(1){
printf("man:%d\n",g_data++);
sleep(1);
}
// int pthread_join(pthread_t thread, void **retval);
pthread_join(th1,NULL);
pthread_join(th2,NULL);
//等待th1线程终止,但是不获取th1线程终止状态
return 0;
}
th1的退出条件是基于g_data的值,但由于g_data被多个线程修改,它可能会在未达到3时就被其他线程影响。因此,th1可能不会按照预期正常退出。
上边的全局变量g_data
就是一个共享资源,每一个线程都能访问这个资源,并且对这个资源进行修改。虽然极大的方便了线程之间的通信,但是这种方法访问数据的方式不安全,可以使用后边的互斥和同步来解决。(或者使用局部变量,尽量减少全局变量的使用)
/*
* 主线程创建一个新线程th1,执行func1函数。
* 在func1中,打印新线程的ID和传递的数据(100)。
* 主线程打印其自己的ID,然后等待th1结束。
*/
int g_data =0;
void* func1(void *arg)
{
static int ret = 10;
printf("th1:%ld是新线程ID\n",(unsigned long)pthread_self()); //获取新线程的ID
printf("th1:data = %d\n",*((int *)arg)); // 转换成int data
while(1){
printf("t1:%d\n",g_data++);
sleep(1);
if(g_data++ == 3){
pthread_exit(NULL);
}
}
}
void* func2(void *arg)
{
static int ret = 10;
printf("th2:%ld是新线程ID\n",(unsigned long)pthread_self()); //获取新线程的ID
printf("th2:data = %d\n",*((int *)arg)); // 转换成int data
while(1){
printf("t2:%d\n",g_data++);
sleep(1);
}
}
int main()
{
int ret;
pthread_t th1; //线程标识符
pthread_t th2; //线程标识符
int data1 = 100;
int data2 = 100;
int *pret = NULL;
// int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
// void *(*start_routine) (void *), void *arg);
//创建了一个th1线程 //转换成无类型的地址
ret = pthread_create(&th1, NULL, func1, (void*)&data1);
if(ret == 0){
printf("main:创建th1线程成功\n");
}
ret = pthread_create(&th2, NULL, func2, (void*)&data2);
if(ret == 0){
printf("main:创建th2线程成功\n");
}
//获取主线程的ID
printf("main:线程ID = %ld\n",(unsigned long)pthread_self());
while(1){
printf("man:%d\n",g_data++);
sleep(1);
}
// int pthread_join(pthread_t thread, void **retval);
pthread_join(th1,NULL);
pthread_join(th2,NULL);
//等待th1线程终止,但是不获取th1线程终止状态
//
//
return 0;
}
互斥量(mutex)从本质上来说是一把锁,在访问共享资源前对互斥量进行加锁,在访问完成后释放互斥量上的锁。对互斥量进行加锁后,任何其他试图再次对互斥量加锁的线程将会被阻塞直到当前线程释放该互斥锁。如果释放互斥锁时有多个线程阻塞,所有在该互斥锁上的阻塞线程都会变成可运行状态,第一个变为可运行状态的线程可以对互斥量加锁,其他线程将会看到互斥锁依然被锁住,只能回去等待它重新变为可用。在这种方式下,每次只有一个线程可以向前运行。
在设计时需要规定所有的线程必须遵守相同的数据访问规则。只有这样,互斥机制才能正常工作。操作系统并不会做数据访问的串行化。如果允许其中的某个线程在没有得到锁的情况下也可以访问共享资源,那么即使其它的线程在使用共享资源前都获取了锁,也还是会出现数据不一致的问题。
互斥变量用pthread_mutex_t数据类型表示。在使用互斥变量前必须对它进行初始化,可以把它置为常量PTHREAD_MUTEX_INITIALIZER(只对静态分配的互斥量),也可以通过调用pthread_mutex_init函数进行初始化。如果动态地分配互斥量(例如通过调用malloc函数),那么在释放内存前需要调用pthread_mutex_destroy。
3.1. 创建互斥锁函数pthread_mutex_init()原型和头文件:
#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);
int 函数返回值,若成功返回0,失败返回错误编号
pthread_mutex_t *restrict mutex 是指想要初始化的互斥锁的指针
const pthread_mutexattr_t *restrict attr 用于指定互斥锁属性,如果为NULL则使用默认属性(缺省属性)
3.2. 互斥锁属性
互斥锁的属性在创建锁的时候指定,在LinuxThreads实现中仅有一个锁类型属性,不同的锁类型在试图对一个已经被锁定的互斥锁加锁时表现不同。当前(glibc2.2.3,linuxthreads0.9)有四个值可供选择:
- PTHREAD_MUTEX_TIMED_NP,缺省值,也就是普通锁。当一个线程加锁以后,其余请求锁的线程将形成一个等待队列,并在解锁后按优先级获得锁。这种锁策略保证了资源分配的公平性。
- PTHREAD_MUTEX_RECURSIVE_NP,嵌套锁,允许同一个线程对同一个锁成功获得多次,并通过多次unlock解锁。如果是不同线程请求,则在加锁线程解锁时重新竞争。
- PTHREAD_MUTEX_ERRORCHECK_NP,检错锁,如果同一个线程请求同一个锁,则返回EDEADLK,否PTHREAD_MUTEX_TIMED_NP类型动作相同。这样就保证当不允许多次加锁时不会出现最简单情况下的死锁。
- PTHREAD_MUTEX_ADAPTIVE_NP,适应锁,动作最简单的锁类型,仅等待解锁后重新竞争。
3.2.1. 销毁互斥锁函数pthread_mutex_destroy()原型和头文件:
#include <pthread.h>
int pthread_mutex_destroy(pthread_mutex_t *mutex);
int 函数返回值,若成功返回0,失败返回错误编号
pthread_mutex_t *mutex 是指想要销毁的互斥锁指针
3.2.2. 加锁pthread_mutex_lock()和解锁pthread_mutex_unlock()函数原型和头文件:
#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex); //加锁
int pthread_mutex_unlock(pthread_mutex_t *mutex); //解锁
int pthread_mutex_trylock(pthread_mutex_t *mutex); //测试加锁
int 函数返回值,若成功返回0,否则返回错误编号
pthread_mutex_t *mutex 想要操作的互斥锁指针
如果线程不希望被阻塞,它可以使用pthread_mutex_trylock尝试对互斥量进行加锁。如果调用pthread_mutex_trylock时互斥量处于未锁住状态,那么pthread_mutex_trylock将锁住互斥量,不会出现阻塞并返回0,否则pthread_mutex_trylock就会失败,不能锁住互斥量,而返回EBUSY。
3.3. 线程与互斥锁应用实例:
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
int g_data =0;
pthread_mutex_t mutex; //定义互斥量
void* func1(void *arg)
{
printf("th1:%ld是新线程ID\n",(unsigned long)pthread_self()); //获取新线程的ID
printf("th1:data1 = %d\n",*((int *)arg)); // 转换成int data
int ret1 = pthread_mutex_lock(&mutex); // 加锁
if(ret1 != 0){
printf("加锁失败\n");
}
while(1){
printf("th1: data11 = %d\n",g_data++);
sleep(1);
if(g_data++ == 3){
ret1 = pthread_mutex_unlock(&mutex); // 解锁
printf("th1退出============\n");
if(ret1 != 0){
printf("解锁失败\n");
}
pthread_exit(NULL);
}
}
}
void* func2(void *arg)
{
printf("th2:%ld是新线程ID\n",(unsigned long)pthread_self()); //获取新线程的ID
printf("th2:data22 = %d\n",*((int *)arg)); // 转换成int data
while(1){
printf("th2: data = %d\n",g_data);
int ret2 = pthread_mutex_lock(&mutex); //加锁:
if(ret2 != 0){
printf("加锁失败\n");
}
g_data++;
ret2 = pthread_mutex_unlock(&mutex); //加锁
if(ret2 != 0){
printf("解锁失败\n");
}
sleep(1);
}
}
int main()
{
int ret;
pthread_t th1; //线程标识符
pthread_t th2; //线程标识符
int data1 = 100;
int data2 = 100;
int *pret = NULL;
ret = pthread_mutex_init(&mutex,NULL); //创建互斥锁
if(ret != 0){
printf("创建互斥锁失败\n");
}
// int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
// void *(*start_routine) (void *), void *arg);
//创建了一个th1线程 //转换成无类型的地址
ret = pthread_create(&th1, NULL, func1, (void*)&data1);
if(ret == 0){
printf("main:创建th1线程成功\n");
}
ret = pthread_create(&th2, NULL, func2, (void*)&data2);
if(ret == 0){
printf("main:创建th2线程成功\n");
}
//获取主线程的ID
printf("main:线程ID = %ld\n",(unsigned long)pthread_self());
while(1){
printf("man:%d\n",g_data);
sleep(1);
}
// int pthread_join(pthread_t thread, void **retval);
pthread_join(th1,NULL);
pthread_join(th2,NULL);
//等待th1线程终止,但是不获取th1线程终止状态
ret = pthread_mutex_destroy(&mutex); //条件变量销毁
if(ret != 0){
printf("main:条件变量销毁失败\n");
}
return 0;
}
- 线程1 (
func1
) 会在g_data
达到3时退出,线程2 (func2
) 会一直运行,不断修改g_data
。 - 主线程 (
main
) 也会持续运行,打印g_data
的值。 - 线程1先夺得互斥锁,直到线程1满足退出条件,解锁后,线程2的主要代码部分才开始运行。
- 代码分析
首先主控线程创建了两个新的线程,然后在主控线程中打印g_data的值,此时g_data的值为0,主控线程休眠一秒,此时子进程2抢占到了CPU,执行子进程2的程序,子进程2同样打印了g_data的值,此时g_data的值没有发生变化。子进程2休眠一秒,子进程1抢占到CPU,此时执行子进程1的代码,子进程1首先加互斥锁,然后执行g_data++会先打印后会它的值加加,此时g_data的值已经变为1。子进程休眠一秒后被主控线程捕获,此时打印g_data的值为1,然后休眠一秒,此时可能子进程2抢占到了CPU,继续接着之前的代码执行,子进程2尝试去上锁,但此时这把锁已经被子进程1获取,所以子进程2就被阻塞,继而接着去执行子进程1的程序,直到子进程检测到g_data的值变为3后解锁并将整个进程退出。
3.4. 死锁
死锁是指两个或多个执行单元(如进程、线程或任务)在等待彼此持有的资源时,导致它们都无法向前推进执行的一种现象。
产生死锁的条件:
- 互斥条件:至少有一个资源每次只能被一个执行单元使用。
- 占有和等待条件:一个执行单元至少持有一个资源,并等待获取其他执行单元持有的资源。
- 不剥夺条件:执行单元已获得的资源,在未使用完之前,不能被其他执行单元强行剥夺。
- 循环等待条件:存在一种执行单元资源的循环等待链。
简单来说,死锁就是目前有两把锁,它们各自都想拿到对方的那把锁,但是拿不到最终导致线程一直阻塞的现象。
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>
int g_data = 0;
pthread_mutex_t mutex; //定义互斥锁类型
pthread_mutex_t mutex2;
void *pthread_func1(void *arg)
{
pthread_mutex_lock(&mutex); //加锁,对共享资源进行保护
sleep(1);
pthread_mutex_lock(&mutex2);
while(1)
{
printf("tid1: %d\n",g_data++);
sleep(1);
if(g_data == 3)
{
printf("tid1 quit==========\n");
exit(0);
pthread_mutex_unlock(&mutex); //解锁,使得其他线程能够获取到这个锁
pthread_exit(NULL);
}
}
}
void *pthread_func2(void *arg)
{
pthread_mutex_lock(&mutex2);
sleep(1);
pthread_mutex_lock(&mutex);
while(1)
{
printf("tid2: %d\n",g_data);
sleep(1);
pthread_mutex_lock(&mutex);
g_data++;
pthread_mutex_unlock(&mutex);
}
}
int main()
{
pthread_t tid1,tid2;
int ret;
pthread_mutex_init(&mutex,NULL); //对互斥锁进行初始化
pthread_mutex_init(&mutex2,NULL);
if((ret = pthread_create(&tid1,NULL,pthread_func1,NULL)) == 0)
{
printf("main: create new thread successfully\n");
}
if((ret = pthread_create(&tid2,NULL,pthread_func2,NULL)) == 0)
{
printf("main: create new thread successfully\n");
}
while(1)
{
printf("main: %d\n",g_data);
sleep(1);
}
pthread_mutex_destroy(&mutex); //对互斥锁进行销毁
pthread_mutex_destroy(&mutex2);
return 0;
}
什么是死锁,就是有两个锁 1,2锁,线程a先定义加了个锁1,之后加了个锁2,线程b先定义加了个锁2,之后加了个锁1,a线程锁1加完之后,没有解锁,等带锁2,结果锁2被线程2加锁了,没有解锁。两个线程就互相等待对方的锁,就相互卡死了,导致程序陷入死锁。
4. 条件变量相关函数
条件变量:本身不是锁,但可以造成线程阻塞,通常与互斥锁配合使用; 条件变量是利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作:一个线程等待 "条件变量的条件成立 "而挂起;另一个线程使 "条件成立 "(给出条件成立信号)。为了防止竞争,条件变量的使用总是和一个互斥锁结合在一起。
4.1. 创建条件变量函数pthread_cond_init()原型和头文件:
#include <pthread.h>
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);
int 函数返回值,若成功返回0,失败返回错误编号
pthread_cond_t *restrict cond 想要初始化的条件变量的指针
pthread_condattr_t *restrict attr 用于指定互斥锁属性,如果为NULL则使用默认属性(缺省属性)
4.2. 销毁条件变量函数pthread_cond_destroy()原型和头文件:
#include <pthread.h>
int pthread_cond_destroy(pthread_cond_t *cond);
int 函数返回值,若成功返回0,失败返回错误编号
pthread_cond_t *cond 想要销毁的条件变量的指针
4.3. 条件变量等待函数pthread_cond_wait()原型和头文件(无条件等待):
#include <pthread.h>
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
int 函数返回值,若成功返回0,失败返回错误编号
pthread_cond_t *restrict cond 条件变量指针
pthread_mutex_t *restrict mutex 互斥锁指针
pthread_cond_wait等待条件变为真。如果在给定的时间内条件不能满足,那么会生成一个代表一个出错码的返回变量。传递给pthread_cond_wait的互斥量对条件进行保护,调用者把锁住的互斥量传给函数。函数把调用线程放到等待条件的线程列表上,然后对互斥量解锁,这两个操作都是原子操作。这样就关闭了条件检查和线程进入休眠状态等待条件改变这两个操作之间的时间通道,这样线程就不会错过条件的任何变化。pthread_cond_wait返回时,互斥量再次被锁住。
4.4. 条件变量等待函数pthread_cond_timedwait()原型和头文件(计时等待):
#include <pthread.h>
int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex,
struct timespec *restrict abstime);
int 函数返回值,若成功返回0,失败返回错误编号
pthread_cond_t *restrict cond 条件变量指针
pthread_mutex_t *restrict mutex 互斥锁指针
struct timespec *restrict abstime 是一个绝对时间,Linux中常用的时间结构有struct timespec 和 struct timeval
#include <sys/time.h> //需要包含的头文件
struct timespec
{
time_t tv_sec; /* Seconds. */
long tv_nsec; /* Nanoseconds. */
};
struct timeval {
time_t tv_sec;
suseconds_t tv_usec;
};
/*这里面的超时时间是一个绝对值,也就是距离1970-1-1 日的时间值,而不是一个时间段。比如说当前时间为2024-6-8 14:43:00.100,我们想通过这个函数设置最大超时为2500ms,那么就需要设置abstime时间为2024-6-8 14:43:02.600.*/
/*abstime参数设置实例*/
struct timespec abstime;
struct timeval now;
gettimeofday(&now, NULL);
abstime.tv_sec = now.tv_sec + 5;
abstime.tv_nsec = now.tv_usec * 1000;
pthread_cond_timedwait函数的工作方式与pthread_cond_wait函数类似,只是多了一个abstime。abstime指定了等待的时间,它是通过timespec结构指定
4.5. 条件变量触发函数pthread_cond_signal和pthread_cond_broadcast原型和头文件:
#include <pthread.h>
int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);
int 函数返回值,若成功返回0,失败返回错误编号
pthread_cond_t *cond 条件变量指针
4.6. 条件变量与互斥量结合案例:
- 当线程2操作全局变量num为3时,线程1执行
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
pthread_mutex_t mutex; //互斥锁
pthread_cond_t cond; //条件变量
int num = 0; //共享变量
void* func1(void *arg)
{
printf("th1:线程ID = %ld\n",(unsigned long)pthread_self()); //获取th1线程ID
printf("th1:参数 = %d\n",*((int *)arg)); //打印参数
while(1){
pthread_cond_wait(&cond, &mutex); //条件变量等待
printf("th1:num = %d\n",num); //打印共享变量
printf("th1 run=============================\n");
num = 0; //重置共享变量
sleep(1); //睡眠1秒
}
pthread_exit(NULL); //线程退出
}
void* func2(void *arg)
{
printf("th2:线程ID = %ld\n",(unsigned long)pthread_self()); //获取th2线程ID
printf("th2:参数 = %d\n",*((int *)arg)); //打印参数
while(1){
printf("th2:num = %d\n",num); //打印共享变量
pthread_mutex_lock(&mutex); //加锁
num++; //加1
if(num == 3){
pthread_cond_signal(&cond); //条件变量通知
}
pthread_mutex_unlock(&mutex); //解锁
sleep(1); //睡眠1秒
}
pthread_exit(NULL); //线程退出
}
int main()
{
int ret;
int data = 100;
pthread_t th1; //th1线程标识符
pthread_t th2; //th2线程标识符
//int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);
ret = pthread_mutex_init(&mutex, NULL); //互斥锁初始化
if(ret != 0){
printf("main:mutex初始化失败\n");
}
//int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);
ret = pthread_cond_init(&cond, NULL); //条件变量初始化
if(ret != 0){
printf("main:条件变量初始化失败\n");
}
//int pthread_create(pthread_t *restrict tidp, const pthread_attr_t *restrict attr, void *(*start_rtn)(void *), void *restrict arg);
ret = pthread_create(&th1, NULL, func1, (void *)&data); //创建th1线程
if(ret == 0){
printf("main:th1线程创建成功\n");
}
ret = pthread_create(&th2, NULL, func2, (void *)&data); //创建th2线程
if(ret == 0){
printf("main:th2线程创建成功\n");
}
pthread_join(th1,NULL); //等待th1线程退出
pthread_join(th2,NULL); //等待th2线程退出
//int pthread_mutex_destroy(pthread_mutex_t *mutex);
ret = pthread_mutex_destroy(&mutex); //互斥锁销毁
if(ret != 0){
printf("main:mutex销毁失败\n");
}
//int pthread_cond_destroy(pthread_cond_t *cond);
ret = pthread_cond_destroy(&cond); //条件变量销毁
if(ret != 0){
printf("main:条件变量销毁失败\n");
}
return 0;
}
编译结果
代码分析:当主控线程创建新线程后,系统可能会给线程1分配时间片,此时线程1调用函数pthread_cond_wait等待条件满足,但此时条件不满足,所以进入到阻塞状态。系统转而去执行线程2,当经过三轮的执行后,满足条件,线程2调用pthread_cond_signal函数唤醒线程1执行,然后线程1重新将g_data的值赋值为0,以此类推的执行。
4.7. 静态初始化互斥锁和条件变量:
对于互斥锁与条件变量的初始化,也可以用静态初始化即使用宏,如:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER //互斥锁
==
pthread_mutex_init(&mutex, NULL);
pthread_cond_t cond = PTHREAD_COND_INITIALIZER //条件变量
==
pthread_cond_init(&cond, NULL);
这部分代码是在C语言中使用pthread库来创建互斥锁和条件变量的实例。互斥锁用于保护共享资源,以确保在同一时间只有一个线程可以访问它。条件变量用于在线程之间的同步,允许一个线程在满足特定条件之前等待,或者当条件发生变化时通知其他线程。
代码中使用了PTHREAD_MUTEX_INITIALIZER和PTHREAD_COND_INITIALIZER宏,它们分别用于静态地初始化互斥锁和条件变量,确保它们被正确地设置为默认值。
总的来说,这部分代码的主要功能是初始化了一个互斥锁和一个条件变量,以便后续在多线程中使用。
点个赞吧老铁!!!