前言:线程是解决进程间通信的一个非常好的方法,它保留了进程技术实现多任务的特性,是内核调度的最小单元。多线程在多核心CPU上面更有优势。
我们知道进程技术发明的主要目的是用来实现多任务系统需求(多任务的需求是客观的)。
进程技术的主要功能是为了实现CPU的分时复用,从而实现宏观上的并行。但是要知道进程之间的调度(切换)是需要耗费一定成本的。比如A进程执行到某一步去切换B进程,首先需要保存A进程此时的断点,然后去执行B进程,等到回来再恢复断点继续执行……这其中断点得保存与恢复都是要付出代价的。并且进程间的通信由于进程隔离的缘故,会比较麻烦。
因此我们可以通过线程的方式来实现进程这些劣势的弥补,线程技术不仅保留了进程技术实现多任务的特性,还在线程间切换和线程间通信上提升了效率,并且在多核心CPU上面更有优势。
线程是参与内核调度的最小单元,一个进程中可以有多个线程(一个线程相当于一个分支任务),线程是依附于进程存在的(先得有了一个进程,才能有里面的线程),操作系统在调度时候调度的是进程里面的线程。
线程可以像进程一样可被OS调度,继承了进程能够可以被单独调用的特性。
同一进程的多个线程之间很容易高效率通信(同一个进程中的多个线程就好像同一个文件中的多个函数一样),进程中线程与线程的通信就相当于函数与主函数之间的通信,可以使用全局变量即可。
在多核心CPU(对称多处理器架构SMP)架构下效率最大化。可以保证多线程里面的多个线程运行在不同的CPU核心上,实现一种并行同时运行的状态。
线程的创建与回收:
(1)pthread_create :主线程用来创造子线程的
(2)pthread_join:主线程用来等待(阻塞)回收子线程
(3)pthread_detach: 主线程用来分离子线程,分离后主线程不必再去回收子线程(子线程自己管自己)
线程的取消:
(1)pthread_cancel: 一般都是主线程调用该函数去取消(让它赶紧死)子线程
(2)pthread_setcancelstate :子线程设置自己是否允许被取消(如果设置宏为不可被取消,那么cancel不管用)
(3)pthread_setcanceltype:取消的时候的模式(允许取消时候才会生效)
线程函数的退出:
(1)pthread_exit:标准的子线程退出方式(子线程不能调用exit返回,因为你属于进程的一部分,调用exit直接把进程整个都退出了)
(2)pthread_cleanup_push:解决破坏锁的问题(具体分析见下面)
(3)pthread_cleanup_pop:接应push函数,将内部保存的信息从栈中取出来(0表示拿出来不执行,1表示拿出来执行)
解释一下上述过程:
首先如果线程正常运行没有被取消,那么cnt在进行线程操作之前被加1(上锁),线程运行结束后cnt减1(解锁),其中cnt的变化为0->1->0,解锁后的cnt可以供其他线程使用。
如果线程在没有结束时就被取消了,那么cnt没有经历减1(解锁),则其他线程将无法进入(锁坏掉)。所以我们可以使用pthread_cleanup_push函数,在线程执行前先将解锁函数(function)提前保存在栈中,如果线程被取消,那么能够通过栈中的解锁函数来保证锁不会坏掉。
通过一个任务来实现线程间的同步。
任务:用户从终端输入任意字符然后统计个数显示,输入end则结束。
线程同步就是主线程和子线程之间的配合。想要的效果是平时子线程时阻塞住的状态,主线程获取用户输入的字符,如果不等于“end”,那么就去激活子线程,将输入的内容打印出来。
使用信号量的方式就是通过定义一个cnt,使其为0,在线程执行前给cnt++(上锁),线程结束后给cnt–(解锁)。这里使用封装好的库函数即可。
sem_init:初始化未命名的信号量。
sem_post:发送解锁信号给指向的信号量。
sem_wait:锁定指向的信号量。
sem_destroy:销毁定义的信号量。
#include
#include
#include
#include
#include
char buf[200] = {0};
//定义一个sem_t类型的变量
sem_t sem;
unsigned int flag = 0;
// 子线程程序,作用是统计buf中的字符个数并打印
void *func(void *arg)
{
// 子线程首先应该有个循环
// 循环中阻塞在等待主线程激活的时候,子线程被激活后就去获取buf中的字符长度,然后打印;完成后再次被阻塞
//阻塞住,等到发送信号才执行
sem_wait(&sem);
while (flag == 0)
{
printf("本次输入了%d个字符\n", strlen(buf));
memset(buf, 0, sizeof(buf));
sem_wait(&sem);
}
//退出线程
pthread_exit(NULL);
}
int main(void)
{
int ret = -1;
pthread_t th = -1;
//初始化一个信号量
sem_init(&sem, 0, 0);
//创建一个线程
ret = pthread_create(&th, NULL, func, NULL);
if (ret != 0)
{
printf("pthread_create error.\n");
exit(-1);
}
printf("输入一个字符串,以回车结束\n");
while (scanf("%s", buf)) //如果有输入内容进入循环
{
// 去比较用户输入的是不是end,如果是则退出,如果不是则继续
if (!strncmp(buf, "end", 3))
{
printf("程序结束\n");
flag = 1;
//发送信号
sem_post(&sem);
break;
}
// 主线程在收到用户收入的字符串,并且确认不是end后就去发信号激活子线程来计数。
// 子线程被阻塞,主线程可以激活,这就是线程的同步问题。
// 信号量就可以用来实现这个线程同步
sem_post(&sem);
}
// 回收子线程
printf("等待回收子线程\n");
ret = pthread_join(th, NULL);
if (ret != 0)
{
printf("pthread_join error.\n");
exit(-1);
}
printf("子线程回收成功\n");
//销毁信号量
sem_destroy(&sem);
return 0;
}
互斥锁又称互斥量。互斥锁和信号量的关系:可以认为互斥锁是一种特殊的信号量。信号量可以一直加(能够实现排队),而互斥锁只能是0或者1。
互斥锁一般用来实现关键段的保护,流程一般为上锁->执行->解锁。
pthread_mutex_init:使用attr指定的属性初始化由互斥锁引用的互斥锁。
pthread_mutex_lock:用来锁定互斥锁。
pthread_mutex_unlock:解除互斥锁。
pthread_mutex_destroy:销毁互斥锁。
#include
#include
#include
#include
char buf[200] = {0};
//定义一个互斥锁的变量
pthread_mutex_t mutex;
unsigned int flag = 0;
// 子线程程序,作用是统计buf中的字符个数并打印
void *func(void *arg)
{
// 子线程首先应该有个循环
// 循环中阻塞在等待主线程激活的时候,子线程被激活后就去获取buf中的字符
// 长度,然后打印;完成后再次被阻塞
sleep(1);
while (flag == 0)
{
pthread_mutex_lock(&mutex);
printf("本次输入了%d个字符\n", strlen(buf));
memset(buf, 0, sizeof(buf));
pthread_mutex_unlock(&mutex);
sleep(1);
}
pthread_exit(NULL);
}
int main(void)
{
int ret = -1;
pthread_t th = -1;
//创建一个互斥锁
pthread_mutex_init(&mutex, NULL);
//创建一个子线程
ret = pthread_create(&th, NULL, func, NULL);
if (ret != 0)
{
printf("pthread_create error.\n");
exit(-1);
}
printf("输入一个字符串,以回车结束\n");
while (1)
{
//互斥锁上锁
pthread_mutex_lock(&mutex);
scanf("%s", buf);
//互斥锁解锁
pthread_mutex_unlock(&mutex);
// 去比较用户输入的是不是end,如果是则退出,如果不是则继续
if (!strncmp(buf, "end", 3))
{
printf("程序结束\n");
flag = 1;
break;
}
//这里sleep的作用是确保主子线程不会抢锁
sleep(1);
}
// 回收子线程
printf("等待回收子线程\n");
ret = pthread_join(th, NULL);
if (ret != 0)
{
printf("pthread_join error.\n");
exit(-1);
}
printf("子线程回收成功\n");
pthread_mutex_destroy(&mutex);
return 0;
}
这里只是一个简单的互斥锁使用示例,正常情况下互斥锁用来保护一些关键的位置,这些位置有着明显的先后顺序,所以不至于用sleep来控制抢锁。
条件变量是线程同步的一种特有的方法,也是效率比较高的一种方法。当线程不满足条件时,就一直等待这个条件(阻塞),另一个提供条件的线程当满足发送条件时,就会给这个线程发送一个条件从而激活这个阻塞住的线程,被激活之后就会继续运行。
条件变量在使用时可以与互斥锁配合使用。
pthread_cond_init:初始化cond所引用的一个条件变量。
pthread_cond_destroy:销毁被引用的条件变量。
pthread_cond_wait:该函数应在条件变量上阻塞。 它们应使用被调用线程锁定的互斥锁或未定义的行为结果来调用。
pthread_cond_signal/pthread_cond_broadcast:这些函数应解除阻塞在条件变量上阻塞的线程。
#include
#include
#include
#include
char buf[200] = {0};
//定义互斥锁变量
pthread_mutex_t mutex;
//定义条件变量
pthread_cond_t cond;
unsigned int flag = 0;
// 子线程程序,作用是统计buf中的字符个数并打印
void *func(void *arg)
{
// 子线程首先应该有个循环
// 循环中阻塞在等待主线程激活的时候,子线程被激活后就去获取buf中的字符
// 长度,然后打印;完成后再次被阻塞
while (flag == 0)
{
//上锁
pthread_mutex_lock(&mutex);
//等待条件变量满足才能继续执行
pthread_cond_wait(&cond, &mutex);
printf("本次输入了%d个字符\n", strlen(buf));
memset(buf, 0, sizeof(buf));
//解锁
pthread_mutex_unlock(&mutex);
}
//退出子线程
pthread_exit(NULL);
}
int main(void)
{
int ret = -1;
pthread_t th = -1;
//初始化一个互斥锁
pthread_mutex_init(&mutex, NULL);
//初始化一个条件变量
pthread_cond_init(&cond, NULL);
//创建一个子线程
ret = pthread_create(&th, NULL, func, NULL);
if (ret != 0)
{
printf("pthread_create error.\n");
exit(-1);
}
printf("输入一个字符串,以回车结束\n");
while (1)
{
scanf("%s", buf);
//发送条件变量满足信号
pthread_cond_signal(&cond);
// 去比较用户输入的是不是end,如果是则退出,如果不是则继续
if (!strncmp(buf, "end", 3))
{
printf("程序结束\n");
flag = 1;
break;
}
// 主线程在收到用户收入的字符串,并且确认不是end后
// 就去发信号激活子线程来计数。
// 子线程被阻塞,主线程可以激活,这就是线程的同步问题。
// 信号量就可以用来实现这个线程同步
}
// 回收子线程
printf("等待回收子线程\n");
ret = pthread_join(th, NULL);
if (ret != 0)
{
printf("pthread_join error.\n");
exit(-1);
}
printf("子线程回收成功\n");
//销毁互斥锁
pthread_mutex_destroy(&mutex);
//销毁条件变量
pthread_cond_destroy(&cond);
return 0;
}