9.1进程回顾
9.1.1多进程实现同时读取键盘和鼠标
注意:进程相关函数:fork()。
代码如下:
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(void)
{
// 创建子进程,然后父子进程中分别进行读键盘和鼠标的工作
int ret = -1;
int fd = -1;
char buf[200];
ret = fork();
if (ret == 0)
{
// 子进程
fd = open("/dev/input/mouse1", O_RDONLY);
if (fd < 0)
{
perror("open:");
return -1;
}
while (1)
{
memset(buf, 0, sizeof(buf));
printf("before read.\n");
read(fd, buf, 50);
printf("读出鼠标的内容是:[%s].\n", buf);
}
}
else if (ret > 0)
{
// 父进程
while (1)
{
memset(buf, 0, sizeof(buf));
printf("before read.\n");
read(0, buf, 5);
printf("读出键盘的内容是:[%s].\n", buf);
}
}
else
{
perror("fork:");
}
return 0;
}
运行结果:
当鼠标移动出现如下画面:
键盘按下:
9.1.2使用进程技术的优势
(1)CPU时分复用,单核心CPU可以实现宏观上的并行
(2)实现多任务系统需求(多任务的需求是客观的)
(3)程序的并发执行和资源共享。多道程序设计出现后,实现了程序的并发执行和资源共享,提高了系统的效率和系统的资源利用率。
(4)顺序程序的特点:具有封闭性和可再现性.
(5)每个进程互相独立,不影响主程序的稳定性,子进程崩溃没关系; 通过增加CPU,就可以容易扩充性能; 可以尽量减少线程加锁/解锁的影响,极大提高性能,就算是线程运行的模块算法效率低也没关系;
9.1.3进程技术的劣势
(1)进程间切换开销大
(2)进程间通信麻烦而且效率低
(3)逻辑控制复杂,需要和主程序交互; 需要跨进程边界,如果有大数据量传送,就不太好,适合小数据量传送、密集运算 多进程调度开销比较大;
9.1.4线程技术对比
特点:
(1)线程技术保留了进程技术实现多任务的特性。
(2)线程的改进就是在线程间切换和线程间通信上提升了效率。
(3)多线程在多核心CPU上面更有优势。
(4))改善程序结构。一个既长又复杂的进程可以考虑分为多个线程,成为几个独立或半独立的运行部分,这样的程序会利于理解和修改。
9.2线程技术的引入
9.2.1 什么是线程?
(1)相对进程而言,线程是一个更加接近于执行体的概念,它可以与同进程中的其他线程共享数据,但拥有自己的栈空间,拥有独立的执行序列。在串行程序基础上引入线程和进程是为了提高程序的并发度,从而提高程序运行效率和响应时间。
(2)是一种轻量级进程。
(3)线程是参与内核调度的最小单元。
(4)一个进程中可以有多个线程。
9.2.1线程的创建
POSIX通过pthread_create()函数创建线程,API定义如下:
int pthread_create(pthread_t * thread, pthread_attr_t * attr, void * (*start_routine)(void *), void * arg)
9.2.2使用线程技术同时读取键盘和鼠标
代码如下:
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <pthread.h>
char buf[200];
void *func(void *arg)
{
while (1)
{
memset(buf, 0, sizeof(buf));
printf("before read.\n");
read(0, buf, 5);
printf("读出键盘的内容是:[%s].\n", buf);
}
}
int main(void)
{
// 思路就是创建子进程,然后父子进程中分别进行读键盘和鼠标的工作
int ret = -1;
int fd = -1;
pthread_t th = -1;
ret = pthread_create(&th, NULL, func, NULL);
if (ret != 0)
{
printf("pthread_create error.\n");
return -1;
}
// 因为主线程是while(1)死循环,所以可以在这里pthread_detach分离子线程
// 主任务
fd = open("/dev/input/mouse1", O_RDONLY);
if (fd < 0)
{
perror("open:");
return -1;
}
while (1)
{
memset(buf, 0, sizeof(buf));
printf("before read.\n");
read(fd, buf, 50);
printf("读出鼠标的内容是:[%s].\n", buf);
}
return 0;
}
注意:编译时候不要忘记-lpthread。
运行结果:
鼠标与键盘结果和进程结果相同。
9.2.3线程简单图形理解(以上面程序为参考)
注意:线程是依赖于进程。以上面代码为列:注意:此流程图由于线程回收等新内容(上面程序并没有写到),流程图并不完整,后面会涉及到。
注意:线程是依赖于进程。以上面代码为列:
所以:没有进程也就没有线程,*func线程结束并不影响,进程结束所有线程结束。
9.3线程基本函数
9.3.1线程创建与回收
(1) pthread_create 主线程用来创造子线程的
(2) pthread_join 主线程用来等待(阻塞)回收子线程
(3) pthread_detach 主线程用来分离子线程,分离后主线程不必再去回收子线程
9.3.2线程取消
(1) pthread_cancel 一般都是主线程调用该函数去取消(让它赶紧死)子线程
(2) pthread_setcancelstate 子线程设置自己是否允许被取消
(3) pthread_setcanceltype
注意:
(1)int pthread_cancel(pthread_t thread) 。thread:线程号,创建时定义的线程号。(如果子线程允许取消)。
(2)int pthread_setcancelstate(int state, int *oldstate);子线程默认是可以取消。
1.PTHREAD_CANCEL_ENABLE可以取消。
2.PTHREAD_CANCEL_DISABLE不允许取消。
(3)int pthread_setcanceltype(int type, int *oldtype);取消类型
1.PTHREAD_CANCEL_DEFERRED立即取消。
2.PTHREAD_CANCEL_ASYNCHRONOUS不立即等待合适时刻取消
9.3.3线程函数退出相关
(1) pthread_exit与return 线程退出 pthread_exit标准子线程退出
(2) pthread_cleanup_push
(3) pthread_cleanup_pop
void pthread_exit(void *retval);*retval:退出时返回的值。
void pthread_cleanup_push(void (*routine)(void *),void *arg);
void pthread_cleanup_pop(int execute);
线程终止有两种情况:正常终止和非正常终止。非正常终止情况下如果遇见在子线程取消情况下有互斥锁(后面讲到),会出现死锁,所以会用pthread_cleanup_push,pthread_cleanup_pop清理函数。
注意:在下面三种情况下,pthread_cleanup_push()压栈的“清理函数”会被调用:
1, 线程调用pthread_exit()函数,而不是直接return.
2, 响应取消请求时,也就是有其它的线程对该线程调用pthread_cancel()函数。
3, 本线程调用pthread_cleanup_pop()函数,并且其参数非0
4当pthread_cleanup_pop()函数的参数为0时,仅仅在线程调用pthread_exit函数或者其它线程对本线程调用pthread_cancel函数时,才在弹出“清理函数”的同时执行该“清理函数”。
5注意pthread_exit终止线程与线程直接return终止线程的区别,调用return函数是不会在弹出“清理函数”的同时执行该“清理函数的。
6.pthread_cleanup_push()函数与pthread_cleanup_pop()函数必须成对的出现在同一个函数中。
参考部分程序:
/*清理的函数体*/
static void cleanup_handler(void *arg)
{
printf("Called clean-up handler\n");
cnt = 0;
}
/*子进程*/
static void *thread_start(void *arg)
{
time_t start, curr;
printf("New thread started\n");
pthread_cleanup_push(cleanup_handler, NULL);
curr = start = time(NULL);
while(!done){
pthread_testcancel();
if(curr < time(NULL)){
curr = time(NULL);
printf("cnt= %d\n", cnt); cnt++;
}
}
pthread_cleanup_pop(1);
return NULL;
}
9.3.4获取线程id
pthread_t pthread_self(void);获取子线程的ID号
9.4 线程同步之信号量
9.4.1基本函数
int sem_init (sem_t *sem, int pshared, unsigned int value);
sem指定的信号量进行初始化,设置好它的共享选项,并指定一个整数类型的初始值。
int sem_wait(sem_t * sem);
函数sem_wait( sem_t *sem )被用来阻塞当前线程直到信号量sem的值大于0,解除阻塞后将sem的值减一,表明公共资源经使用后减少。函数sem_trywait ( sem_t *sem )是函数sem_wait()的非阻塞版本,它直接将信号量sem的值减一。
int sem_post(sem_t * sem);
函数sem_post( sem_t *sem )用来增加信号量的值。当有线程阻塞在这个信号量上时,调用这个函数会使其中的一个线程不在阻塞,选择机制同样是由线程的调度策略决定的。
int sem_destroy(sem_t *sem);
函数的作用是在我们用完信号量对它进行清理。
9.4.2信号量程序实现
任务:使用多线程实现:用户从终端输入任意字符然后统计个数显示,输入end则结束。主线程获取用户输入并判断是否退出,子线程计数。
参考程序:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <pthread.h>
#include <semaphore.h>
char buf[200] = {0};
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);
//exit(0);
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;
}
程序运行结果:
9.5 线程同步之互斥锁
9.5.1 什么是互斥锁?
是一种特殊的信号量,常用来防止两个进程或线程在同一时刻访问相同的共享资源。互斥量来保证对变量(关键的代码段)的排他性访问。
9.5.2基本函数
(1)初始化互斥锁
int pthread_mutex_init(pthread_mutex_t *mp, const pthread_mutexattr_t *mattr)
参数说明:mp 互斥锁地址 mattr 属性 通常默认 null
注意:初始化互斥锁之前,必须将其所在的内存清零。
(2)互斥锁锁定
int pthread_mutex_lock(pthread_mutex_t *mutex); #include pthread_mutex_t mutex; int ret; ret = pthread_ mutex_lock(&mp); /* acquire the mutex */
参数说明:
当 pthread_mutex_lock() 返回时,说明该互斥锁已被锁定,如果该互斥锁已被另一个线程锁定和拥有,就会调用线程将被阻塞,直到该互斥锁变为可用为止。
如果互斥锁类型为 PTHREAD_MUTEX_NORMAL,则不提供死锁检测。尝试重新锁定互斥锁会导致死锁。如果某个线程尝试解除锁定的互斥锁不是由该线程锁定或未锁定,则将产生不确定的行为。
如果互斥锁类型为 PTHREAD_MUTEX_ERRORCHECK,则会提供错误检查。如果某个线程尝试重新锁定的互斥锁已经由该线程锁定,则将返回错误。如果某个线程尝试解除锁定的互斥锁不是由该线程锁定或者未锁定,则将返回错误。
如果互斥锁类型为 PTHREAD_MUTEX_RECURSIVE,则该互斥锁会保留锁定计数这一概念。线程首次成功获取互斥锁时,锁定计数会设置为 1。线程每重新锁定该互斥锁一次,锁定计数就增加 1。线程每解除锁定该互斥锁一次,锁定计数就减小 1。 锁定计数达到 0 时,该互斥锁即可供其他线程获取。如果某个线程尝试解除锁定的互斥锁不是由该线程锁定或者未锁定,则将返回错误。
如果互斥锁类型是 PTHREAD_MUTEX_DEFAULT,则尝试以递归方式锁定该互斥锁将产生不确定的行为。对于不是由调用线程锁定的互斥锁,如果尝试解除对它的锁定,则会产生不确定的行为。如果尝试解除锁定尚未锁定的互斥锁,则会产生不确定的行为。
函数的返回值:
成功返回0,返回EAGAIN时超出了互斥锁递归锁定的最大次数无法上锁,返回EDEADLK时已经拥有互斥锁。
(3)互斥锁解除锁定
int pthread_mutex_unlock(pthread_mutex_t *mutex); #include pthread_mutex_t mutex; int ret; ret = pthread_mutex_unlock(&mutex); /* release the mutex */
说明:释放 mutex 引用的互斥锁对象。成功返回0,其他返回都为错误,
(4)互斥锁锁定的非阻塞方式
int pthread_mutex_trylock(pthread_mutex_t *mutex); #include pthread_mutex_t mutex; int ret; ret = pthread_mutex_trylock(&mutex); /* try to lock the mutex */
函数说明:pthread_mutex_trylock() 是 pthread_mutex_lock() 的非阻塞版本。
成功返回0,EBUSY :已经锁定,EAGAIN次数超限。
(5)互斥锁的销毁
int pthread_mutex_destroy(pthread_mutex_t *mp); #include pthread_mutex_t mp; int ret; ret = pthread_mutex_destroy(&mp); /* mutex is destroyed */
成功返回0,其它都为错误。
9.5.3代码实现:
实现内容:与信号量相同使用多线程实现:用户从终端输入任意字符然后统计个数显示,输入end则结束。主线程获取用户输入并判断是否退出,子线程计数。
参考代码:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <pthread.h>
char buf[200] = {0};
pthread_mutex_t mutex;
unsigned int flag = 0;
// 子线程程序,作用是统计buf中的字符个数并打印
void *func(void *arg)
{
// 子线程首先应该有个循环
// 循环中阻塞在等待主线程激活的时候,子线程被激活后就去获取buf中的字符
// 长度,然后打印;完成后再次被阻塞
//while (strncmp(buf, "end", 3) != 0)
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;
//exit(0);
break;
}
sleep(1);
// 主线程在收到用户收入的字符串,并且确认不是end后
// 就去发信号激活子线程来计数。
// 子线程被阻塞,主线程可以激活,这就是线程的同步问题。
// 信号量就可以用来实现这个线程同步
}
// 回收子线程
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;
}
运行结果:
9.5.4本章注意问题:
当一个互斥量已经被别的线程锁定后,另一个线程调用pthread_mutex_lock()函数去锁定它时,会挂起自己的线程等待这个互斥量被解锁。可能出现以下两种情况:
“饥饿状态”:这个互斥量一直没有被解锁,等待锁定它的线程将一直被挂着,即它请求某个资源,但永远得不到它。用户必须在程序中努力避免这种“饥饿”状态出现。Pthread函数库不会自动处理这种情况。
“死锁”:一组线程中的所有线程都在等待被同组中另外一些线程占用的资源,这时,所有线程都因等待互斥量而被挂起,它们中的任何一个都不可能恢复运行,程序无法继续运行下去。这时就产生了死锁。Pthread函数库可以跟踪这种情形,最后一个线程试图调用pthread_mutex_lock()时会失败,并返回类型为EDEADLK的错误。
9.6 线程同步之条件变量
9.6.1什么是条件变量?
允许线程阻塞和等待另一个线程发送信号的方法弥补了互斥锁的不足,它常和互斥锁一起使用。
9.6.2基本函数
(1)条件变量初始化
int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);
说明:cond 条件变量,attr 条件变量属性。成功返回0,出错返回错误编号。当attr为空,它将使用缺省的属性来设置所指定的条件变量。
(2)条件变量摧毁
int pthread_cond_destroy(pthread_cond_t *cond);
说明:cond 条件变量,成功返回0,出错返回错误编号。
(3)条件变量等待
int pthread_cond_wait(pthread_cond_t *cond,pthread_mutex_t *mutex);
int pthread_cond_timedwait(pthread_cond_t *cond,pthread_mutex_t mytex,const struct timespec *abstime);
说明:cond 条件变量mutex 互斥锁。成功返回0,出错返回错误编号。*cond是指向一个条件变量的指针。*mutex则是对相关的互斥锁的指针。
pthread_cond_timedwait函数类型与函数pthread_cond_wait区别:
如果达到或是超过所引用的参数*abstime,它将结束并返回错误ETIME.pthread_cond_timedwait函数的参数*abstime指向一个timespec结构,阻塞等待时间。
该结构如下:
typedef struct timespec{
time_t tv_sec;
long tv_nsex;
}timespec_t;
(4)条件变量发送
int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);
说明:cond 条件变量,成功返回0,出错返回错误编号。调用pthread_cond_signal时一个在相同条件变量上阻塞的线程将被解锁,pthread_cond_broadcast通知阻塞在这个条件变量上的所有线程。
9.6.3代码实现
实现内容:与信号量相同使用多线程实现:用户从终端输入任意字符然后统计个数显示,输入end则结束。主线程获取用户输入并判断是否退出,子线程计数。
参考代码:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <pthread.h>
char buf[200] = {0};
pthread_mutex_t mutex;
pthread_cond_t cond;
unsigned int flag = 0;
// 子线程程序,作用是统计buf中的字符个数并打印
void *func(void *arg)
{
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;
}
。
}
// 回收子线程
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;
}
程序说明:当键盘输入字符后调用pthread_cond_signal(&cond);条件变量发送,子线程pthread_cond_wait(&cond, &mutex);由之前阻塞后执行后面代码。
程序运行结果:
9.7线程同步总结
将一个大流程中实现的功能放到了多个小流程中,程序更加的简洁和易于阅读。提高了程序的执行效率。多线程”使得程序的模块化更强,有利于追踪程序执行过程和排查问题。线程同步是为了防止多个线程访问一个数据对象时,对数据造成破坏。线程的同步是保证多线程安全访问资源的一种手段。