操作系统具有管理进程
,进程调度
的能力,线程,决定哪个进程、线程使用 CPU。很多时候我们需要在同一时间干不同的任务,这就需要我们通过多进程或者多线程来进行,在我们学习和工作中我们大部分用到的都是多线程,本文主要是在linux下探索c语言的多进程的使用方法
文中的内容大部分是从大丙老师博客地址(https://subingwen.cn/linux)那里copy来的,有些内容是为了完善内容体系或者我自己的理解加的
资源分配
的最小单位,而线程是 CPU 调度
的最小单位;进程是由内核管理和调度的,所以进程的切换只能发生在
内核态
。所以,进程的上下文切换不仅包含了虚拟内存、栈、全局变量等用户空间的资源,还包括了内核堆栈、寄存器等内核空间的资源
。通常,会把交换的信息保存在进程的 PCB,当要运行另外一个进程的时候,我们需要从这个进程的 PCB 取出上下文,然后恢复到 CPU 中,这使得这个进程可以继续执行
进程可以使多个程序并发执行,以提高资源的利用率和系统的吞吐量,但是其带来了一些缺点:
基于以上的缺点,操作系统引入了比进程粒度更小的线程,作为并发执行的基本单位,从而减少程序在并发执行时所付出的时间和空间开销,提高并发性能。
#include
#include
#include
#include
#include
void* callback(void* arg)
{
for(int i=0; i<5;i++)
{
printf("子线程:i=%d\n", i);
}
printf("子线程ID:%ld\n", pthread_self());
return NULL;
}
int main()
{
pthread_t tid;
pthread_create(&tid, NULL, callback, NULL);
for (int i=0; i<5;++i)
{
printf("主线程:i = %d\n", i);
}
printf("主线程ID:%ld\n", pthread_self());
return 0;
}
pthread.h
:多线程库
pthread_t pthread_self(void);
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
gcc pthread_create.c -lpthread # 要链接上静态库:线程库的名字叫pthread, 全名: libpthread.so libptread.a
值得注意的是执行上述代码后在打印的日志输出中为什么子线程处理函数没有执行完毕呢(只看到了子线程的部分日志输出)?
主线程一直在运行,执行期间创建出了子线程,说明主线程有 CPU 时间片,在这个时间片内将代码执行完毕了,主线程就退出了。子线程被创建出来之后需要抢cpu时间片, 抢不到就不能运行,如果主线程退出了, 虚拟地址空间就被释放了, 子线程就一并被销毁了。但是如果某一个子线程退出了, 主线程仍在运行, 虚拟地址空间依旧存在.
得到的结论:在没有人为干预的情况下,虚拟地址空间的生命周期和主线程是一样的,与子线程无关。
目前的解决方案:让子线程执行完毕,主线程再退出,可以在主线程中添加挂起函数 sleep();
#include
#include
#include
#include
#include
void* callback(void* arg)
{
for(int i=0; i<5;i++)
{
printf("子线程:i=%d\n", i);
}
printf("子线程ID:%ld\n", pthread_self());
return NULL;
}
int main()
{
pthread_t tid;
pthread_create(&tid, NULL, callback, NULL);
for (int i=0; i<5;++i)
{
printf("主线程:i = %d\n", i);
}
printf("主线程ID:%ld\n", pthread_self());
sleep(1);
return 0;
}
但是不会导致虚拟地址空间的释放(针对于主线程),我们就可以调用线程库中的线程退出函数,只要调用该函数当前线程就马上退出了,并且不会影响到其他线程的正常运行·
,不管是在子线程或者主线程中都可以使用。这样也可以让子线程正常的执行#include
#include
#include
#include
#include
void* callback(void* arg)
{
for(int i=0; i<5;i++)
{
printf("子线程:i=%d\n", i);
}
printf("子线程ID:%ld\n", pthread_self());
return NULL;
}
int main()
{
pthread_t tid;
pthread_create(&tid, NULL, callback, NULL);
for (int i=0; i<5;++i)
{
printf("主线程:i = %d\n", i);
}
printf("主线程ID:%ld\n", pthread_self());
// 主线程调用退出函数退出, 地址空间不会被释放
pthread_exit(NULL);
return 0;
}
所用函数
void pthread_exit(void *retval);
线程和进程一样,子线程退出的时候其内核资源主要由主线程回收,线程库中提供的线程回收函叫做 pthread_join()
,这个函数是一个阻塞函数,如果还有子线程在运行,调用该函数就会阻塞,子线程退出函数解除阻塞进行资源的回收,函数被调用一次,只能回收一个子线程,如果有多个子线程则需要循环进行回收。
另外通过线程回收函数还可以获取到子线程退出时传递出来的数据,函数原型如下:
int pthread_join(pthread_t thread, void **retval);
在子线程退出的时候可以使用 pthread_exit()
的参数将数据传出,在回收这个子线程的时候可以通过 phread_join()
的第二个参数来接收子线程传递出的数据。接收数据有很多种处理方式,下面来列举几种:
#include
#include
#include
#include
#include
struct Test
{
/* data */
int num;
int age;
};
void* callback(void* arg)
{
// 此时我们把结构体放在子线程的栈中
struct Test test;
for(int i=0; i<5;i++)
{
printf("子线程:i=%d\n", i);
}
test.num = 100;
test.age = 18;
printf("子线程ID:%ld\n", pthread_self());
// 子线程结束后传出数据
pthread_exit(&test);
return NULL;
}
int main()
{
pthread_t tid;
pthread_create(&tid, NULL, callback, NULL);
for (int i=0; i<5;++i)
{
printf("主线程:i = %d\n", i);
}
printf("主线程ID:%ld\n", pthread_self());
// 主线程结束后不会释放地址空间,即主线程结束不会让子线程结束
//pthread_exit(NULL);
// 另外一种可以让子线程执行完毕的方法:让主线程堵塞,等待子线程执行完再结束
void *ptr=NULL;
// 这个ptr是一级指针,&ptr取出它的二级指针,返回的值是子线程pthread_exit(&test);这个语句传出的test
pthread_join(tid, &ptr);
// 强制类型转换,将ptr转化为指向pt的指针
struct Test* pt = (struct Test*)ptr;
printf("年龄为:%d\n", pt->age);
printf("数字为:%d\n",pt->num);
return 0;
}
主线程:i = 0
主线程:i = 1
主线程:i = 2
主线程:i = 3
主线程:i = 4
主线程ID:140555521054528
子线程:i=0
子线程:i=1
子线程:i=2
子线程:i=3
子线程:i=4
子线程ID:140555512719104
年龄为:32725
数字为:-1595400192
运行上面程序我们可以看到,打印的年龄和数字不是我们在子线程中定义的在主线程中没有没有得到子线程返回的数据信息,具体原因是这样的:
如果多个线程共用同一个虚拟地址空间,每个线程在栈区都有一块属于自己的内存,相当于栈区被这几个线程
平分
了,当线程退出,线程在栈区的内存也就被回收了,因此随着子线程的退出,写入到栈区的数据也就被释放了。即数据就会变得很奇怪
位于同一虚拟地址空间中的线程,虽然不能共享栈区数据,但是可以共享全局数据区和堆区数据,因此在子线程退出的时候可以将传出数据存储到全局变量
、静态变量
或者堆内存
中。即我们可以将结构体的定义放在全局区。在下面的例子中将数据存储到了全局变量中:
#include
#include
#include
#include
#include
struct Test
{
/* data */
int num;
int age;
};
// 我们将结构体放在全局变量即放在全局区,这样子线程被释放后也不会出现问题
struct Test test;
void* callback(void* arg)
{
for(int i=0; i<5;i++)
{
printf("子线程:i=%d\n", i);
}
test.num = 100;
test.age = 18;
printf("子线程ID:%ld\n", pthread_self());
// 子线程结束后传出数据
pthread_exit(&test);
return NULL;
}
int main()
{
pthread_t tid;
pthread_create(&tid, NULL, callback, NULL);
for (int i=0; i<5;++i)
{
printf("主线程:i = %d\n", i);
}
printf("主线程ID:%ld\n", pthread_self());
// 主线程结束后不会释放地址空间,即主线程结束不会让子线程结束
//pthread_exit(NULL);
// 另外一种可以让子线程执行完毕的方法:让主线程堵塞,等待子线程执行完再结束
void *ptr=NULL;
// 这个ptr是一级指针,&ptr取出它的二级指针,返回的值是子线程pthread_exit(&test);这个语句传出的test
pthread_join(tid, &ptr);
struct Test* pt = (struct Test*)ptr;
printf("%d\n", pt->age);
printf("%d\n",pt->num);
return 0;
}
将数据放在全局区,这样子线程退出后返回的数据在主线程就能拿到了
虽然每个线程都有属于自己的栈区空间,但是位于同一个地址空间的多个线程是可以相互访问对方的栈空间上的数据的。由于很多情况下还需要在主线程中回收子线程资源,所以主线程一般都是最后退出,基于这个原因在下面的程序中将子线程返回的数据保存到了主线程的栈区内存中
#include
#include
#include
#include
#include
struct Test
{
/* data */
int num;
int age;
};
void* callback(void* arg)
{
struct Test* p = (struct Test*)arg;
// 此时我们把结构体放在子线程的栈中
for(int i=0; i<5;i++)
{
printf("子线程:i=%d\n", i);
}
p->num = 100;
p->age = 18;
printf("子线程ID:%ld\n", pthread_self());
// 子线程结束后传出数据
pthread_exit(p);
return NULL;
}
int main()
{
pthread_t tid;
struct Test test;
pthread_create(&tid, NULL, callback, &test);
for (int i=0; i<5;++i)
{
printf("主线程:i = %d\n", i);
}
printf("主线程ID:%ld\n", pthread_self());
// 主线程结束后不会释放地址空间,即主线程结束不会让子线程结束
//pthread_exit(NULL);
// 另外一种可以让子线程执行完毕的方法:让主线程堵塞,等待子线程执行完再结束
void *ptr=NULL;
// 这个ptr是一级指针,&ptr取出它的二级指针,返回的值是子线程pthread_exit(&test);这个语句传出的test
pthread_join(tid, &ptr);
struct Test* pt = (struct Test*)ptr;
printf("年龄为:%d\n", pt->age);
printf("数字为:%d\n",pt->num);
return 0;
}
即我们将数据存储到主线程的栈中,在子线程更改这个数据,主线程就能拿到这个数据
#include
#include
#include
#include
#include
struct Test
{
int num;
int age;
};
void* callback(void* arg)
{
// 此时我们把结构体放在堆区中
struct Test* test = (struct Test*)malloc(20);
for(int i=0; i<5;i++)
{
printf("子线程:i=%d\n", i);
}
test->num = 100;
test->age = 18;
printf("子线程ID:%ld\n", pthread_self());
// 子线程结束后传出数据
pthread_exit(test);
}
int main()
{
pthread_t tid;
pthread_create(&tid, NULL, callback, NULL);
for (int i=0; i<5;++i)
{
printf("主线程:i = %d\n", i);
}
printf("主线程ID:%ld\n", pthread_self());
// 主线程结束后不会释放地址空间,即主线程结束不会让子线程结束
// pthread_exit(NULL);
// 另外一种可以让子线程执行完毕的方法:让主线程堵塞,等待子线程执行完再结束
void *ptr=NULL;
// 这个ptr是一级指针,&ptr取出它的二级指针,
// 返回的值是子线程pthread_exit(&test);这个语句传出的test
// ptr指向test
pthread_join(tid, &ptr);
// 强制类型转换,将ptr转化为指向pt的指针
struct Test* pt = (struct Test*)ptr;
printf("年龄为:%d\n", pt->age);
printf("数字为:%d\n",pt->num);
return 0;
}
从上面代码我们可以看到,当我们把子线程要传出来的资源放在堆区时,此时在子线程运行结束后并不会被释放,我们把指向这块内存的地址传给主线程,然后在主线程拿到数据,最后把它
free
掉
在某些情况下,程序中的主线程有属于自己的业务处理流程,如果让主线程负责子线程的资源回收,调用 pthread_join()
只要子线程不退出主线程就会一直被阻塞,主要线程的任务也就不能被执行了。
在线程库函数中为我们提供了线程分离函数 pthread_detach()
,调用这个函数之后指定的子线程就可以和主线程分离,当子线程退出的时候,其占用的内核资源就被系统的其他进程接管并回收了
。线程分离之后在主线程中使用pthread_join()
就回收不到子线程资源了
int pthread_detach(pthread_t thread);
下面的代码中,在主线程中创建子线程,并调用线程分离函数,实现了主线程和子线程的分离:
#include
#include
#include
#include
#include
// 子线程的处理代码
void* working(void* arg)
{
printf("我是子线程, 线程ID: %ld\n", pthread_self());
for(int i=0; i<9; ++i)
{
printf("child == i: = %d\n", i);
}
return NULL;
}
int main()
{
// 1. 创建一个子线程
pthread_t tid;
pthread_create(&tid, NULL, working, NULL);
printf("子线程创建成功, 线程ID: %ld\n", tid);
// 2. 子线程不会执行下边的代码, 主线程执行
printf("我是主线程, 线程ID: %ld\n", pthread_self());
for(int i=0; i<3; ++i)
{
printf("i = %d\n", i);
}
// 设置子线程和主线程分离
pthread_detach(tid);
// 让主线程自己退出即可
pthread_exit(NULL);
return 0;
}
线程取消的意思就是在某些特定情况下在一个线程中杀死另一个线程。使用这个函数杀死一个线程需要分两步:
系统调用
(从用户区切换到内核区),否则线程 B 可以一直运行。int pthread_cancel(pthread_t thread);
在下面的示例代码中,主线程调用线程取消函数,只要在子线程中进行了系统调用,当子线程执行到这个位置就挂掉了。
#include
#include
#include
#include
#include
// 子线程的处理代码
void* working(void* arg)
{
int j=0;
for(int i=0; i<9; ++i)
{
j++;
}
// 这个函数会调用系统函数, 因此这是个间接的系统调用
printf("我是子线程, 线程ID: %ld\n", pthread_self());
for(int i=0; i<9; ++i)
{
printf(" child i: %d\n", i);
}
return NULL;
}
int main()
{
// 1. 创建一个子线程
pthread_t tid;
pthread_create(&tid, NULL, working, NULL);
printf("子线程创建成功, 线程ID: %ld\n", tid);
// 2. 子线程不会执行下边的代码, 主线程执行
printf("我是主线程, 线程ID: %ld\n", pthread_self());
for(int i=0; i<3; ++i)
{
printf("i = %d\n", i);
}
// 杀死子线程, 如果子线程中做系统调用, 子线程就结束了
pthread_cancel(tid);
// 让主线程自己退出即可
pthread_exit(NULL);
return 0;
}
这个函数很有意思诶,意思是一个线程调用
pthread_cancel
这个函数让子线程结束,但是它不会立刻结束,而是子线程做了系统调用后才结束,有点类似,你给别人下毒了,但是这个毒性不会立刻发作,你得满足一些条件(发生了系统调用
)才会发生作用,有点类似于七步断肠散
在 Linux 中线程 ID 本质就是一个无符号长整形,因此可以直接使用比较操作符比较两个线程的 ID,但是线程库是可以跨平台使用的
,在某些平台上 pthread_t 可能不是一个单纯的整形,这中情况下比较两个线程的 ID 必须要使用比较函数,函数原型如下:
int pthread_equal(pthread_t t1, pthread_t t2);
参数:t1 和 t2 是要比较的线程的线程 ID
返回值:如果两个线程 ID 相等返回非 0 值,如果不相等返回 0
假设有 4 个线程 A、B、C、D,当前一个线程 A 对内存中的共享资源进行访问的时候,其他线程 B, C, D 都不可以对这块内存进行操作,直到线程 A 对这块内存访问完毕为止,B,C,D 中的一个才能访问这块内存,剩余的两个需要继续阻塞等待,以此类推,直至所有的线程都对这块内存操作完毕。 线程对内存的这种访问方式就称之为线程同步,通过对概念的介绍,我们可以了解到所谓的同步并不是多个线程同时对内存进行访问,而是按照先后顺序依次进行的。
在研究线程同步之前,先来看一个两个线程交替数数(每个线程数 10 个数,交替数到 20)的例子:
#include
#include
#include
#include
#include
#include
#include
#define MAX 10
// 全局变量
int number;
// 线程处理函数
void* funcA_num(void* arg)
{
for(int i=0; i<MAX; ++i)
{
int cur = number;
cur++;
usleep(10);
number = cur;
printf("Thread A, id = %lu, number = %d\n", pthread_self(), number);
}
return NULL;
}
void* funcB_num(void* arg)
{
for(int i=0; i<MAX; ++i)
{
int cur = number;
cur++;
number = cur;
printf("Thread B, id = %lu, number = %d\n", pthread_self(), number);
usleep(5);
}
return NULL;
}
int main(int argc, const char* argv[])
{
pthread_t p1, p2;
// 创建两个子线程
pthread_create(&p1, NULL, funcA_num, NULL);
pthread_create(&p2, NULL, funcB_num, NULL);
// 阻塞,资源回收
pthread_join(p1, NULL);
pthread_join(p2, NULL);
return 0;
Thread B, id = 139963328038656, number = 1
Thread B, id = 139963328038656, number = 2
Thread A, id = 139963336431360, number = 1
Thread A, id = 139963336431360, number = 2
Thread B, id = 139963328038656, number = 3
Thread A, id = 139963336431360, number = 3
Thread B, id = 139963328038656, number = 4
Thread B, id = 139963328038656, number = 5
Thread A, id = 139963336431360, number = 5
Thread A, id = 139963336431360, number = 6
Thread B, id = 139963328038656, number = 7
Thread A, id = 139963336431360, number = 7
Thread B, id = 139963328038656, number = 8
Thread A, id = 139963336431360, number = 8
Thread B, id = 139963328038656, number = 9
Thread A, id = 139963336431360, number = 9
Thread B, id = 139963328038656, number = 10
Thread A, id = 139963336431360, number = 10
Thread A, id = 139963336431360, number = 11
Thread B, id = 139963328038656, number = 12
通过对上面例子的测试,可以看出虽然每个线程内部循环了 10 次每次数一个数,但是最终没有数到 20,通过输出的结果可以看到,有些数字被重复数了多次,其原因就是没有对线程进行同步处理,造成了数据的混乱。
值得注意的前提是,多线程的运行是无序的,也就是说,我们无法预料哪个线程可以优先抢到cpu时间片,
两个线程在数数的时候需要分时复用 CPU 时间片
,并且测试程序中调用了 sleep() 导致线程的 CPU 时间片没用完就被迫挂起了,这样就能让 CPU 的上下文切换(保存当前状态,下一次继续运行的时候需要加载保存的状态)更加频繁,更容易再现数据混乱的这个现象。
CPU 对应寄存器、一级缓存、二级缓存、三级缓存是独占的,用于存储处理的数据和线程的状态信息,数据被 CPU 处理完成需要再次被写入到物理内存中,物理内存数据也可以通过文件 IO 操作写入到磁盘中。
在测试程序中两个线程共用全局变量 number
当线程变成运行态之后开始数数,从物理内存加载数据,让后将数据放到 CPU 进行运算,最后将结果更新到物理内存中。如果数数的两个线程都可以顺利完成这个流程,那么得到的结果肯定是正确的。
如果线程 A 执行这个过程期间就失去了 CPU 时间片,线程 A 被挂起了最新的数据没能更新到物理内存。线程 B 变成运行态之后从物理内存读数据,很显然它没有拿到最新数据,只能基于旧的数据往后数,然后失去 CPU 时间片挂起。线程 A 得到 CPU 时间片变成运行态,第一件事儿就是将上次没更新到内存的数据更新到内存,但是这样会导致线程 B 已经更新到内存的数据被覆盖,活儿白干了,最终导致有些数据会被重复数很多次。
对于多个线程访问共享资源出现数据混乱
的问题,需要进行线程同步。常用的线程同步方式有四种:互斥锁
、读写锁
、条件变量
、信号量
。所谓的共享资源就是多个线程共同访问的变量,这些变量通常为全局数据区变量
或者堆区变量
,这些变量对应的共享资源也被称之为临界资源
。
找到临界资源之后,再找和临界资源相关的上下文代码,这样就得到了一个代码块,这个代码块可以称之为临界区。确定好临界区(临界区越小越好)之后,就可以进行线程同步了,线程同步的大致处理思路是这样的:
并行访问
就变为串行访问
了。互斥锁是线程同步最常用的一种方式,通过互斥锁可以锁定一个代码块,被锁定的这个代码块,所有的线程只能顺序执行 (不能并行处理),这样多线程访问共享资源数据混乱的问题就可以被解决了,需要付出的代价就是执行效率的降低,因为默认临界区多个线程是可以并行处理的,现在只能串行处理。
在 Linux 中互斥锁的类型为 pthread_mutex_t
,创建一个这种类型的变量就得到了一把互斥锁:
pthread_mutex_t mutex;
在创建的锁对象中保存了当前这把锁的状态信息:锁定还是打开,如果是锁定状态还记录了给这把锁加锁的线程信息(线程 ID)。一个互斥锁变量只能被一个线程锁定,被锁定之后其他线程再对互斥锁变量加锁就会被阻塞,直到这把互斥锁被解锁,被阻塞的线程才能被解除阻塞。一般情况下,每一个共享资源(静态变量,堆变量)对应一个把互斥锁,锁的个数和线程的个数无关
。
Linux 提供的互斥锁操作函数如下,如果函数调用成功会返回 0,调用失败会返回相应的错误号:
// 初始化互斥锁
// restrict: 是一个关键字, 用来修饰指针, 只有这个关键字修饰的指针可以访问指向的内存地址, 其他指针是不行的
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t *restrict attr);
// 释放互斥锁资源
int pthread_mutex_destroy(pthread_mutex_t *mutex);
// 修改互斥锁的状态, 将其设定为锁定状态, 这个状态被写入到参数 mutex 中
int pthread_mutex_lock(pthread_mutex_t *mutex);
这个函数被调用,首先会判断参数 mutex 互斥锁中的状态是不是锁定状态:
阻塞
在这把锁上,即一直等待// 尝试加锁
int pthread_mutex_trylock(pthread_mutex_t *mutex);
调用这个函数对互斥锁变量加锁还是有两种情况:
// 对互斥锁解锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);
互斥锁使用:我们可以将上面多线程交替数数的例子修改一下,使用互斥锁进行线程同步。两个线程一共操作了同一个全局变量,因此需要添加一互斥锁,来控制这两个线程。
#include
#include
#include
#include
#include
#include
#include
#define MAX 50
// 全局变量
int number;
// 定义一个互斥锁
pthread_mutex_t mutex;
// 线程处理函数
void* funcA_num(void* arg)
{
for(int i=0; i<MAX; ++i)
{
pthread_mutex_lock(&mutex);
int cur = number;
cur++;
usleep(10);
number = cur;
printf("Thread A, id = %lu, number = %d\n", pthread_self(), number);
pthread_mutex_unlock(&mutex);
}
return NULL;
}
void* funcB_num(void* arg)
{
for(int i=0; i<MAX; ++i)
{
pthread_mutex_lock(&mutex);
int cur = number;
cur++;
number = cur;
printf("Thread B, id = %lu, number = %d\n", pthread_self(), number);
pthread_mutex_unlock(&mutex);
usleep(5);
}
return NULL;
}
int main(int argc, const char* argv[])
{
pthread_t p1, p2;
// 初始化互斥锁
pthread_mutex_init(&mutex, NULL);
// 创建两个子线程
pthread_create(&p1, NULL, funcA_num, NULL);
pthread_create(&p2, NULL, funcB_num, NULL);
// 阻塞,资源回收
pthread_join(p1, NULL);
pthread_join(p2, NULL);
// 销毁互斥锁
pthread_mutex_destroy(&mutex);
return 0;
}
值得注意的是,加锁的覆盖范围应该尽可能将代码块缩小,否则将降低代码的执行效率,只放在执行全局变量的最小代码块范围
当多个线程访问共享资源,需要加锁,如果锁使用不当,就会造成死锁这种现象。如果线程死锁造成的后果是:所有的线程都被阻塞,并且线程的阻塞是无法解开的(因为可以解锁的线程也被阻塞了)。
// 场景1
void func()
{
for(int i=0; i<6; ++i)
{
// 当前线程A加锁成功, 当前循环完毕没有解锁, 在下一轮循环的时候自己被阻塞了
// 其余的线程也被阻塞
pthread_mutex_lock(&mutex);
....
.....
// 忘记解锁
}
}
// 场景2
void func()
{
for(int i=0; i<6; ++i)
{
// 当前线程A加锁成功
// 其余的线程被阻塞
pthread_mutex_lock(&mutex);
....
.....
if(xxx)
{
// 函数退出, 没有解锁(解锁函数无法被执行了)
return ;
}
pthread_mutex_lock(&mutex);
}
}
void func()
{
for(int i=0; i<6; ++i)
{
// 当前线程A加锁成功
// 其余的线程阻塞
pthread_mutex_lock(&mutex);
// 锁被锁住了, A线程阻塞
pthread_mutex_lock(&mutex);
....
.....
pthread_mutex_unlock(&mutex);
}
}
// 隐藏的比较深的情况
void funcA()
{
for(int i=0; i<6; ++i)
{
// 当前线程A加锁成功
// 其余的线程阻塞
pthread_mutex_lock(&mutex);
....
.....
pthread_mutex_unlock(&mutex);
}
}
void funcB()
{
for(int i=0; i<6; ++i)
{
// 当前线程A加锁成功
// 其余的线程阻塞
pthread_mutex_lock(&mutex);
funcA(); // 重复加锁
....
.....
pthread_mutex_unlock(&mutex);
}
}
场景描述:
1. 有两个共享资源:X, Y,X对应锁A, Y对应锁B
- 线程A访问资源X, 加锁A
- 线程B访问资源Y, 加锁B
2. 线程A要访问资源Y, 线程B要访问资源X,因为资源X和Y已经被对应的锁锁住了,因此这个两个线程被阻塞
- 线程A被锁B阻塞了, 无法打开A锁
- 线程B被锁A阻塞了, 无法打开B锁
读写锁是互斥锁的升级版,在做读操作的时候可以提高程序的执行效率,如果所有的线程都是做读操作, 那么读是并行的
,但是使用互斥锁,读操作也是串行的。
读写锁是一把锁,锁的类型为 pthread_rwlock_t
,有了类型之后就可以创建一把互斥锁了:
声明代码:pthread_rwlock_t rwlock;
之所以称其为读写锁,是因为这把锁既可以锁定读操作,也可以锁定写操作。为了方便理解,可以大致认为在这把锁中记录了这些信息:
读写锁的使用方式也互斥锁的使用方式是完全相同的:
并行的
,读锁是共享的
。串行的
,写锁是独占的
。访问写锁临界区的线程继续运行
,访问读锁临界区的线程阻塞
,因为写锁比读锁的优先级高。如果说程序中所有的线程都对共享资源做写操作,使用读写锁没有优势,和互斥锁是一样的,如果说程序中所有的线程都对共享资源有写也有读操作,并且对共享资源读的操作越多,读写锁更有优势。
#include
pthread_rwlock_t rwlock;
// 初始化读写锁
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,
const pthread_rwlockattr_t *restrict attr);
// 释放读写锁占用的系统资源
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
rwlock
: 读写锁的地址,传出参数attr
: 读写锁属性,一般使用默认属性,指定为 NULL// 在程序中对读写锁加读锁, 锁定的是读操作
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
**作用:**调用这个函数,如果读写锁是打开的,那么加锁成功;如果读写锁已经锁定了读操作,调用这个函数依然可以加锁成功,因为读锁是共享的;如果读写锁已经锁定了写操作,调用这个函数的线程会被阻塞。
// 这个函数可以有效的避免死锁
// 如果加读锁失败, 不会阻塞当前线程, 直接返回错误号
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
**作用:**调用这个函数,如果读写锁是打开的,那么加锁成功;如果读写锁已经锁定了读操作,调用这个函数依然可以加锁成功,因为读锁是共享的;如果读写锁已经锁定了写操作,调用这个函数加锁失败,对应的线程不会被阻塞,可以在程序中对函数返回值进行判断,添加加锁失败之后的处理动作。
// 在程序中对读写锁加写锁, 锁定的是写操作
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
**作用:**调用这个函数,如果读写锁是打开的,那么加锁成功;如果读写锁已经锁定了读操作或者锁定了写操作,调用这个函数的线程会被阻塞。
// 这个函数可以有效的避免死锁
// 如果加写锁失败, 不会阻塞当前线程, 直接返回错误号
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
**作用:**调用这个函数,如果读写锁是打开的,那么加锁成功;如果读写锁已经锁定了读操作或者锁定了写操作,调用这个函数加锁失败,但是线程不会阻塞,可以在程序中对函数返回值进行判断,添加加锁失败之后的处理动作。
// 解锁, 不管锁定了读还是写都可用解锁
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
#include
#include
#include
#include
#include
#include
#include
#define MAX 10
// 全局变量
int number = 0;
int _write_num = 0;
int _read_num = 0;
// 定义一个互斥锁
pthread_rwlock_t rwlock;
// 线程处理函数
void* read_num(void* arg)
{
while(1)
{
pthread_rwlock_rdlock(&rwlock);
printf("Thread read, id = %lu, number = %d\n", pthread_self(), number);
pthread_rwlock_unlock(&rwlock);
usleep(rand() % 100);
if (number == 30) break;
_read_num++;
}
return NULL;
}
void* write_num(void* arg)
{
for(int i=0; i<MAX; ++i)
{
pthread_rwlock_wrlock(&rwlock);
int cur = number;
cur++;
number = cur;
printf("Thread write, id = %lu, number = %d\n", pthread_self(), number);
_write_num++;
pthread_rwlock_unlock(&rwlock);
usleep(rand() % 100);
}
return NULL;
}
int main(int argc, const char* argv[])
{
pthread_t p1[5], p2[3];
// 初始化互斥锁
pthread_rwlock_init(&rwlock, NULL);
for(int i = 0; i < 5; i++)
{
pthread_create(&p1[i], NULL, read_num, NULL);
}
for(int i = 0; i < 3; i++)
{
pthread_create(&p2[i], NULL, write_num, NULL);
}
// 阻塞,资源回收
for(int i = 0; i < 3; i++)
{
pthread_join(p2[i], NULL);
}
for(int i = 0; i < 5; i++)
{
pthread_join(p1[i], NULL);
}
printf("读线程运行了%d次\n", _read_num);
printf("写线程运行了%d次\n", _write_num);
// 销毁互斥锁
pthread_rwlock_destroy(&rwlock);
return 0;
}
运行结果如下:
Thread read, id = 140060088956672, number = 0
Thread read, id = 140059957262080, number = 0
Thread read, id = 140060080563968, number = 0
Thread read, id = 140060063778560, number = 0
Thread write, id = 140060055385856, number = 1
Thread read, id = 140060072171264, number = 1
Thread write, id = 140060046993152, number = 2
Thread read, id = 140059957262080, number = 2
Thread write, id = 140060038600448, number = 3
Thread read, id = 140060080563968, number = 3
Thread read, id = 140060063778560, number = 3
Thread read, id = 140060088956672, number = 3
Thread write, id = 140060055385856, number = 4
Thread read, id = 140060080563968, number = 4
Thread read, id = 140059957262080, number = 4
Thread read, id = 140060088956672, number = 4
Thread read, id = 140060072171264, number = 4
Thread read, id = 140060063778560, number = 4
Thread write, id = 140060055385856, number = 5
Thread read, id = 140059957262080, number = 5
Thread read, id = 140060072171264, number = 5
Thread read, id = 140060080563968, number = 5
Thread read, id = 140059957262080, number = 5
Thread write, id = 140060046993152, number = 6
Thread read, id = 140060072171264, number = 6
Thread read, id = 140060088956672, number = 6
Thread read, id = 140060063778560, number = 6
Thread write, id = 140060038600448, number = 7
Thread read, id = 140059957262080, number = 7
Thread write, id = 140060055385856, number = 8
Thread read, id = 140060080563968, number = 8
Thread write, id = 140060046993152, number = 9
Thread read, id = 140060072171264, number = 9
Thread read, id = 140059957262080, number = 9
Thread write, id = 140060046993152, number = 10
Thread read, id = 140060080563968, number = 10
Thread write, id = 140060055385856, number = 11
Thread read, id = 140060080563968, number = 11
Thread read, id = 140060088956672, number = 11
Thread read, id = 140060063778560, number = 11
Thread read, id = 140060072171264, number = 11
Thread read, id = 140060088956672, number = 11
Thread read, id = 140060080563968, number = 11
Thread read, id = 140059957262080, number = 11
Thread read, id = 140060072171264, number = 11
Thread write, id = 140060055385856, number = 12
Thread write, id = 140060046993152, number = 13
Thread read, id = 140059957262080, number = 13
Thread read, id = 140060072171264, number = 13
Thread read, id = 140060088956672, number = 13
Thread write, id = 140060038600448, number = 14
Thread write, id = 140060046993152, number = 15
Thread read, id = 140060080563968, number = 15
Thread read, id = 140060063778560, number = 15
Thread read, id = 140059957262080, number = 15
Thread write, id = 140060055385856, number = 16
Thread read, id = 140060080563968, number = 16
Thread read, id = 140060072171264, number = 16
Thread read, id = 140060088956672, number = 16
Thread write, id = 140060046993152, number = 17
Thread write, id = 140060055385856, number = 18
Thread write, id = 140060038600448, number = 19
Thread read, id = 140059957262080, number = 19
Thread read, id = 140060072171264, number = 19
Thread read, id = 140060063778560, number = 19
Thread read, id = 140060088956672, number = 19
Thread write, id = 140060046993152, number = 20
Thread read, id = 140060063778560, number = 20
Thread read, id = 140060080563968, number = 20
Thread read, id = 140059957262080, number = 20
Thread read, id = 140060072171264, number = 20
Thread write, id = 140060055385856, number = 21
Thread read, id = 140060080563968, number = 21
Thread read, id = 140059957262080, number = 21
Thread read, id = 140060072171264, number = 21
Thread read, id = 140060088956672, number = 21
Thread read, id = 140060063778560, number = 21
Thread write, id = 140060046993152, number = 22
Thread read, id = 140060080563968, number = 22
Thread write, id = 140060055385856, number = 23
Thread read, id = 140060088956672, number = 23
Thread read, id = 140060063778560, number = 23
Thread read, id = 140059957262080, number = 23
Thread read, id = 140060072171264, number = 23
Thread write, id = 140060038600448, number = 24
Thread read, id = 140060088956672, number = 24
Thread read, id = 140060080563968, number = 24
Thread read, id = 140060063778560, number = 24
Thread write, id = 140060046993152, number = 25
Thread read, id = 140060072171264, number = 25
Thread read, id = 140059957262080, number = 25
Thread read, id = 140060080563968, number = 25
Thread write, id = 140060038600448, number = 26
Thread read, id = 140060088956672, number = 26
Thread read, id = 140060063778560, number = 26
Thread write, id = 140060038600448, number = 27
Thread read, id = 140059957262080, number = 27
Thread read, id = 140060080563968, number = 27
Thread read, id = 140060072171264, number = 27
Thread read, id = 140060063778560, number = 27
Thread read, id = 140060088956672, number = 27
Thread read, id = 140059957262080, number = 27
Thread read, id = 140060072171264, number = 27
Thread read, id = 140060063778560, number = 27
Thread read, id = 140060080563968, number = 27
Thread read, id = 140060088956672, number = 27
Thread write, id = 140060038600448, number = 28
Thread read, id = 140060080563968, number = 28
Thread read, id = 140059957262080, number = 28
Thread read, id = 140060080563968, number = 28
Thread read, id = 140060072171264, number = 28
Thread read, id = 140060088956672, number = 28
Thread read, id = 140060063778560, number = 28
Thread write, id = 140060038600448, number = 29
Thread read, id = 140060080563968, number = 29
Thread read, id = 140060072171264, number = 29
Thread read, id = 140059957262080, number = 29
Thread read, id = 140060072171264, number = 29
Thread read, id = 140060080563968, number = 29
Thread read, id = 140059957262080, number = 29
Thread read, id = 140060072171264, number = 29
Thread read, id = 140059957262080, number = 29
Thread read, id = 140060088956672, number = 29
Thread write, id = 140060038600448, number = 30
读线程运行了89次
写线程运行了30次
可以看到我们起了三个读线程是运行了89次,但是我们起了五个写线程却只运行30次,可以看出如果我们用读锁锁住了临界区,代码的运行是并行的,我们用写锁锁住了临界区,代码运行时串行的
严格意义上来说,条件变量的主要作用不是处理线程同步,而是进行线程的阻塞。如果在多线程程序中只使用条件变量无法实现线程的同步,必须要配合互斥锁来使用。虽然条件变量和互斥锁都能阻塞线程,但是二者的效果是不一样的,二者的区别如下:
一般情况下条件变量用于处理生产者和消费者模型,并且和互斥锁配合使用。条件变量类型对应的类型为 pthread_cond_t
,这样就可以定义一个条件变量类型的变量了:
pthread_cond_t cond;
被条件变量阻塞的线程的线程信息会被记录到这个变量中,以便在解除阻塞的时候使用。
#include
pthread_cond_t cond;
// 初始化
int pthread_cond_init(pthread_cond_t *restrict cond,
const pthread_condattr_t *restrict attr);
// 销毁释放资源
int pthread_cond_destroy(pthread_cond_t *cond);
cond
:条件变量的地址attr
:条件变量属性,一般使用默认属性,指定为 NULL// 线程阻塞函数, 哪个线程调用这个函数, 哪个线程就会被阻塞
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
通过函数原型可以看出,该函数在阻塞线程的时候,需要一个互斥锁参数,这个互斥锁主要功能是进行线程同步,让线程顺序进入临界区,避免出现数共享资源的数据混乱。该函数会对这个互斥锁做以下几件事情:
mutex
上锁,那么会将这把锁打开,这样做是为了避免死锁mutex
互斥锁锁上,继续向下访问临界区// 表示的时间是从1971.1.1到某个时间点的时间, 总长度使用秒/纳秒表示
struct timespec {
time_t tv_sec; /* Seconds */
long tv_nsec; /* Nanoseconds [0 .. 999999999] */
};
// 将线程阻塞一定的时间长度, 时间到达之后, 线程就解除阻塞了
int pthread_cond_timedwait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);
这个函数的前两个参数和 pthread_cond_wait
函数是一样的,第三个参数表示线程阻塞的时长,但是需要额外注意一点:struct timespec
这个结构体中记录的时间是从1971.1.1
到某个时间点的时间,总长度使用秒/纳秒
表示。因此赋值方式相对要麻烦一点:
time_t mytim = time(NULL); // 1970.1.1 0:0:0 到当前的总秒数
struct timespec tmsp;
tmsp.tv_nsec = 0;
tmsp.tv_sec = time(NULL) + 100; // 线程阻塞100s
// 唤醒阻塞在条件变量上的线程, 至少有一个被解除阻塞
int pthread_cond_signal(pthread_cond_t *cond);
// 唤醒阻塞在条件变量上的线程, 被阻塞的线程全部解除阻塞
int pthread_cond_broadcast(pthread_cond_t *cond);
调用上面两个函数中的任意一个,都可以换线被 pthread_cond_wait
或者 pthread_cond_timedwait
阻塞的线程,区别就在于 pthread_cond_signal
是唤醒至少一个被阻塞的线程(总个数不定),pthread_cond_broadcast
是唤醒所有被阻塞的线程。
#include
#include
#include
#include
#include
pthread_cond_t cond;
pthread_mutex_t mutex;
// 节点
typedef struct Node
{
int number;
struct Node* next;
}node;
// 链表头节点
node* head = NULL;
// 生产者
void* producer(void* arg)
{
while(1)
{
pthread_mutex_lock(&mutex);
node *new_node = (struct Node *) malloc(sizeof(node));
new_node->number = rand() % 1000;
new_node->next = head;
head = new_node;
printf("生产者, id: %ld number: %d\n", pthread_self(), new_node->number);
pthread_mutex_unlock(&mutex);
pthread_cond_broadcast(&cond);
sleep(rand() % 3);
}
return NULL;
}
// 消费者
void* consumer(void* arg)
{
while(1)
{
pthread_mutex_lock(&mutex);
while (head == NULL)
{
// 阻塞消费者线程
pthread_cond_wait(&cond, &mutex);
}
node* _node = head;
printf("消费者, id:%ld, number: %d\n", pthread_self(), _node->number);
head = head->next;
free(_node);
pthread_mutex_unlock(&mutex);
sleep(rand() % 3);
}
return NULL;
}
int main()
{
pthread_mutex_init(&mutex, NULL);
pthread_cond_init(&cond, NULL);
pthread_t t1[5], t2[5];
for(int i = 0; i < 5; i++)
{
pthread_create(&t1[i], NULL, producer, NULL);
}
for(int i = 0; i < 5; i++)
{
pthread_create(&t2[i], NULL, consumer, NULL);
}
for(int i = 0; i < 5; i++)
{
pthread_join(t1[i], NULL);
}
for(int i = 0; i < 5; i++)
{
pthread_join(t2[i], NULL);
}
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&cond);
return 0;
}
让我们来了解一下这段代码,假设我们开始所有消费者先抢到了cpu时间片,这种情况下生产者是还没有生产东西的,链表是没有任何的节点的,其中第一个消费者它抢到了这把互斥锁并且加锁成功了,那么这时第一个消费者就会被堵塞在这个代码段
while (head == NULL) { // 阻塞消费者线程 pthread_cond_wait(&cond, &mutex); }
其他的消费者线程虽然也抢到了cpu的时间片,但是在执行下面这行代码的时候由于互斥锁已经被锁住,就会堵塞
而后生产者抢到了cpu的时间片,但是在执行下面这行代码也会堵塞,这时就出现了
死锁
pthread_mutex_lock(&mutex);
这时就体现了条件变量的功能所在了,我们调用了这个api,它的其中一个功能就是在阻塞线程时候,如果线程已经对互斥锁
mutex
上锁,那么会将这把锁打开,这样做是为了避免死锁,这样我们就避免了上面这种情况,所以我们才说在生产者消费者这种情况下。我们采用条件变量+互斥锁
这种组合方法来食用。然后它还有第二个功能:当线程解除阻塞的时候,被唤醒的一个或几个消费者线程会抢这个锁,函数内部会帮助这个抢到cpu时间片的线程再次将这个
mutex
互斥锁锁上,继续向下访问临界区,而没有抢到的消费者线程就会继续堵塞在这个函数pthread_cond_wait(&cond, &mutex);
还有一个注意点就是我们在生产者函数中每生产一个节点我们就调用一次
pthread_cond_broadcast(&cond);
这个函数以通知消费者要来消费了,如果此时已经有节点也不会影响代码的执行
信号量用在多线程多任务同步的,一个线程完成了某一个动作就通过信号量告诉别的线程,别的线程再进行某些动作。信号量不一定是锁定某一个资源,而是流程上的概念,比如:有 A,B 两个线程,B 线程要等 A 线程完成某一任务以后再进行自己下面的步骤,这个任务并不一定是锁定某一资源,还可以是进行一些计算或者数据处理之类。
信号量(信号灯)
与互斥锁和条件变量的主要不同在于” 灯” 的概念,灯亮则意味着资源可用,灯灭则意味着不可用。信号量主要阻塞线程,不能完全保证线程安全,如果要保证线程安全,需要信号量和互斥锁一起使用。
信号量和条件变量一样用于处理生产者和消费者模型,用于阻塞生产者线程或者消费者线程的运行。信号的类型为 sem_t
对应的头文件为
#include
sem_t sem;
#include
// 初始化信号量/信号灯
int sem_init(sem_t *sem, int pshared, unsigned int value);
// 资源释放, 线程销毁之后调用这个函数即可
// 参数 sem 就是 sem_init() 的第一个参数
int sem_destroy(sem_t *sem);。
参数:
sem:信号量变量地址
pshared:
value:初始化当前信号量拥有的资源数(>=0),如果资源数为 0,线程就会被阻塞了。
// 参数 sem 就是 sem_init() 的第一个参数
// 函数被调用sem中的资源就会被消耗1个, 资源数-1
int sem_wait(sem_t *sem);
当线程调用这个函数,并且 sem
中的资源数 >0,线程不会阻塞,线程会占用 sem
中的一个资源,因此资源数 -1
,直到 sem
中的资源数减为 0
时,资源被耗尽,因此线程也就被阻塞了。
// 参数 sem 就是 sem_init() 的第一个参数
// 函数被调用sem中的资源就会被消耗1个, 资源数-1
int sem_trywait(sem_t *sem);
当线程调用这个函数,并且 sem
中的资源数 >0
,线程不会阻塞,线程会占用 sem
中的一个资源,因此资源数 - 1,直到 sem
中的资源数减为0
时,资源被耗尽,但是线程不会被阻塞,直接返回错误号,因此可以在程序中添加判断分支,用于处理获取资源失败之后的情况。
// 表示的时间是从1971.1.1到某个时间点的时间, 总长度使用秒/纳秒表示
struct timespec {
time_t tv_sec; /* Seconds */
long tv_nsec; /* Nanoseconds [0 .. 999999999] */
};
// 调用该函数线程获取sem中的一个资源,当资源数为0时,线程阻塞,在阻塞abs_timeout对应的时长之后,解除阻塞。
// abs_timeout: 阻塞的时间长度, 单位是s, 是从1970.1.1开始计算的
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
该函数的参数 abs_timeout
和 pthread_cond_timedwait
的最后一个参数是一样的,使用方法不再过多赘述。当线程调用这个函数,并且 sem
中的资源数 >0,线程不会阻塞,线程会占用sem
中的一个资源,因此资源数 -1,直到 sem
中的资源数减为 0 时,资源被耗尽,线程被阻塞,当阻塞指定的时长之后,线程解除阻塞。
// 调用该函数给sem中的资源数+1
int sem_post(sem_t *sem);
调用该函数会将 sem
中的资源数 +1,如果有线程在调用 sem_wait
、sem_trywait
、sem_timedwait
时因为 sem
中的资源数为 0 被阻塞了,这时这些线程会解除阻塞,获取到资源之后继续向下运行。
// 查看信号量 sem 中的整形数的当前值, 这个值会被写入到sval指针对应的内存中
// sval是一个传出参数
int sem_getvalue(sem_t *sem, int *sval);
通过这个函数可以查看 sem
中现在拥有的资源个数,通过第二个参数 sval
将数据传出,也就是说第二个参数的作用和返回值是一样的。
场景描述:使用信号量实现生产者和消费者模型,生产者有 5 个,往链表头部添加节点,消费者也有 5 个,删除链表头部的节点。
如果生产者和消费者线程使用的信号量对应的总资源数为 1,那么不管线程有多少个,可以工作的线程只有一个,其余线程由于拿不到资源,都被迫阻塞了。
#include
#include
#include
#include
#include
#include
pthread_mutex_t mutex;
// 生产者的信号量
sem_t sem_pro;
// 消费者的信号
sem_t semc_cum;
// 节点
typedef struct Node
{
int number;
struct Node* next;
}node;
// 链表头节点
node* head = NULL;
// 生产者
void* producer(void* arg)
{
while(1)
{
sem_wait(&sem_pro);
pthread_mutex_lock(&mutex);
node *new_node = (struct Node *) malloc(sizeof(node));
new_node->number = rand() % 1000;
new_node->next = head;
head = new_node;
printf("生产者, id: %ld number: %d\n", pthread_self(), new_node->number);
pthread_mutex_unlock(&mutex);
sem_post(&semc_cum);
sleep(rand() % 3);
}
return NULL;
}
// 消费者
void* consumer(void* arg)
{
while(1)
{
sem_wait(&semc_cum);
pthread_mutex_lock(&mutex);
node* _node = head;
printf("消费者, id:%ld, number: %d\n", pthread_self(), _node->number);
head = head->next;
free(_node);
pthread_mutex_unlock(&mutex);
sem_post(&sem_pro);
sleep(rand() % 3);
}
return NULL;
}
int main()
{
sem_init(&sem_pro, 0, 1);
sem_init(&semc_cum, 0, 0);
pthread_t t1[5], t2[5];
for(int i = 0; i < 5; i++)
{
pthread_create(&t1[i], NULL, producer, NULL);
}
for(int i = 0; i < 5; i++)
{
pthread_create(&t2[i], NULL, consumer, NULL);
}
for(int i = 0; i < 5; i++)
{
pthread_join(t1[i], NULL);
}
for(int i = 0; i < 5; i++)
{
pthread_join(t2[i], NULL);
}
sem_destroy(&sem_pro);
sem_destroy(&semc_cum);
return 0;
}
生产者, id: 140119012058880 number: 383
消费者, id:140118970095360, number: 383
生产者, id: 140119003666176 number: 777
消费者, id:140118744626944, number: 777
生产者, id: 140118995273472 number: 386
消费者, id:140118961702656, number: 386
生产者, id: 140118995273472 number: 649
消费者, id:140118870451968, number: 649
......
通过测试代码可以得到如下结论:如果生产者和消费者使用的信号量总资源数为 1,那么不会出现生产者线程和消费者线程同时访问共享资源的情况,不管生产者和消费者线程有多少个,它们都是顺序执行的。
如果生产者和消费者线程使用的信号量对应的总资源数为大于 1,这种场景下出现的情况就比较多了:
以上不管哪一种情况都可能会出现多个线程访问共享资源的情况,如果想防止共享资源出现数据混乱,那么就需要使用互斥锁进行线程同步,处理代码如下:
#include
#include
#include
#include
#include
#include
// 链表的节点
struct Node
{
int number;
struct Node* next;
};
// 生产者线程信号量
sem_t psem;
// 消费者线程信号量
sem_t csem;
// 互斥锁变量
pthread_mutex_t mutex;
// 指向头结点的指针
struct Node * head = NULL;
// 生产者的回调函数
void* producer(void* arg)
{
// 一直生产
while(1)
{
// 生产者拿一个信号灯
sem_wait(&psem);
// 加锁, 这句代码放到 sem_wait()上边, 有可能会造成死锁
pthread_mutex_lock(&mutex);
// 创建一个链表的新节点
struct Node* pnew = (struct Node*)malloc(sizeof(struct Node));
// 节点初始化
pnew->number = rand() % 1000;
// 节点的连接, 添加到链表的头部, 新节点就新的头结点
pnew->next = head;
// head指针前移
head = pnew;
printf("+++producer, number = %d, tid = %ld\n", pnew->number, pthread_self());
pthread_mutex_unlock(&mutex);
// 通知消费者消费
sem_post(&csem);
// 生产慢一点
sleep(rand() % 3);
}
return NULL;
}
// 消费者的回调函数
void* consumer(void* arg)
{
while(1)
{
sem_wait(&csem);
pthread_mutex_lock(&mutex);
struct Node* pnode = head;
printf("--consumer: number: %d, tid = %ld\n", pnode->number, pthread_self());
head = pnode->next;
// 取出链表的头结点, 将其删除
free(pnode);
pthread_mutex_unlock(&mutex);
// 通知生产者生成, 给生产者加信号灯
sem_post(&psem);
sleep(rand() % 3);
}
return NULL;
}
int main()
{
// 初始化信号量
sem_init(&psem, 0, 5); // 生成者线程一共有5个信号灯
sem_init(&csem, 0, 0); // 消费者线程一共有0个信号灯
// 初始化互斥锁
pthread_mutex_init(&mutex, NULL);
// 创建5个生产者, 5个消费者
pthread_t ptid[5];
pthread_t ctid[5];
for(int i=0; i<5; ++i)
{
pthread_create(&ptid[i], NULL, producer, NULL);
}
for(int i=0; i<5; ++i)
{
pthread_create(&ctid[i], NULL, consumer, NULL);
}
// 释放资源
for(int i=0; i<5; ++i)
{
pthread_join(ptid[i], NULL);
}
for(int i=0; i<5; ++i)
{
pthread_join(ctid[i], NULL);
}
sem_destroy(&psem);
sem_destroy(&csem);
pthread_mutex_destroy(&mutex);
return 0;
}
在编写上述代码的时候还有一个需要注意是事项,不管是消费者线程的处理函数还是生产者线程的处理函数内部有这么两行代码:
// 消费者
sem_wait(&csem);
pthread_mutex_lock(&mutex);
// 生产者
sem_wait(&csem);
pthread_mutex_lock(&mutex);
这两行代码的调用顺序是不能颠倒的,如果颠倒过来就有可能会造成死锁,下面来分析一种死锁的场景:
void* producer(void* arg)
{
// 一直生产
while(1)
{
pthread_mutex_lock(&mutex);
// 生产者拿一个信号灯
sem_wait(&psem);
......
......
// 通知消费者消费
sem_post(&csem);
pthread_mutex_unlock(&mutex);
// 生产慢一点
sleep(rand() % 3);
}
return NULL;
}
// 消费者的回调函数
void* consumer(void* arg)
{
while(1)
{
pthread_mutex_lock(&mutex);
sem_wait(&csem);
......
......
// 通知生产者生成, 给生产者加信号灯
sem_post(&psem);
pthread_mutex_unlock(&mutex);
sleep(rand() % 3);
}
return NULL;
}
int main()
{
// 初始化信号量
sem_init(&psem, 0, 5); // 生成者线程一共有5个信号灯
sem_init(&csem, 0, 0); // 消费者线程一共有0个信号灯
......
......
return 0;
}
在上面的代码中,初始化状态下消费者线程没有任务信号量资源,假设某一个消费者线程先运行,调用 pthread_mutex_lock(&mutex);
对互斥锁加锁成功,然后调用 sem_wait(&csem);
由于没有资源,因此被阻塞了。其余的消费者线程由于没有抢到互斥锁,因此被阻塞在互斥锁上。对应生产者线程第一步操作也是调用 pthread_mutex_lock(&mutex);
但是这时候互斥锁已经被消费者线程锁上了,所有生产者都被阻塞,到此为止,多余的线程都被阻塞了,程序产生了死锁。