文章目录
- 进程和线程
- 线程的操作
- 线程创建
- 线程退出
- 线程回收
- 线程分离
- 线程取消和ID比较
- 线程同步
- 互斥锁
- 死锁
- 读写锁
- 条件变量
- 信号量
进程和线程
线程是轻量级的进程,在Linux环境下线程的本质还是进程。
在计算机上运行的程序是一组指令及指令参数的组合,指令按照既定的逻辑控制计算机的运行。
操作系统以进程为单位分配资源,进程是操作系统资源分配的最小单位,进程是操作系统调度执行的最小单位。
进程和线程的区别:
1.进程有自己独立的地址空间,多个线程共用同一个地址空间。
线程更加节省系统资源,而且效率更高。在同一个地址空间中,每个线程独享的资源有:自己的栈区和寄存器,多个线程共享的资源有:代码段、堆区、全局数据区、打开的文件等。
2.线程是程序的最小执行单位,进程是操作系统中最小的资源分配单位。
每个进程对应一个虚拟的地址空间,一个进程只能抢一个CPU时间片。一个地址空间中可以划分出多个线程,在有效的资源基础上,能够抢更多的CPU时间片。多线程中抢占时间片是随机的,并不是按照顺序进行的。
3.关于CPU的调度和切换,线程的上下文切换要比进程快得多。
上下文切换指的是进程或者线程在分时复用CPU时间片时,在切换之前会将上一个任务的状态进行保存,下次切换回这个任务的时候,从寄存器中加载这个状态继续运行。任务从保存到再次加载的这个过程就是一次上下文切换。
4.线程更加廉价,启动速度更快,退出也快,对系统资源的冲击小。
在处理多任务程序的时候,使用多线程比使用多进程要有优势,但线程并不是越多越好。在进行文件I/O操作的时候,其对CPU的使用率不高,此时线程等于2倍的CPU核心数时效率最高。而在处理复杂算法的时候,线程的个数等于CPU核心数时,效率是最高的。
5.一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉,所以多进程要比多线程健壮。
线程的操作
线程创建
线程的创建使用的是pthread_create函数,该函数的原型如下。
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
thread用来保存新创建的线程ID;attr为线程属性,设置为NULL表示所有属性设置为默认值;新创建的线程从start_routine()函数开始运行;arg是传递给start_routine()函数的参数。线程创建成功函数就返回0,创建失败将返回一个错误号。
含有pthread_create函数时,在编译的时候需要加上-lpthread,使用-l选项指定链接库pthread,因为pthread不在gcc的默认链接库中,所以需要手动指定。
线程退出
多线程程序中,如果想要让线程退出,但是不会导致虚拟地址空间的释放,就可以调用线程库中的线程退出函数pthread_exit(),该函数原型如下。
void pthread_exit(void *retval); //retval是线程退出时携带的数据,当前子线程的主线程会得到该数据,不需要使用时就设置为NULL。
调用该函数后,当前线程马上就退出了,但是不会影响到其他线程的正常运行,其在主线程和子线程中都可以使用。
下面是一个线程创建的例子。
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
void* ThreadFun(void *arg)
{
for(int i=0;i<5;i++)
printf("子线程:i=%d ",i);
printf("子线程:%ld\n",pthread_self());
if(arg != NULL)
printf("arg = %s\n",(char*)arg); //参数不为空就打印
return NULL;
}
int main()
{
char *c = "abcdefg";
pthread_t tid;
pthread_create(&tid,NULL,ThreadFun,NULL); //不传参
for(int i=0;i<5;i++)
printf("主线程:i=%d ",i);
printf("主线程:%ld\n",pthread_self());
pthread_create(&tid,NULL,ThreadFun,(void*)c); //传参
printf("主线程2:%ld\n",pthread_self());
pthread_exit(NULL);
//sleep(1); //保证主线程等到子线程执行完毕后再退出
return 0;
}
将上面程序编译后运行,结果如下图所示。
可以看到,同一个程序执行两次的结果不一样,这是因为时间片的抢占是随机的。
如果程序中既不加sleep()函数,也不加pthread_exit()函数,那么第二个子线程可能还没来得及执行,主线程的地址空间就被释放了,具体的执行结果如下图所示。
线程回收
线程和进程一样,子线程退出的时候其内核资源主要由主线程回收,线程库中提供的线程回收函数是pthread_join(),该函数的原型如下。
int pthread_join(pthread_t thread, void **retval);
thread是要回收的子线程的ID,retval是一个二级指针,其指向一级指针的地址,该地址中存储的是线程退出函数pthread_exit()传递出来的数据,如果不需要该参数,将该参数设置为NULL。线程回收成功返回0,回收失败则返回一个错误号。
pthread_join()函数是一个阻塞函数,如果还有子线程在运行,调用该函数就会阻塞,子线程退出,函数解除阻塞进行资源的回收,函数调用一次只能回收一个子线程,如果有多个子线程则需要循环进行回收。
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
typedef struct
{
int age;
char* name;
}Test;
Test t; //1.全局变量
void* ThreadFun(void *arg)
{
printf("子线程:%ld\n",pthread_self());
//static Test t; //2.静态变量
t.age = 10;
t.name = "zhangsan";
pthread_exit(&t); //t是结构体变量,此时应当取其地址
return NULL;
}
int main()
{
pthread_t tid;
pthread_create(&tid,NULL,ThreadFun,NULL);
printf("主线程:%ld\n",pthread_self());
void *p;
pthread_join(tid,&p);
Test *pt = (Test *)p;
printf("age = %d name = %s\n",pt->age,pt->name);
return 0;
}
函数体中定义的临时变量是存放在栈区的,因此在定义结构体变量的时候应该定义为全局变量或者静态变量,这样就能确保得到的数据是正确的。
上面的实现过程还有一种方法,将结构体变量定义在主函数中,然后将变量的地址通过pthread_create()的第四个参数进行传递,然后在函数体中将该类型进行强制转换,赋值后通过pthread_exit()函数退出子线程,主线程中通过pthread_join()函数接收,第二个参数设置为空。
具体的代码如下。
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
typedef struct
{
int age;
char* name;
}Test;
void* ThreadFun(void *arg)
{
printf("子线程:%ld\n",pthread_self());
Test *t = (Test *)arg;
t->age = 10;
t->name = "zhangsan";
pthread_exit(t); //t是指针变量,直接传
return NULL;
}
int main()
{
Test t;
pthread_t tid;
pthread_create(&tid,NULL,ThreadFun,&t); //将结构体变量的地址放在第四个参数位置
printf("主线程:%ld\n",pthread_self());
pthread_join(tid,NULL);
printf("age = %d name = %s\n",t.age,t.name);
return 0;
}
线程分离
程序中的主线程一般都有属于自己的业务处理流程,如果让主线程负责子线程的资源回收,使用函数pthread_join(),但是如果子线程不退出,主线程就会一直被阻塞,主线程中的任务就不能被执行了。
线程库函数中提供了线程分离函数pthread_detach()函数,该函数原型如下。
int pthread_detach(pthread_t thread); //thread是要分离的线程ID
调用这个函数后,指定的子线程就会和主线程分离,当子线程退出的时候,其占用的内核资源就被系统的其他进程接管并回收了。线程分离之后在主线程中使用pthread_join()函数就回收不到子线程的资源了。
在上面程序的基础上,在主函数中加入线程分离函数pthread_detach()。
int main()
{
Test t;
pthread_t tid;
pthread_create(&tid,NULL,ThreadFun,&t);
printf("主线程:%ld\n",pthread_self());
pthread_detach(tid);
pthread_join(tid,NULL);
printf("age = %d name = %s\n",t.age,t.name);
pthread_exit(NULL);
return 0;
}
再次运行程序,可以看到,主线程就回收不到子线程的资源了。
线程取消和ID比较
线程取消指的是在某些特定的情况下,在一个线程中杀死另一个线程,使用的函数是pthread_cancel(),该函数的原型如下。
int pthread_cancel(pthread_t thread); //thread是要杀死的线程ID
杀死一个线程需要分两步:第一步是在线程A中调用线程取消函数,指定要杀死的线程B;第二步是在线程B中进行一次系统调用,即从用户区切换到内核区。这两步都完成,线程B才能被取消。
线程ID的比较使用的函数是pthread_equal()函数,该函数的原型如下。
int pthread_equal(pthread_t t1,pthread_t t2); //相等返回非0值,不相等返回0
在Linux中,线程ID是一个无符号的长整型,因此可以直接使用比较操作符比较两个线程的ID,但是线程库是可以跨平台使用的,在有些平台上可能不是一个单纯的整型,这种情况下比较两个线程的ID就需要用到线程比较函数。
线程同步
创建两个线程访问全局变量,代码如下。
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#define MAX 50
int number = 0; //全局变量,共享资源
void* CountNum1(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* CountNum2(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()
{
pthread_t tid_a,tid_b;
pthread_create(&tid_a,NULL,CountNum1,NULL);
pthread_create(&tid_b,NULL,CountNum2,NULL);
pthread_join(tid_a,NULL);
pthread_join(tid_b,NULL);
return 0;
}
上面代码运行后,其中部分运行结果如下图所示。
可以看到线程A和线程B在访问全局变量的时候,会有重复,这是因为线程在抢占到了CPU时间片后会从物理内存中读取数据,物理内存和CPU寄存器之间一般设有三级缓存。
上一个线程在失去时间片之后,还没来得及将最新一次的数据写回物理内存,这样切换到另一个线程计数时,就会发生计数重复。
对于多个线程访问共享资源出现数据混乱的问题,需要进行线程同步,常用的线程同步方式有:互斥锁、读写锁、条件变量、信号量。共享资源就是多个线程共同访问的变量,这些变量通常为全局数据区变量或者堆区变量,这些变量对应的共享资源也称为临界资源。
互斥锁
互斥锁是线程同步最常用的一种方式,通过互斥锁可以锁定一个代码块,被锁定的代码块只能被锁定它的线程解锁。所有的线程只能顺序执行,不能并行处理,这样多线程访问共享资源数据混乱的问题就可以被解决了,付出的代价是执行效率的降低,因为默认临界区多个线程是可以并行处理的,现在只能串行处理。
互斥锁的类型为pthread_mutex_t,在创建的锁对象中保存了当前锁的状态信息,锁定或者打开,如果是锁定状态,其中还记录了给这把锁加锁的线程ID。一个互斥锁变量只能被一个线程锁定,被锁定之后其他线程再对互斥变量加锁就会被阻塞,直到这把互斥锁被解锁,被阻塞的线程才能被解除阻塞。一般情况下,每一个共享资源对应一把互斥锁,锁的个数和线程的个数无关。
互斥锁的操作函数原型如下。
pthread_mutex_t mutex;
int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr); //初始化互斥锁,attr一般设置为NULL
//restrict是一个关键字,用来修饰指针,只有这个关键字修饰的指针可以访问指向的内存地址
int pthread_mutex_destroy(pthread_mutex_t *mutex); //释放互斥锁资源
int pthread_mutex_lock(pthread_mutex_t *mutex); //上锁
pthread_mutex_lock()函数被调用,首先会判断参数mutex互斥锁中的状态是不是锁定状态,如果没有被锁定,当前线程就可以加锁成功,而且这个锁种会记录是哪个线程加锁成功了。如果被锁定了,其他线程加锁就失败了,这些线程都会阻塞在这把锁上。当这把锁被解开之后,这些阻塞在锁上的线程就解除阻塞了,并且这些线程是通过竞争的方式对这把锁加锁的,没抢到锁的线程会继续阻塞。
int pthread_mutex_trylock(pthread_mutex_t *mutex); //尝试加锁
调用pthread_mutex_trylock()函数对互斥锁加锁有两种情况,锁是打开的就加锁成功,如果锁变量是被锁住的,调用这个函数加锁的线程不会被阻塞,而是直接返回错误信号。
int pthread_mutex_unlock(pthread_mutex_t *mutex); //解锁
互斥锁的应用例子如下。
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#define MAX 50
int number = 0; //全局变量,共享资源
pthread_mutex_t mutex; //创建互斥锁变量
void* CountNum1(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* CountNum2(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()
{
pthread_t tid_a,tid_b;
pthread_mutex_init(&mutex,NULL); //在创建子线程之前进行互斥锁的初始化
pthread_create(&tid_a,NULL,CountNum1,NULL);
pthread_create(&tid_b,NULL,CountNum2,NULL);
pthread_join(tid_a,NULL);
pthread_join(tid_b,NULL);
pthread_mutex_destroy(&mutex); //销毁互斥锁
return 0;
}
上面程序编译后的运行结果如下图所示。
可以看到,这个时候全局变量不再重复了,是逐个增加变化的。
死锁
当多个线程访问共享资源时,就需要加锁,如果锁使用不当,就会造成死锁现象,后果是所有的线程都被阻塞,并且线程的阻塞是无法解开的,因为可以解锁的线程也被阻塞了。
造成死锁的原因:加锁之后忘记解锁;程序虽然有解锁代码,但是在解锁之前return了;重复加锁造成死锁;两个函数中分别有加锁和解锁代码,但是在一个函数的临界区调用了另一个函数;有两个临界资源,两个线程各自对一个资源上锁,但是该资源中要访问另一个资源。
死锁产生之后,程序就会阻塞,只能强制退出,运行结果如下图所示。
避免死锁的方法:检查代码,防止多次上锁;对共享资源访问完毕后一定要解锁,或者加锁的时候使用trylock()函数,这样即使有线程阻塞,尝试加锁的线程也不会被阻塞;程序中有多把锁的情况下,如果条件允许,可以控制对锁的访问顺序,在对其他互斥锁加锁之前,先释放当前线程的互斥锁;可以引入一些专门用于死锁检测的模块。
读写锁
读写锁是互斥锁的升级版,在做读操作的时候可以提高程序的执行效率,如果所有的线程都是做读操作,那么读是并行的,但是使用互斥锁,读操作也是串行的。
读写锁是一把锁,既可以锁定读操作,也可以锁定写操作,如果使用读写锁锁定了读操作,那么需要先解锁才能去锁定写操作,反之亦然。读写锁的类型为pthread_rwlock_t,锁种记录了锁的状态(锁定/打开)、锁定的操作(读操作/写操作)、锁定该锁的线程。
读写锁的特点:
1、使用读写锁的读锁锁定了临界区,线程对临界区的访问是并行的,读锁是共享的;
2、使用读写锁的写锁锁定了临界区,线程对临界区的访问是串行的,写锁是独占的,此时和互斥锁一样;
3、使用读写锁分别对两个临界区加了读锁和写锁,两个线程要同时访问者两个临界区,访问写锁临界区的线程继续运行,而访问读锁临界区的线程阻塞,因为写锁比读锁的优先级高。
如果程序中所有的线程都对共享资源做写操作,使用读写锁和互斥锁是一样的,没有体现优势;如果程序中的线程对共享资源有写也有读操作,并且对共享资源读的操作多,那么读写锁相比于互斥锁更有优势。
读写锁的相关函数原型如下。
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); //尝试加读锁
调用pthread_rwlock_rdlock()函数加读锁,如果读写锁已经锁定了读操作,调用这个函数依然可以加锁成功,因为读锁是共享的;如果读写锁已经锁定了写操作,调用这个函数的线程就会被阻塞。调用pthread_rwlock_tryrdlock()函数加读锁时,如果碰到读写锁已经锁定写操作的情况,加锁失败,但是该线程不会被阻塞。
写锁操作的相关函数原型如下。
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock); //加写锁
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock); //尝试加写锁
调用pthread_rwlock_wrlock()函数加写锁,如果读写锁是打开的,那么加锁成功;如果读写锁已经锁定了读操作或写操作,调用这个函数的线程就会被阻塞。调用pthread_rwlock_trywrlock()函数加写锁时,如果碰到读写锁已经锁定写操作的情况,加锁失败,但是该线程不会被阻塞。
不管是读锁还是写锁,解锁的函数都是一样的,解锁的函数原型如下。
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock); //解锁
读写锁使用案例:8个线程同时操作同一个全局变量,3个线程不定时的写这个全局变量,5个线程不定时的读这个全局变量。
该读写锁案例的代码如下。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#define MAX 50
int number = 0; //全局变量,共享资源
pthread_rwlock_t rwlock; //创建读写锁变量
void* read_num(void *arg)
{
for(int i=0;i<MAX;i++)
{
pthread_rwlock_rdlock(&rwlock); //加读锁
//临界区开始
printf("Read number, thread_id = %lu, number = %d\n",pthread_self(),number);
//临界区结束
pthread_rwlock_unlock(&rwlock); //解锁
usleep(5);
}
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("Write number, thread_id = %lu, number = %d\n",pthread_self(),number);
//临界区结束
pthread_rwlock_unlock(&rwlock); //解锁
usleep(rand()%5);
}
return NULL;
}
int main()
{
pthread_t tid_rd[5],tid_wr[3];
pthread_rwlock_init(&rwlock,NULL); //在创建子线程之前进行读写锁的初始化
for(int i=0;i<3;i++)
{
pthread_create(&tid_wr[i],NULL,write_num,NULL); //写线程创建
}
for(int i=0;i<5;i++)
{
pthread_create(&tid_rd[i],NULL,read_num,NULL); //读线程创建
}
for(int i=0;i<5;i++)
{
pthread_join(tid_rd[i],NULL); //读线程回收
}
for(int i=0;i<3;i++)
{
pthread_join(tid_wr[i],NULL); //写线程回收
}
pthread_rwlock_destroy(&rwlock); //销毁读写锁
return 0;
}
上面程序编译后的执行结果如下图所示。
有三个不同的线程在写数,有五个不同的线程在读数。
条件变量
条件变量的主要作用不是用来处理线程同步的,而是进行线程阻塞的。如果在多线程程序中只使用条件变量无法实现线程的同步,必须要配合互斥锁来使用。
条件变量和互斥锁都能阻塞线程,但是二者的效果是不一样的。假设有A-Z这样的26个线程,这些线程共同访问同一把互斥锁,如果线程A加锁成功,那么其余B-Z线程访问互斥锁都阻塞,所有的线程只能顺序访问临界区。条件变量只有在满足指定条件时才会阻塞线程,如果条件不满足,多个线程可以同时进入临界区,同时读写临界资源,这种情况下会出现共享资源中数据的混乱。
条件变量用于处理类似于生产者消费者模型,并且配合着互斥锁来使用。条件变量的类型为pthread_cond_t,被条件变量阻塞的线程信息会被记录在pthread_cond_t类型的变量中,其在解除阻塞的时候会使用。
条件变量相关的操作函数原型如下。
pthread_cond_t cond;
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr); //初始化
//cond是条件变量的地址,attr是条件变量的属性,一般设置为空
int pthread_cond_destroy(pthread_cond_t *cond); //销毁
int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex); //线程阻塞函数,线程调用该函数就会被阻塞
pthread_cond_wait()函数原型在阻塞线程的时候,需要一个互斥锁参数,这个互斥锁的功能是进行线程同步,让线程顺序进入临界区,避免出现共享资源的数据混乱。使用该函数阻塞线程时候,如果线程已经对互斥锁mutex上锁,那么这把锁会打开,这样做是为了避免死锁;当线程解除阻塞的时候,函数内部会帮这个线程将mutex互斥锁锁上,然后继续向下访问临界区。
struct timespec
{
time_t tv_sec; //秒
long tv_nsec; //纳秒
}; //结构体中记录的时间是从1971.01.01开始到某个时间点的时间
int pthread_cond_timedwait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex,const struct timespec *restrict abstime); //abstime表示阻塞时长
pthread_cond_timedwait()函数的作用是将线程阻塞一定的时间,阻塞到时后,线程就解除阻塞了。
time_t cur_time = time(NULL);
struct timespec tmsp;
tmsp.tv_nsec = 0;
tmsp.tv_sec = cur_time + 100; //表示线程阻塞100秒
唤醒函数原型如下。
int pthread_cond_signal(pthread_cond_t *cond); //至少唤醒一个被阻塞的线程
int pthread_cond_broadcast(pthread_cond_t *cond); //唤醒所有被阻塞的线程
条件变量案例:生产者和消费者模型,5个生产者和5个消费者,生产者往链表头部添加节点,消费者删除链表头部节点。
该条件变量的实现的代码如下。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
struct Node //定义链表
{
int number;
struct Node *next;
};
struct Node *head = NULL; //链表头结点
int max_cap = 5;
pthread_mutex_t mutex; //创建互斥锁变量
pthread_cond_t cond_p,cond_c; //创建条件变量
void* producer(void *arg)
{
while(1)
{
pthread_mutex_lock(&mutex);
//临界区开始
while(max_cap == 0)
{
pthread_cond_wait(&cond_p,&mutex); //链表满了,阻塞生产者
printf("生产已满,阻塞的生产者ID : %ld\n",pthread_self());
}
struct Node *newNode = (struct Node*)malloc(sizeof(struct Node)); //创建新节点
newNode->number = rand() % 100; //初始化新节点的属性
newNode->next = head;
head = newNode; //移动头结点的位置
printf("producer id : %ld, number = %d\n",pthread_self(),newNode->number);
max_cap--;
//临界区结束
pthread_mutex_unlock(&mutex);
pthread_cond_signal(&cond_c); //生产出产品,唤醒消费者
//sleep(1); //休眠
}
return NULL;
}
void* consumer(void *arg)
{
while(1)
{
pthread_mutex_lock(&mutex);
//临界区开始
while(head == NULL)
{
pthread_cond_wait(&cond_c,&mutex); //链表为空,阻塞消费者
printf("链表为空,阻塞的消费者ID : %ld\n",pthread_self());
}
struct Node *node = head;
printf("consumer id : %ld, number = %d\n",pthread_self(),node->number);
head = head->next; //链表头结点后移
free(node); //释放取出来的头结点
max_cap++;
//临界区结束
pthread_mutex_unlock(&mutex);
pthread_cond_signal(&cond_p); //消费了产品,唤醒生产者
//sleep(3);
}
return NULL;
}
int main()
{
pthread_t tid_p[5],tid_c[5];
pthread_mutex_init(&mutex,NULL); //初始化互斥锁
pthread_cond_init(&cond_p,NULL); //初始化条件变量
pthread_cond_init(&cond_c,NULL); //初始化条件变量
for(int i=0;i<5;i++)
{
pthread_create(&tid_p[i],NULL,producer,NULL); //创建生产者线程
}
for(int i=0;i<5;i++)
{
pthread_create(&tid_c[i],NULL,consumer,NULL); //创建消费者线程
}
for(int i=0;i<5;i++)
{
pthread_join(tid_p[i],NULL); //线程回收
pthread_join(tid_c[i],NULL);
}
pthread_mutex_destroy(&mutex); //销毁互斥锁
pthread_cond_destroy(&cond_p); //销毁条件变量
pthread_cond_destroy(&cond_c); //销毁条件变量
return 0;
}
上面程序编译后的执行结果如下图所示。
需要注意的是,线程阻塞后会将互斥锁打开,否则生产者没办法接着生产,就会产生死锁。只有阻塞的线程将互斥锁打开,生产者才能继续生产然后唤醒消费者,消费者线程被唤醒后会再次加锁向下执行。
信号量
信号量用在多线程多任务同步,一个线程完成了某一个动作就通过信号量告诉别的线程,别的线程再进行某些动作。信号量不 一定是锁定某一个资源,而是流程上的概念,比如有A,B两个线程,B线程要等A线程完成某一任务以后再进行自己下面的步 骤,这个任务并不一定是锁定某一资源,还可以是进行一些计算或者数据处理之类。
信号量(信号灯)与互斥锁和条件变量的主要不同在于”灯”的概念,灯亮则意味着资源可用,灯灭则意味着不可用。信号量主要 阻塞线程,不能完全保证线程安全,如果要保证线程安全,需要信号量和互斥锁一起使用。
使用信号量需要用到信号量的头文件,信号的类型为sem_t类型。
#include <semaphore.h>
sem_t sem;
信号量操作函数的原型如下。
int sem_init(sem_t *sem,int pshared,unsigned int value); //初始化信号量
int sem_destroy(sem_t *sem); //销毁
其中,sem是信号量变量的地址;pshared如果设置为0表示线程同步,非0则表示进程同步;value用于初始化当前信号量拥有的资源数,是一个大于等于0的数,如果为0,线程就会被阻塞。
int sem_wait(sem_t *sem); //调用该函数一次,资源数量-1,资源减至0时,线程被阻塞
int sem_trywait(sem_t *sem); //资源为0时,线程不会被阻塞
int sem_post(sem_t *sem); //调用该函数一次,资源数量+1
int sem_getvalue(sem_t *sem,int *sval); //查看信号量中整型数的当前值,值存放在sval指针对应的内存中
生产者在给自己的资源数量做减1操作后,要给消费者的资源数加1,同理,消费者在给自己的资源数做减1操作后,也要给生产者的资源数加1。
使用信号量且只有一个资源可用的情况下,可以不使用互斥锁,相应的代码如下。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <semaphore.h> //信号量头文件
struct Node //定义链表
{
int number;
struct Node *next;
};
struct Node *head = NULL; //链表头结点
sem_t sem_p,sem_c; //创建信号量变量
void* producer(void *arg)
{
while(1)
{
sem_wait(&sem_p); //生产者资源减1
struct Node *newNode = (struct Node*)malloc(sizeof(struct Node)); //创建新节点
newNode->number = rand() % 100; //初始化新节点的属性
newNode->next = head;
head = newNode; //移动头结点的位置
printf("producer id : %ld, number = %d\n",pthread_self(),newNode->number);
sem_post(&sem_c); //消费者资源加1
sleep(rand()%3); //休眠
}
return NULL;
}
void* consumer(void *arg)
{
while(1)
{
sem_wait(&sem_c); //消费者资源减1
struct Node *node = head;
printf("consumer id : %ld, number = %d\n",pthread_self(),node->number);
head = head->next; //链表头结点后移
free(node); //释放取出来的头结点
sem_post(&sem_p); //生产者资源加1
sleep(rand()%3);
}
return NULL;
}
int main()
{
sem_init(&sem_p,0,1); //生产者信号量初始化,只有一个资源
sem_init(&sem_c,0,0); //消费者信号量初始化
pthread_t tid_p[5],tid_c[5];
for(int i=0;i<5;i++)
{
pthread_create(&tid_p[i],NULL,producer,NULL); //创建生产者线程
}
for(int i=0;i<5;i++)
{
pthread_create(&tid_c[i],NULL,consumer,NULL); //创建消费者线程
}
for(int i=0;i<5;i++)
{
pthread_join(tid_p[i],NULL); //线程回收
pthread_join(tid_c[i],NULL);
}
sem_destroy(&sem_c);
sem_destroy(&sem_p);
return 0;
}
上面程序编译后的执行结果如下图所示。
可以看到,输出结果严格按照生产一个、消费一个的顺序进行着。
使用信号量但有一个以上资源可用的情况下,需要使用互斥锁,相应的代码如下。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <semaphore.h> //信号量头文件
struct Node //定义链表
{
int number;
struct Node *next;
};
struct Node *head = NULL; //链表头结点
sem_t sem_p,sem_c; //创建信号量变量
pthread_mutex_t mutex;
void* producer(void *arg)
{
while(1)
{
sem_wait(&sem_p); //生产者资源减1
pthread_mutex_lock(&mutex); //互斥锁加锁一定在信号量之后
struct Node *newNode = (struct Node*)malloc(sizeof(struct Node)); //创建新节点
newNode->number = rand() % 100; //初始化新节点的属性
newNode->next = head;
head = newNode; //移动头结点的位置
printf("producer id : %ld, number = %d\n",pthread_self(),newNode->number);
pthread_mutex_unlock(&mutex); //解锁的顺序可以随意
sem_post(&sem_c); //消费者资源加1
sleep(rand()%3); //休眠
}
return NULL;
}
void* consumer(void *arg)
{
while(1)
{
sem_wait(&sem_c); //消费者资源减1
pthread_mutex_lock(&mutex); //互斥锁加锁一定在信号量之后
struct Node *node = head;
printf("consumer id : %ld, number = %d\n",pthread_self(),node->number);
head = head->next; //链表头结点后移
free(node); //释放取出来的头结点
pthread_mutex_unlock(&mutex); //解锁的顺序可以随意
sem_post(&sem_p); //生产者资源加1
sleep(rand()%3);
}
return NULL;
}
int main()
{
sem_init(&sem_p,0,5); //生产者信号量初始化,设置5个资源
sem_init(&sem_c,0,0); //消费者信号量初始化
pthread_mutex_init(&mutex,NULL);
pthread_t tid_p[5],tid_c[5];
for(int i=0;i<5;i++)
{
pthread_create(&tid_p[i],NULL,producer,NULL); //创建生产者线程
}
for(int i=0;i<5;i++)
{
pthread_create(&tid_c[i],NULL,consumer,NULL); //创建消费者线程
}
for(int i=0;i<5;i++)
{
pthread_join(tid_p[i],NULL); //线程回收
pthread_join(tid_c[i],NULL);
}
pthread_mutex_destroy(&mutex);
sem_destroy(&sem_c);
sem_destroy(&sem_p);
return 0;
}
上面程序编译后的执行结果如下图所示。
当把资源的数量设置为5以后,不再是生产者生产一个,消费者消费一个了,生产者有可能连续生产,消费者也有可能连续消费。
特别需要强调的是,互斥锁加锁一定要在信号量的sem_wait()之后,这样才能避免死锁。
如果生产者生产已满,且生产者抢到时间片先执行,这个时候如果互斥加锁在前,那么执行了该操作后,在下一步的sem_wait()已不能继续向下执行,因为生产已满!这个时候再转到消费者,由于生产者没有对互斥锁解锁,因此消费者也不能继续向下执行,产生死锁。同理,如果消费者已经全部消费完产品,且抢到时间片先执行,同样会发生死锁。因此,互斥加锁的操作必须在后。
本文参考视频:
多线程和线程同步-C/C++