多线程编程(1)
一.线程的基本概念
在我们前面的学习中,我们知道,进程是在各自独立的空间中运行的,如果要进行资源的交换,则需要进行进程间通(如管道,消息队列,信号量,共享内存等),实现起来较麻烦,而且每次只能有一个进程在运行,如果我们想要同时做多件事情(比如,我现在一边打字,一边听音乐)是不可能的,因此我们提出了线程的概念。什么是线程呢?线程就是进程的一个执行分支。线程在进程内部执行,资源共享(数据段和代码段等),因此很容易进行线程间通信。
在Linux内部是没有线程这个概念的,都是用进程模拟线程。我们将这些线程称为轻量级进程。除了共享代码段和数据段,以及一些全局变量等,各线程还共享一下进程资源和环境:
1.文件描述符表
2.各种信号的处理方式(SIG_IGN、SIG_DFL或者⾃自定义的信号处理函数)
3.当前工作目录
4.用户id和组id
但有些资源还是各线程独有的:
1.线程id
2.上下文,包括各种寄存器的值、程序计数器和栈指针
3.栈空间
4.errno变量
5.信号屏蔽字
6.调度优先级
二.线程控制
有关线程的函数大多都放在pthread.h的头文件中,注意编译的时候要加上-lpthread,这样就可以链接动态库了。
1.进程的创建
int pthread_create(pthread_t *thread,const pthread_attr_t *attr,void *(start_routine)(void *),void *arg);
参数说明:thread:表示新线程的id
attr:表示对线程做的一些设置,如果是NULL,表示按照默认格式。
start_routine:是一个参数是void*,返回值是void*的函数指针,这个函数表示创建的新的线程去这个函数中执行。
arg:是第三个参数的参数,如果没有,则传递NULL。
返回值:成功返回0,失败返回错误码(可以通过char* strerror(int errnum)获得)。
2.线程的终止
只终止某个进程的线程而不终止进程有三种方法
1.从线程函数return,这种方法对主线程不使用,从main函数return相当于调用exit。
2.一个线程可以调用pthread_canel终止同一进程的另一个线程。
3.线程可以调用ptread_exit终止自己。
用ptread_cancel终止一个线程分同步和异步两种情况。比较复杂,暂不研究,下面介绍pthread_exit和pthread_join的用法。
int pthread_exit(void *retval);
pthread_exit用来终止线程自身,参数是返回的错误码,要想要获得这个错误码,可以通过pthread_join来获得。
retval是一个二级指针,用途是用来获取线程的退出码。
int pthread_join(pthread_t thread,void **retval);
这个函数相当于进程中wait和waitpid函数,进行线程等待。thread就是线程号,通过pthread_t pthread_self(void)函数获得。
3.线程的可结合与可分离
在任何一个时间点上,线程是可结合的或者是分离的。一个可结合的线程能够被其他线程收回资源和杀死。在被其他线程回收之前,他的存储器资源(例如栈)是不释放的。相反,一个分离的线程是不能被其他线程回收或者杀死的,他的存储器资源在它终止时由系统自动释放。
默认情况下,线程被创建成可结合的。如果一个可结合的线程运行结束没有被join,则它的状态类似于进程中的僵尸进程。如果调用pthread_join后该现车个没有运行结束,调用者会被阻塞。如果主线程并不希望因为调用pthread_join而阻塞(因为还要继续处理之后到来的链接请求),这时可以在子进程中加入代码:
pthread_detach(pthread_self())
或者父线程调用pthread_detach(thread_id)(非阻塞,可立即返回)这将该子线程的状态设置为分离的(detached),如此一来,该线程运行结束后会自动释放所有的资源。
四、线程的同步与互斥
多个线程访问共享数据时会发生冲突,下面举这样的例子:当两个线程都要同时对全局变量+1时,代码如下:
#include<stdio.h>
#include<pthread.h>
int count=0;
void * pthread_run(void *arg)
{
int val=0;
int i=0;
}
int main()
{
pthread_t tid1;
pthread_t tid2;
pthread_create(&tid1,NULL,&pthread_run,NULL);
pthread_create(&tid2,NULL,&pthread_run,NULL);
pthread_join(tid1,NULL);
pthread_join(tid2,NULL);
printf("couint :%d\n",count);
return 0;
}
运行结果:
当一个线程可以修改变量,其他线程也可以进行修改或者读取时,这时候就需要对线程进行同步,以保证他们在访问变量的内容不会访问到无效的值。其实实质就是当存储器读和存储器写交叉执行,就会使得执行结果和预期结果不一致。两个线程都要把全局变量增加一,这个操作在平台上分为三步:
1.从内存读变量到寄存器
2.把寄存器的值加一
3.再把寄存器的值写
这个时候如果非互斥的,那么这个时候对同一份临界资源进行操作的时候就会出现冲突的时候,比如当你对临界资源操作的时候,可能会中途进行线程的切换,这个时候原本你所读取的状态会随着硬件上下文和pc指针这些东西会保存下来,切换了线程以后,新切换的线程可能会去读取前一次你所读取的临界资源,然后对这份临界资源进行修改,然后这个时候新线程可能会再次切换,切换到你所原来保存的线程中,然后,回复了以前保存的硬件上下文和pc指针这些内容以后,这个时候线程所读取的临界资源的状态等信息还是在没有修改之前的,所以这个时候就会有隐患,造成一些缺点。
对于多线程的程序,访问的冲突问题是很普遍的,结局方法是引入互斥锁(Mutex)。获得锁的线程可以完成“读-修改-写”三步操作组成一个原子操作,要么都执行,要么都不执行,不会执行到中间被打断,也不会在其他处理器上并行的做这个操作。
使用锁的时候我们需要对锁进行初始化:
可以去调用初始化函数或者定义锁利用宏进行初始化。
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t *restrict attr);
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
加锁的函数:
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
当我们建立了锁以后,我们就需要去销毁锁。
int pthread_mutex_destroy(pthread_mutex_t *mutex);
这三个函数第一个lock进行阻塞式申请锁,就是当你没申请到,那么就是一直阻塞等待。第二个函数trylock是若锁资源无法获得,这个时候不阻塞,进行询问。没调用一次,进行询问一次,最后一个unlock,这个函数就是用来解锁的,无论是lock还是trylock最后都要调用unlock来进行解锁。
另外需要注意,锁的所有操作都应该是原子的。要么执行要么不执行,并且中间不能够被打断。
所以这里需要来说下关于互斥锁的实现:
为了实现对mutex便利的读取、判断、修改都是原子操作,所以大多数的体系结构都提供swap或exchange指令,这个指令把寄存器和内存单元的数据相交换,因为只有一条指令,所以保证了原子性。
如果你要让线程进行切换,那么就可以有两种方式,一种是跑完时间片,这样一个线程就会切换。另外一种方式就是模式的切换,从内核态切换到用户态。这个期间会检查内部信息,简单的模式切换就是调用系统调用,系统调用本身是操作系统暴露给用户的一些接口,这些接口大部分内容都是相关于内核的,所以会发生从用户切换到操作系统 。
说完了互斥,接下来我们需要说一下同步的概念,关于同步的概念:强调进程间协同,按照顺序的去访问临界区,一般都是在互斥下进行同步。
下面对原来的代码进行修改:
#include<stdio.h>
#include<pthread.h>
//关于锁的初始化
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
int count=0;
void * pthread_run(void *arg)
{
int val=0;
int i=0;
while(i<5000)
{
//在临界区加上互斥锁,这样就可解决线程访问冲突的问题了
pthread_mutex_lock(&mutex);
i++;
val=count;
printf("pthread: %lu,count:%d\n",pthread_self(),count);
count =val+1;
pthread_mutex_unlock(&mutex);
}
return NULL;
}
int main()
{
pthread_t tid1;
pthread_t tid2;
pthread_create(&tid1,NULL,&pthread_run,NULL);
pthread_create(&tid2,NULL,&pthread_run,NULL);
pthread_join(tid1,NULL);
pthread_join(tid2,NULL);
printf("couint :%d\n",count);
return 0;
}
运行结果: