3.7.1.再论进程
3.7.1.1、多进程实现同时读取键盘和鼠标
3.7.1.2、使用进程技术的优势
(1)CPU时分复用,单核心CPU可以实现宏观上的并行、微观上的串行
(2)实现多任务系统需求(多任务的需求是客观的,多任务就是同时要做很多事情)
3.7.1.3、进程技术的劣势
(1)进程间切换开销大(进程断点的保护和进程断点的恢复)
(2)进程间通信麻烦而且效率低(进程与进程之间天生是隔离的或者说是独立的)
3.7.1.4、解决方案就是线程技术
(1)线程技术保留了进程技术实现多任务的特性。
(2)线程的改进就是在线程间切换和线程间通信上提升了效率。【这个是关键点】
(3)多线程在多核心CPU上面更有优势。
什么是多核心CPU:就是一块CPU上面有2个运算单元核心就叫做双核
一个CPU上只有1个CPU核心就叫做单核
有3个的叫做3核
4个的叫做4核
5个就是5核(== 没见过5核)
但是有6核.....
3.7.2.线程的引入
3.7.2.1、使用线程技术同时读取键盘和鼠标
3.7.2.2、linux中的线程简介
(1)一种轻量级进程(某种意义上来说进程和线程是相似的,一个进程中包含有多个线程)一个进程里可以有多个线程。线程只能通过或者说是依附于进程存在。
(2)线程是参与内核调度的最小单元
(3)一个进程中可以有多个线程
3.7.2.3、线程技术的优势
(1)像进程一样可被OS调度
(2)同一进程的多个线程之间很容易高效率通信(本质上一个进程里的线程就是一个一个函数,线程间通信本质就是一个程序(可以看作是一个进程)之间的函数间通信,而进程间通信可以理解成不同程序间的通信,效率比较低)
(3)在多核心CPU(对称多处理器架构SMP)架构下效率最大化
多核心CPU能保证多个【线程】在真正意义上的并行,而不能保证多个【进程】的同时运行。
-----------------------------------------------------------------------------------------------------------------------------------------------------------
多线程编程补充:
(1)线程的基本操作包括线程的创建、合并、终止线程以及设置线程的属性,Linux下的多线程编程接口遵循POSIX标准,称为pthread,注意线程使用的一个基本数据结构pthread_t(实际上是一个unsigned long int 数据)。
(2)线程的创建函数是:pthread_create(),类似于fork()函数,对这个函数的解析:----------------这个函数写在主线程中
头文件
#include<pthread.h>
函数声明
int pthread_create(pthread_t *restrict tidp,const pthread_attr_t *restrict_attr,void*(*start_rtn)(void*),void *restrict arg);
返回值
若成功则返回0,否则返回出错编号EAGAIN/EINVAL。
第一个参数:是一个指针,指向pthread_t 类型的数据,&id为前面定义的一个pthread_t 类型对象的地址,以获取新建线程的标识符;第二个参数用来设置线程的属性,也是一个指针,指向 pthread_attr_t类型的数据;第三个参数用来确定线程运行函数的起始地址和传递给这个函数的参数。
(3)合并线程:pthread_join()函数:【类似于多进程中的wait函数】----------------这个函数写在主线程中
pthread_join使一个线程等待另一个线程结束。子线程代码中如果没有pthread_join函数,主线程会很快结束从而使整个进程结束,从而使创建的线程没有机会开始执行就结束了。加入pthread_join后,主线程会一直阻塞等待直到等待的线程结束自己才结束,使创建的线程有机会执行。所有线程都有一个线程号,也就是Thread ID。其类型为pthread_t。通过调用pthread_self()函数可以获得自身的线程号。一个线程不能够被多个线程等待。
(4)终止线程:两种方式:----------------这个函数写在被调用子线程中
1)线程自身调用pthread_exit()函数
原型:void pthread_exit(void *retval), 使用函数pthread_exit退出线程,这是线程的主动行为; 线程通过调用pthread_exit函数终止执行,就如同进程在结束时调用exit函数一样。这个函数的作用是,终止调用它的线程。函数参数是函数返回时的代码。【注意一点的是这函数必须在pthread_join()函数第二个参数不为NULL的前提下才能够执行 】----------------这个pthread_exit()函数写在被调用子线程中
2)其他线程调用pthread_cancel()函数;这种方式类似于其它线程给我们的被调用线程发送了一个终止信号;这个函数的原型是:
int pthread_cancel(pthread_t thread),这个函数唯一的参数就是我们要终止的线程的标识符。----------------这个函数写在主线程中
其实子线程还可以设置自己的属性来决定是否被允许取消,如果允许以何种方式取消。pthread_setcancelstate()函数决定着线程是否允许接收取消请求,它的第一个参数可以设置为PTHREAD_CANCEL或者是PTHREAD_CANCEL_DISABLE,它的第二个参数设置为NULL;----------------这个函数写在被调用子线程中
如果子线程设置为允许被取消,则还需要设置是立即取消还是延迟取消,pthread_setcanceltype()函数的第一个参数可以是 PTHREAD_CANCEL_DEFERRED(延迟取消)或者是 PTHREAD_CANCEL_ASYNCHRONOUS(立即取消),函数的第二个参数为NULL 。----------------这个函数写在被调用子线程中 【注意的是pthread_setcanceltype()函数要和pthread_cancel()函数结合起来使用】
(5)线程属性以及对其属性的限制:线程属性的数据结构是pthread_attr_t,它本质是一个联合体union。操作线程属性需要特定的函数。
1)pthread_attr_init(pthread_attr_t *__attr)函数,它的作用是对一个线程属性对象进行初始化,----------------这个函数写在主线程中
2)在线程属性对象的初始化完成后我们可以调用其他相关函数来设置线程的各种属性:【注意2.1和2.2的前提条件都是pthread_attr_init初始化后】
2.1)设置线程的分离状态属性:
所谓线程的分离状态也就是说让一个线程不用被主线程等待回收,而是自己去回收自己,自己终止自己,并释放系统资源。
设置线程分离状态的函数为pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate)函数, 第二个参数可选为PTHREAD_CREATE_DETACHED(分离线程)和 PTHREAD _CREATE_JOINABLE(非分离线程)----------------这个函数写在主线程中
2.2)设置线程的优先级属性:线程的优先级用来指定线程的执行规则,使得重要的线程有机会被优先执行,它存放在结构体sched_param中;线程的优先级使用pthread_attr_getschedparam()函数和pthread_attr_setschedparam()设置。我们一般是(1)先读取优先级,(2)对取得的值进行修改(3)再存放回去。----------------这个函数写在主线程中
(6)线程同步:目前实现线程同步的主要方式就是:互斥量、条件变量、信号量
线程的最大特点是资源的共享性,但资源共享中的同步问题是多线程编程的难点。linux下提供了多种方式来处理线程同步,最常用的是互斥锁、条件变量和信号量。
1、互斥量:互斥量提供了对共享资源的保护访问,当一个线程使用互斥量锁定某个资源后,其他线程对该资源的访问都会被阻塞。 用于保护临界区(共享资源),以保证在任何时刻只有一个线程能够访问共享的资源。 互斥量类型声明为pthread_mutex_t数据类型,在<bits/pthreadtypes.h>中有具体的定义。
互斥量初始化和销毁:
/* Initialize a mutex. */
int pthread_mutex_init (pthread_mutex_t *__mutex,\
__const pthread_mutexattr_t *__mutexattr);
/* Destroy a mutex. */
int pthread_mutex_destroy (pthread_mutex_t *__mutex);
如果互斥量是静态分配的,可以通过常量进行初始化,如下:pthread_mutex_t mlock = PTHREAD_MUTEX_INITIALIZER;
当然也可以通过pthread_mutex_init()进行初始化。对于动态分配的互斥量由于不能直接赋值进行初始化就只能采用这种方式进行初始化,pthread_mutex_init()的第二个参数是互斥量的属性,如果采用默认的属性设置,可以传入NULL。
当不在需要使用互斥量时,需要调用pthread_mutex_destroy()销毁互斥量所占用的资源。
对互斥量的两个基本操作:加锁和解锁
/* Try locking a mutex. */
int pthread_mutex_trylock (pthread_mutex_t *__mutex);
/* Lock a mutex. */
int pthread_mutex_lock (pthread_mutex_t *__mutex);
/* Unlock a mutex. */
int pthread_mutex_unlock (pthread_mutex_t *__mutex);
这里要强调的是:互斥量是用于上锁的,不能用于等待。
简单说就是,互斥量的使用流程应该是:线程占用互斥量,然后访问共享资源,最后释放互斥量。而不应该是:线程占用互斥量,然后判断资源是否可用,如果不可用,释放互斥量,然后重复上述过程。这种行为称为轮转或轮询,是一种浪费CPU时间的行为。
2、条件变量:
3、信号量:
信号量用来控制对共享资源的访问,同步线程,初始化和注销函数分别是:sem_init()和sem_destroy()函数。sem_post()函数用来增加信号量的值,加1,sem_wait()用来阻塞当前线程直到信号量的值大于0,解除阻塞后将信号量的值减1,以表明资源经过使用后减少。
一、什么是信号量
线程的信号量与进程间通信中使用的信号量的概念是一样,它是一种特殊的变量,它可以被增加或减少,但对其的关键访问被保证是原子操作。如果一个程序中有多个线程试图改变一个信号量的值,系统将保证所有的操作都将依次进行。而只有0和1两种取值的信号量叫做二进制信号量,在这里将重点介绍。而信号量一般常用于保护一段代码,使其每次只被一个执行线程运行。我们可以使用二进制信号量来完成这个工作。
二、信号量的接口和使用
信号量的函数都以sem_开头,线程中使用的基本信号量函数有4个,它们都声明在头文件semaphore.h中。
代码示例:
#include <unistd.h>
#include <pthread.h>
#include <semaphore.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
//线程函数
void *thread_func(void *msg);
sem_t sem;//信号量
#define MSG_SIZE 512
int main()
{
int res = -1;
pthread_t thread;
void *thread_result = NULL;
char msg[MSG_SIZE];
//初始化信号量,其初值为0
res = sem_init(&sem, 0, 0);
if(res == -1)
{
perror("semaphore intitialization failed\n");
exit(EXIT_FAILURE);
}
//创建线程,并把msg作为线程函数的参数
res = pthread_create(&thread, NULL, thread_func, msg);
if(res != 0)
{
perror("pthread_create failed\n");
exit(EXIT_FAILURE);
}
//输入信息,以输入end结束,由于fgets会把回车(\n)也读入,所以判断时就变成了“end\n”
printf("Input some text. Enter 'end'to finish...\n");
while(strcmp("end\n", msg) != 0)
{
fgets(msg, MSG_SIZE, stdin);
//把信号量加1
sem_post(&sem);
}
printf("Waiting for thread to finish...\n");
//等待子线程结束
res = pthread_join(thread, &thread_result);
if(res != 0)
{
perror("pthread_join failed\n");
exit(EXIT_FAILURE);
}
printf("Thread joined\n");
//清理信号量
sem_destroy(&sem);
exit(EXIT_SUCCESS);
}
void* thread_func(void *msg)
{
//把信号量减1
sem_wait(&sem);
char *ptr = msg;
while(strcmp("end\n", msg) != 0)
{
int i = 0;
//把小写字母变成大写
for(; ptr[i] != '\0'; ++i)
{
if(ptr[i] >= 'a' && ptr[i] <= 'z')
{
ptr[i] -= 'a' - 'A';
}
}
printf("You input %d characters\n", i-1);
printf("To Uppercase: %s\n", ptr);
//把信号量减1
sem_wait(&sem);
}
//退出线程
pthread_exit(NULL);
}
-----------------------------------------------------------------------------------------------------------------------------------------------------------
3.7.3.线程常见函数(一个线程的核心就是pthread_create函数中调用的一个函数)
其实在Linux中,新建的线程并不是在原先的进程中,而是系统通过一个系统调用clone()。该系统copy了一个和原先进程完全一样的进程,并在这个进程中执行线程函数。不过这个copy过程和fork不一样。 copy后的进程和原先的进程共享了所有的变量,运行环境。这样,原先进程中的变量变动在copy后的进程中便能体现出来。
每一个线程都会有自己独立的栈。
3.7.3.1、线程创建与回收
(1)pthread_create 主线程用来创造子线程的
/*#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg);
*/
(2)pthread_join
/*int pthread_join(pthread_t thread, void **retval);//注意这里的retval是一个二重指针 */
主线程用来等待(意味着阻塞,有点类似于以前的wait函数)回收子线程利用的资源
一个线程不能被多个线程等待,也就是说对一个线程只能调用一次pthread_join,否则只有一个能正确返回,其他的将返回ESRCH 错误。
(3)pthread_detach 主线程用来分离子线程,分离后主线程不必再去回收子线程,而是让子线程自己去回收自己,自己去管理自己,主线程就不再管了
3.7.3.2、线程取消
(1)pthread_cancel
/*int pthread_cancel(pthread_t thread);*/
函数调用成功返回0。一般都是【主线程调用】该函数去取消(让它赶紧死)子线程(得有相应的权限)
(2)pthread_setcancelstate
/*int pthread_setcancelstate(int state, int *oldstate);*/ state:终结状态,可以为PTHREAD_CANCEL_DISABLE或者PTHREAD_CANCEL_ENABLE。【子线程调用】设置自己是否允许被取消
(3)pthread_setcanceltype
/*int pthread_setcanceltype(int type, int *oldtype);*/type:要设置的状态,可以为PTHREAD_CANCEL_DEFERRED或者为PTHREAD_CANCEL_ASYNCHRONOUS。
3.7.3.3、(子)线程函数退出相关(指的是子线程自己退出)
(1)pthread_exit与return退出(最正规的做法)
一个线程的结束有两种途径,一种是函数结束了,调用它的线程也就结束了;另一种方式是通过函数pthread_exit来实现。
(2)pthread_cleanup_push(压栈)
(3)pthread_cleanup_pop(弹栈)
注意在线程里不能用exit函数来退出,因为在线程里一旦用这种方式退出则整个程序就退出了。
线程锁:(上厕所的例子)
int lock=0;
if(lock==0) //判断是否处于解锁状态
{
(1)lock++; //表示置锁
(2)pthread_cleanup_push //(压栈)函数,这个函数就是负责将来解锁,怕死在里面出不来了(更保险更安全一点)
pthread_cleanup_push(function,arg);
pthread_cleanup_push(function1,arg);
(3)//需要执行的东西
//子线程有可能在这个步骤中被主线程取消cancle(子线程死在厕所里,别人也进不去了)
//解决方案就是pthread_cleanup_push(压栈)函数
(4)pthread_cleanup_pop(0);
pthread_cleanup_pop(0); //参数是0表示把前面pthread_cleanup_push函数压栈的内容弹出并不执行,如果是1表示把前面pthread_cleanup_push函数压栈的内容弹出并执行。总结就是pthread_cleanup_pop函数和pthread_cleanup_push函数是一种避免死锁的一种安全机制。
(5)lock--; //表示解锁
}
//这个function函数就是负责解锁的
void function(void * arg)
{
lock--; //表示解锁
}
posix thread(posix标准)
3.7.3.4、获取线程id函数
(1)pthread_self
3.7.4_5.线程同步之信号量:
线程同步:
3.7.4.1、任务:用户从终端输入任意字符然后统计个数显示,输入end则结束
代码示例:
#include <stdio.h>
#include <pthread.h>
#include <errno.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
/*用户从终端输入任意字符然后统计个数显示,输入end则结束*/
int main(void)
{
char buf[100]={0};
char a[10]={"end"};
printf("请输入一个字符串:“end”结束.\n");
while(1)
{
memset(&buf,0,sizeof(buf));
read(0,&buf,sizeof(buf));
printf("您输入的字符串是:[%s]\n",buf);
if(!strncmp(buf,a,3))
{
break;
}
else{
printf("您输入的字符串个数是:%d\n",(strlen(buf)-1));
}
}
printf("您输入的字符串是:‘end’,程序退出\n");
return 0;
}
3.7.4.2、使用多线程实现:主线程获取用户输入并判断是否退出,子线程计数
(1)为什么需要多线程实现
(2)问题和困难点是?不能让子线程循环执行。
(3)理解什么是线程同步:【子线程阻塞住自己,主线程去激活子线程。让两个线程去配合工作】
3.7.4.3、信号量的介绍和使用
线程的信号量与进程间通信中使用的信号量的概念是一样,它是一种特殊的变量,它可以被增加或减少,但对其的关键访问被保证是原子操作。如果一个程序中有多个线程试图改变一个信号量的值,系统将保证所有的操作都将依次进行。而只有0和1两种取值的信号量叫做二进制信号量,在这里将重点介绍。而信号量一般常用于保护一段代码,使其每次只被一个执行线程运行。我们可以使用二进制信号量来完成这个工作.
3.7.6.线程同步之互斥锁
3.7.6.1、什么是互斥锁
(1)互斥锁又叫互斥量(mutex)
(2)相关函数:
pthread_mutex_init
pthread_mutex_destroy
pthread_mutex_lock
pthread_mutex_unlock
(3)互斥锁和信号量的关系:可以认为互斥锁是一种特殊的信号量(互斥锁是值只能是0和1的信号量)
(4)互斥锁主要用来实现关键段代码保护
3.7.6.2、用互斥锁来实现上节的代码
(1)上锁 pthread_mutex_lock()
(2)访问
(3)解锁pthread_mutex_unlock()
注意:man 3 pthread_mutex_init时提示找不到函数,说明你没有安装pthread相关的man手册。安装方法:1、虚拟机上网;2、sudoapt-get install manpages-posix-dev
3.7.7.线程同步之条件变量
3.7.7.1、什么是条件变量
3.7.7.2、相关函数
pthread_cond_init
pthread_cond_destroy
pthread_cond_wait
pthread_cond_signal(只能唤醒一个)/pthread_cond_broadcast(广播激活多个线程)
3.7.7.3、使用条件变量来实现上节代码
3.7.7.4、线程同步总结
我有个朋友是爵士音乐家,他有次跟我说:如果你不是乐队里最差的演奏者,马上换支乐队。我现在在Spotify工作,在这里我每天都觉得自己是个【狗屁程序员】,也因此我离开了上一家公司,从Spotify重头开始。当我在这里再也感觉不到自己是个【狗屁程序员】的时候,我会选择离开,然后找一个能让我再次感到自己是坨屎的地方。这种策略在我的职业生涯里确实 帮了我大忙。
------译自 Quora