文章目录
前言
针对C语言线程相关实现,线程锁,线程连接,资源共享,参数传递。全面解析线程相关编程。
一、进程、线程、协程
1.进程
指在系统中正在运行的一个应用程序;程序一旦运行就是进程;进程是指程序执行时的一个实例,即它是程序已经执行到课中程度的数据结构的汇集。 是并发执行的程序在执行过程中分配和管理资源的基本单位,是一个动态的概念。
进程的状态:创建态、就绪态、运行态、阻塞态、结束态。
从内核的观点来看,进程的目的就是担当竞争、分配系统资源(CPU时间、内存等)的基本单位。
2.线程
是进程中执行运算的最小单位,是进程之内独立执行的一个单元执行流。进程内可调度的实体。
系统分配处理器时间资源的基本单元,是被系统独立调度和分派的基本单位,
线程自己不拥有资源,只拥有一点在运行中必不可少的资源,但它可以与同属一个进程中的其他线程共享进程所拥有的全部资源。一个线程可以创建和撤销另一个线程,同一进程中多个线程可以并发执行。
进程——资源分配的最小单位
线程——程序执行的最小单位
3.协程
是一种比线程更加轻量级的存在。一个线程也可以拥有多个协程。其执行过程更类似于子例程,或者说不带返回值的函数调用。
二、区别与关系
1.进程与线程基本区别
1.进程是资源分配最小单位,线程是程序执行最小单位(资源调度最小单位)
2.进程拥有自己的独立地址空间,每启动一个进程,系统就会给他分配地址空间,建立数据表来维护代码段、堆栈段、数据段。
而线程共享进程中的数据,使用相同的地址空间,CPU切换花费与创建线程开销都比较小。
2plus:因为进程拥有独立的堆栈空间和数据段,所以当启动新进程必须给它独立的地址空间,建立众多数据表维护它的代码段、堆栈段、和数据段。线程拥有独立的堆栈空间,但是共享数据段,它们彼此之间使用相同的地址空间,共享大部分数据。
3.线程通信方便,同一进程下的线程共享全局变量、静态变量等,进程间通信需要以通信的方式(IPC)进行。
3plus:进程之间互不干扰相互独立,通信机制复杂,管道,信号,消息队列,共享内存,套接字等机制。而线程有于共享数据段,通信很方便。
4.多进程程序健壮性比多线程程序更加健壮,多线程只要有一个线程死掉,整个进程死掉,而一个进程不会对另一个进程产生影响。
2.线程与协程
协程避免了无意义的调度,提高性能,但必须人为承担调度责任,同时协程失去标准线程使用多CPU能力。
线程:
相对独立
有自己上下文
切换受系统控制
协程:
相对独立
有自己上下文
切换由自己控制,当前协程切换到其他协程,由当前协程控制
3.进程与线程的关系(进程是爹妈,管着众多的线程儿子)
1.一个线程只属于一个进程,一个进程有多个线程,但至少有一个线程,线程是操作系统可识别的最小执行和调度单位。
2.资源分配给进程,同一进程内所有线程共享该进程的所有资源。同一进程中的多个线程共享代码段(代码和常量),数据段(全局变量和静态变量),扩展段(堆存储)。但是每个线程拥有自己的栈段,栈段又叫运行时段,用来存放所有的局部变量和临时变量。
3.处理机分配给进程,实际上真正在处理及上运行的是线程。
4.线程在执行过程中需要协作同步,不同进程的线程间要利用消息通信的方法实现同步。
三、线程共享资源
1.线程的私有资源
线程的本质就是函数的执行,函数执行总会有一个源头,即所谓的入口函数,CPU从入口函数开始执行从而形成一个执行流,人为给执行流起个名字就是线程。
函数运行时的信息保存在栈帧中,栈帧中保存了函数的返回值、调用其他函数的参数、该函数使用的局部变量、该函数使用的寄存器信息,假设A函数调用B函数:
线程运行的本质是函数运行,函数运行时信息保存在栈帧中,每个线程都有自己独立的、私有的栈区。
此外,函数运行时需要额外的寄存器来保存信息,部分局部变量,这些也是线程私有的。一个线程不可能访问到另一个线程的这类寄存器信息。
线程拥有的栈区、程序计数器PC、栈指针、运行时使用的寄存器,为私有的,组成进程的上下文(thread context)。
2.线程间共享资源
1.进程的地址空间
线程共享进程地址空间中除线程上下文信息中的所有内容,即线程可以直接读取这些内容。
2.代码区
保存编译后的可执行机器指令,这些指令从可执行文件中加载到内存,可执行程序中的代码区就是用来初始化进程地址空间中的代码区的。
线程之间共享代码区,即程序中的任何一个函数都可以放到线程中去执行,不存在某个函数只能被特定线程执行的情况。
3.堆区
堆区,在C/C++中malloc或者new出来的数据存放在堆区。只要知道变量的地址,即指针,任何线程都可以访问指针指向的数据,所以堆区也是线程共享的属于进程的资源。
4.栈区
理论上来说,栈区是线程私有的,但实际上,栈区属于线程这一规则并没有被严格遵守。
线程的栈区不像进程地址空间的严格隔离,线程的栈区并没有严格的隔离机制来保护,如果一个线程拿到另一个线程栈帧上的指针,该线程就可以改变另一个线程的栈区,即这些线程可以修改本属于另一个线程栈区中的变量。
5.文件
如果程序在运行过程中打开了一些文件,则进程地址空间中还保存有打开的文件信息,这些信息也可以被线程间共享。
6.线程局部存储 (TLS)
线程局部存储,是指存放在该区域中的变量有两个含义:
1.存放在该区域对为全局变量,其他线程都可以访问
2.虽然看上去所有线程访问的都是同一个变量,但该全局变量独属于一个线程,一个线程对该变量的修改对其他线程不可见。
int a = 1;
__thread b = 1;
void print_ab(){
cout<<a<<b<<endl;
}
void run(){
++a;
++b;
print_ab();
}
void main(){
thread t1(run);
t1.join();
thread t2(run);
t2.join();
}
a的值会打印出2,3,b的值会打印出2,2
线程局部存储使得thread1对b操作,不会影响到thread2对b进行操作。
3.线程共享的环境
线程共享的环境有:进程代码段、进程公有数据、进程打开的文件描述符、信号的处理器、进程的当前目录、进程用户ID、进程组ID。
同时进程拥有共性的同时,还拥有个性:线程ID、寄存器组的值、线程的堆栈、错误返回码、线程的信号屏蔽码、线程的优先级。
四、进程通信
共有七种进程间通信:signal、file、pipe、shm、sem、meg、socket
1.signal
信号通信目的:某某事件发生!此时需要处理什么,进程间(可以是不相关进程)传递信号
场景:信号又称为中断,需要处理的是中断函数。设置断点、形参入栈、保存现场信息,执行中断函数、处理完毕后,恢复现场信息,程序继续向下执行。
Linux可用kill -l查看所有信号,共64种。
发送信号:kill(pid, 信号) //对指定进程发送什么信号
raise(信号) == kill(getpid(),信号) //给自己发送信号
alarm(秒数) //定时产生一个SIGALRM信号,调用alarm方法后,只会产生一次该信号
接收信号:signal(信号, 函数指针) //对该信号接收,并调用自己的函数指针进行处理。
信号通信局限性:不能传递复杂的、有效的、具体的数据。
2.file
每打开一个文件,就会产生一个文件控制块,而文件控制块与文件描述符是一一对应的,通过对文件描述符的操作而进行文件的操作。
文件描述符的分配原则:编号的连续性(节省编号资源)
通过文件系统对文件描述符的读写控制,进程间一方对文件读,一方对文件写,达到文件之间的通信;可以是不相关进程之间的通信
使用API:write()和read()
为实现有序传输,需要使用信号
(1)通过pause()等待对方发起一个信号,已确认可以开始执行下一次读写操作
pause():只要接收到任何信号,立马就可以向下执行
(2)通过kill(, SIGUSR1)方法向对方发出明确的信号,可以开始下一步执行读写
缺点:1.文件通信没有访问规则,2.(CPU>内存>文件)是低速的。
3.pipe
在通信的进程间构建一个单向的数据流动的通道,数据通过管道从一个进程流向另一个进程是具有时间先后顺序的所以是半双工通信。管道是一种临时文件,不是磁盘上真真正正的文件,是一块存储区域
分为:
fd[0]:读出数据
fd[1]:写入数据
无名管道:只能用于亲缘关系的父子进程,fd = pipe(),得到管道文件描述符,通过fd,用的是write()和read()读写数据.
达到双方通信:需要用2个管道,达到可以发多句话,fork()子进程处理
有名管道:非父子进程间通信mkfifo()
mkfifo会在文件系统中创建一个管道文件,然后使其映射内存的一个特殊区域,凡是能够打开mkfifo创建的管道文件进程(通过这个文件描述符),都可以使用该文件实现FIFO的数据流动
mkfifo(文件名, O_CREAT | O_EXCL | 0755);创建了2个管道文件,在客户端创建一个读的管道文件,在服务器创建一个写的管道文件,然后当做文件操作即可
socketpair可以创建双向管道,fd[0]、fd[1]都是同时具有读和写的属性;
优点:(1)、有强制的访问规则FIFO,(2)、用内存模仿文件,也就是用文件的方式操作内存
管道通信的特点:
(1)、如果管道为空,从管道读取数据的一方会阻塞。直到管道中有新的数据为止
(2)、管道的数据通信具有FIFO特性,这样可以避免数据的混乱(first in first out)
(3)、管道数据的读取与发送并没有次数限制,而是管道是否为空时最重要的指标
(4)、这种管道的使用具有一个最大的局限性:只适用于父子进程之间。从程序的设计中可以看到,管道的创建是父进程完成的,而且是在创建子进程之前,从而才使得子进程拥有了管道文件描述符,才能够使得父子进程约定持有管道的入口或出口
(5)、一个管道只能实现单向的数据流
使用ipcs可以查看当前系统中IPC资源的情况。ipcrm -m shmid ipcrm -s semid
ipcrm -q msgid
4.shm
各个进程都能够共同访问的共享的内存区域;是独立于所有的进程空间之外的地址区域; (不相关)进程之间的通信
进程对于共享内存的操作与管理主要是:
(1)、申请创建一个共享内存区域(操作系统内核是不可能主动为进程创建共享内存的!),操作系统内核得到申请然后创建
(2)、申请使用一个已存在的共享内存区域
(3)、申请释放共享内存区域(操作系统内核也是不可能主动释放共享内存区域的!),操作系统内核得到申请然后释放
说明key_t key
i)key_t是一个long类型,是IPC资源外部约定的key(关键)值,通过key值映射对应的唯一存在的某一个IPC资源
ii)通过key_t的值就能够判断某一个对应的共享内存区域在哪,是否已经创建等等。
iii)一个key值只能映射一个共享内存区域,但同时还可以映射一个信号量,而且还能同时映射一个消息队列资源,于是就可以使用一个key值管理三种不同的资源
key_t值的产生,有两种方式:
i)把key值写死; //自己直接写一个数字即可
ii)根据文件的inode编号生成。需要调用的API:ftok("./tmp/a.c", 3)方法,该方法是获取指定文件的inode编号在根据第二个参数计算得到最终的一个整型量。
shm的使用:
i>、建立进程与共享内存的映射关系
ii>、读/写(直接使用指针即可
iii>、如果对于共享内存的使用结束,此时就要断开与共享内存的映射
对于第一步来说,需要使用的API:shmat()方法。
对于第三步来说,需要使用的API:shmdt()方法。
被映射正在使用共享内存是否此时可以执行删除操作呢
是,虽然可以执行删除操作,却不能将其直接删除掉。而是做了2个操作
i>、将其状态置为dest(可回收状态)
ii>、将其key值置为0x00000000,IPC_PRIVATE值
当共享内存处于dest(待回收状态),则将其资源设为"私有"(只能将该共享资源分享给其子进程,其它进程无法创建于该资源的使用),当所有的使用该共享内存的进程都退出,此时操作系统才回收共享内存
共享内存的控制
共享内存的控制信息可以通过shmctl()方法获取,会保存在struct_shmid_ds结构体中
共享内存的控制主要是shmid_ds,即就是共享内存的控制信息
cmd:看执行什么操作(1、获取共享内存信息;2、设置共享内存信息;3、删除共享内存)
API:int shmctl(int shmid, int cmd, struct shmid_ds *buf)
5.sem
原因:进程在访问共享资源是存在冲突的,必须的有一种强制手段说明这些共享资源的访问规则------>信号量
sem:表示的是一种共享资源(空闲)的个数,对共享资源的访问规则
i>、用一种数量去标识某一种共享资源的个数(空闲)
ii>、当有进程需要访问对应的共享资源的时候,则需要先查看(申请),根据资源对应的当前可用数量进行申请。(申请所需要使用的资源个数)
iii>、资源的管理者(操作系统内核),就使用当前的资源个数减去要申请的资源个数,结果 >=0,表示有可用资源,允许该进程继续访问;否则表示资源不可用,则告诉进程(暂停或者立即返回)
iv>、资源数量的变化就表示资源的占用和释放。占用:使得可用资源减少;释放:使得可用资源增加
创建信号量集:int semid = semget(key_t key, int nsems, int semflg)
初始化信号量:
信号量ID事实上是信号量集合的ID,一个ID对应的是一组信号量。此时就使用信号量ID设置整个信号量集合,这种操作分为2种大的可能性
i>、针对信号量集合中的一个信号量进行设置;信号量集合中的信号量是按照数组的方式被管理起来的,从而可以直接使用信号的数组下标来进行访问
ii>、针对整个信号量集和进行统一的设置。
需要使用的API:semctl()方法。
int semctl(int semid, int semnum, int cmd, ...);
cmd参数
GETALL | 获取信号量集合中所有信号量的资源个数 |
SETALL | 设置所有 |
GETVAL | 获取其中一个 |
SETVAL | 设置其中一个 |
第四个参数 可变参
如果cmd是GETALL、SETALL、GETVAL、SETVAL...的话,则需要提供第四个参数。第四个参数是一个共用体,这个共用体在程序中必须的自己定义(作用:初始化资源个数),定义格式如下:
信号量的操作:
API:semop()方法。 (op:operator操作)
int semop(int semid, struct sembuf *sops, unsigned nsops);
第二个参数需要借助结构体struct sembuf:
通过下标直接对其信号量sem_op进行加减即可
信号量的特征:
如果有进程通过信号量申请共享资源,而且此时资源个数已经小于0,则此时对于该进程,有两种可能性:等待资源,不等待。
如果此时进程选择等待资源,则操作系统内核会针对该信号量构建进程等待队列,将等待的进程加入到该队列之中。
如果此时有进程释放资源,则会:(1)、先将资源个数增加;(2)、从等待队列中抽取第一个进程;(3)、根据此时资源个数和第一个进程需要申请的资源个数进行比较,结果大于0,则唤醒该进程;结果小于0,则让该进程继续等待。
所以信号量的操作和共享内存一般联合使用来达到进程间的通信
6.msg
就是在进程间架起通道,从宏观上看是一样的,但是管道在字节流上是连续的,消息队列在发送数据时,分为一个一个独立的数据单元,也就是消息体,每个消息体都是固定大小的存储块,在字节流上不连续;
消息队列与管道不同的地方在于:管道中的数据并没有分割为一个一个的数据独立单位,在字节流上是连续的。然而,消息队列却将数据分成了一个一个独立的数据单位,每一个数据单位被称为消息体。每一个消息体都是固定大小的存储块儿,在字节流上是不连续的。
创建消息队列
int msgget(key_t key, int msgflg); //创建0个消息队列
消息的发送和消息的接收
在发送消息的时候动态的创建消息队列;
(1)、msgsnd()方法在发送消息的时候,是在消息体结构体中指定,当前的消息发送到消息队列集合中的哪一个消息队列上。
(2)、消息体结构体中就必须包含一个type值,type值是long类型,而且还必须是结构体的第一个成员。而结构体中的其他成员都被认为是要发送的消息体数据。
(3)、无论是msgsnd()发送还是msgrcv()接收时,只要操作系统内核发现新提供的type值对应的消息队列集合中的消息队列不存在,则立即为其创建该消息队列
总结:为了能够顺利的发送与接收,发送方与接收方需要约定:i>、同样的消息体结构体;(2)、发送方与接收方在发送和接收的数据块儿大小上要与消息结构体的具体数据部分保持一致! 否则:将不会读出正确的数据
重点注意:
消息结构体被发送的时候,只是发送了消息结构体中成员的值,如果结构体成员是指针,并不会将指针所指向的空间的值发送,而只是发送了指针变量所保存的地址值。数组作为消息体结构体成员是可以的。因为整个数组空间都在消息体结构体中
long mtype制定消息队列编号,下面的数组才是要发送的数据,计算大小,也是这个数组所申请的空间大小。接收方倒数第二个参数为:mtype的值(制定的消息队列编号)。
均是指针连接;
在接收的时候的指明是哪个消息队列进行接收(比发送多了一个参数);
7.socket
网络之间不同进程间通信,相当于网络编程部分了
五、线程实现
1.线程的API函数:
函数名称 | 说明 |
pthread_create() | 创建线程 |
pthread_equal() | 比较线程 |
pthread_self() | 得到当前线程ID |
pthread_exit() | 线程内部退出 |
pthread_join() | 等一个线程的结束(让一个线程加入到另一个线程的执行队列之后) |
pthread_cancel() | 终止某线程的执行 |
pthread_t | 线程ID(线程唯一标识) |
(1)pthread_create():
Linux 中的 pthread_create() 函数用来创建线程,它声明在<pthread.h>
头文件中,语法格式如下:
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
各个参数的含义是:
1) pthread_t *thread:传递一个 pthread_t 类型的指针变量,也可以直接传递某个 pthread_t 类型变量的地址。pthread_t 是一种用于表示线程的数据类型,每一个 pthread_t 类型的变量都可以表示一个线程。
2) const pthread_attr_t *attr:用于手动设置新建线程的属性,例如线程的调用策略、线程所能使用的栈内存的大小等。大部分场景中,我们都不需要手动修改线程的属性,将 attr 参数赋值为 NULL,pthread_create() 函数会采用系统默认的属性值创建线程。
pthread_attr_t 类型以结构体的形式定义在<pthread.h>
头文件中,此类型的变量专门表示线程的属性。关于线程属性,您可以阅读《线程属性有哪些,如何自定义线程属性?》一文做详细地了解。
3) void *(*start_routine) (void *):以函数指针的方式指明新建线程需要执行的函数,该函数的参数最多有 1 个(可以省略不写),形参和返回值的类型都必须为 void* 类型。void* 类型又称空指针类型,表明指针所指数据的类型是未知的。使用此类型指针时,我们通常需要先对其进行强制类型转换,然后才能正常访问指针指向的数据。
如果该函数有返回值,则线程执行完函数后,函数的返回值可以由 pthread_join() 函数接收。
4) void *arg:指定传递给 start_routine 函数的实参,当不需要传递任何数据时,将 arg 赋值为 NULL 即可。
如果成功创建线程,pthread_create() 函数返回数字 0,反之返回非零值。各个非零值都对应着不同的宏,指明创建失败的原因,常见的宏有以下几种:
- EAGAIN:系统资源不足,无法提供创建线程所需的资源。
- EINVAL:传递给 pthread_create() 函数的 attr 参数无效。
- EPERM:传递给 pthread_create() 函数的 attr 参数中,某些属性的设置为非法操作,程序没有相关的设置权限。
以上这些宏都声明在 <errno.h> 头文件中,如果程序中想使用这些宏,需提前引入此头文件。
常用的一般为第一三四参数,其中第四个参数的传值例子如下:
//向线程传递参数
#include <stdio.h>
#include <pthread.h>
#include <stdlib.c>
#include <string.h>
typedef struct Student{
int num;
char name[10];
}info;
void *message(void *arg){
info *p = (info *)arg;
printf("num:%d name:%s\n", p->num, p->name);
}
void *read_str(void *arg){
int *fd;
fd = (int *)arg;
//fd[0] = ((int *)arg)[0];
//fd[1] = ((int *)arg)[1];
printf("fd[0]:%d fd[1]:%d\n", fd[0], fd[1]);
}
int main(int argc, char *argv[]){
info *st = (info *)malloc(sizeof(info));
st->num = 10;
strcpy(str->name, "xiaoming");
int fd[2];
fd[0] = 12;
fd[1] = 32;
pthread_t t1,t2;
//创建两条线程,第一条传送结构体,第二条传送数组
pthread_create(&t1, NULL, message, (void *)st);
pthread_create(&t2, NULL, read_str, (void *)fd);
while(1);
free(st);
return 0;
}
(2)pthread_join()
如果想获取某个线程执行结束时返回的数据,可以调用 pthread_join() 函数来实现。
pthread_join() 函数声明在<pthread.h>
头文件中,语法格式如下:
int pthread_join(pthread_t thread, void ** retval);
thread 参数用于指定接收哪个线程的返回值;retval 参数表示接收到的返回值,如果 thread 线程没有返回值,又或者我们不需要接收 thread 线程的返回值,可以将 retval 参数置为 NULL。
pthread_join() 函数会一直阻塞调用它的线程,直至目标线程执行结束(接收到目标线程的返回值),阻塞状态才会解除。如果 pthread_join() 函数成功等到了目标线程执行结束(成功获取到目标线程的返回值),返回值为数字 0;反之如果执行失败,函数会根据失败原因返回相应的非零值,每个非零值都对应着不同的宏,例如:
- EDEADLK:检测到线程发生了死锁。
- EINVAL:分为两种情况,要么目标线程本身不允许其它线程获取它的返回值,要么事先就已经有线程调用 pthread_join() 函数获取到了目标线程的返回值。
- ESRCH:找不到指定的 thread 线程。
以上这些宏都声明在 <errno.h> 头文件中,如果程序中想使用这些宏,需提前引入此头文件。
再次强调,一个线程执行结束的返回值只能由一个 pthread_join() 函数获取,当有多个线程调用 pthread_join() 函数获取同一个线程的执行结果时,哪个线程最先执行 pthread_join() 函数,执行结果就由那个线程获得,其它线程的 pthread_join() 函数都将执行失败。
对于一个默认属性的线程 A 来说,线程占用的资源并不会因为执行结束而得到释放。而通过在其它线程中执行pthread_join(A,NULL);
语句,可以轻松实现“及时释放线程 A 所占资源”的目的。
为什么要使用pthread_join()
在很多情况下,主线程生成并起动了子线程,如果子线程里要进行大量的耗时的运算,主线程往往将于子线程之前结束,但是如果主线程处理完其他的事务后,需要用到子线程的处理结果,也就是主线程需要等待子线程执行完成之后再结束,这个时候就要用到pthread_join()方法了。
即pthread_join()的作用可以这样理解:主线程等待子线程的终止。也就是在子线程调用了pthread_join()方法后面的代码,只有等到子线程结束了才能执行。
当A线程调用线程B并 pthread_join() 时,A线程会处于阻塞状态,直到B线程结束后,A线程才会继续执行下去。当 pthread_join() 函数返回后,被调用线程才算真正意义上的结束,它的内存空间也会被释放(如果被调用线程是非分离的)。这里有三点需要注意:
-
被释放的内存空间仅仅是系统空间,你必须手动清除程序分配的空间,比如 malloc() 分配的空间。
2.一个线程只能被一个线程所连接。
3.被连接的线程必须是非分离的,否则连接会出错。
所以可以看出pthread_join()有两种作用: -
用于等待其他线程结束:当调用 pthread_join() 时,当前线程会处于阻塞状态,直到被调用的线程结束后,当前线程才会重新开始执行。
-
对线程的资源进行回收:如果一个线程是非分离的(默认情况下创建的线程都是非分离)并且没有对该线程使用 pthread_join() 的话,该线程结束后并不会释放其内存空间,这会导致该线程变成了“僵尸线程”。
使用方式
pthread_t tid;
pthread_create(&tid, NULL, thread_run,NULL);
pthread_join(tid,NULL);
创建线程之后直接调用pthread_join方法就行了。
#include "stdafx.h"
#include <pthread.h>
#include <stdio.h>
#include <Windows.h>
#pragma comment(lib, "pthreadVC2.lib")
static int count = 0;
void* thread_run(void* parm)
{
for (int i=0;i<5;i++)
{
count++;
printf("The thread_run method count is = %d\n",count);
Sleep(1000);
}
return NULL;
}
int main()
{
pthread_t tid;
pthread_create(&tid, NULL, thread_run,NULL);
// 加入pthread_join后,主线程"main"会一直等待直到tid这个线程执行完毕自己才结束
// 一般项目中需要子线程计算后的值就需要加join方法
pthread_join(tid,NULL);
// 如果没有join方法可以看看打印的顺序
printf("The count is = %d\n",count);
getchar();
return 0;
}
#include <stdio.h>
#include <errno.h> //使用宏 ESRCH
#include <pthread.h>
//线程执行的函数
void *ThreadFun(void *arg)
{
pthread_exit("http://c.biancheng.net");
}
int main()
{
int res;
void * thread_result;
pthread_t myThread;
//创建 myThread 线程
res = pthread_create(&myThread, NULL, ThreadFun, NULL);
if (res != 0) {
printf("线程创建失败");
return 0;
}
//阻塞主线程,等待 myThread 线程执行结束
res = pthread_join(myThread, &thread_result);
if (res != 0) {
printf("1:等待线程失败");
}
//输出获取到的 myThread 线程的返回值
printf("%s\n", (char*)thread_result);
//尝试再次获取 myThread 线程的返回值
res = pthread_join(myThread, &thread_result);
if (res == ESRCH) {
printf("2:等待线程失败,线程不存在");
}
return 0;
}
加了pthread_join()方法的打印:
如果把里面的pthread_join()方法注释掉的打印:
可以看得出来,如果没有加pthread_join()方法,main线程里面直接就执行起走了,加了之后是等待线程执行了之后才执行的后面的代码。
假设程序位于 thread.c 文件中,执行过程如下:
[root@localhost ~]# gcc thread.c -o thread.exe -lpthread
[root@localhost ~]# ./thread.exe
http://c.biancheng.net
2:等待线程失败,线程不存在
在程序的在主线程(main() 函数)中,我们尝试两次调用 pthread_join() 函数获取 myThread 线程执行结束的返回值。通过执行结果可以看到,第一个 pthread_join() 函数成功执行,而第二个 Pthread_join() 函数执行失败。原因很简单,第一个成功执行的 pthread_join() 函数会使 myThread 线程释放自己占用的资源,myThread 线程也就不复存在,所以第二个 pthread_join() 函数会返回 ESRCH。
2.void *
void 在英文中作为名词的解释为 "空虚、空间、空隙",而在 C 语言中,void 被翻译为"无类型",相应的void * 为"无类型指针"。
void 似乎只有"注释"和限制程序的作用,当然,这里的"注释"不是为我们人提供注释,而是为编译器提供一种所谓的注释。
(1)void 的作用
1.对函数返回的限定,这种情况我们比较常见。
2.对函数参数的限定,这种情况也是比较常见的。
一般我们常见的就是这两种情况:
- 当函数不需要返回值值时,必须使用void限定,这就是我们所说的第一种情况。例如:void func(int a,char *b)。
- 当函数不允许接受参数时,必须使用void限定,这就是我们所说的第二种情况。例如:int func(void)。
(2)void 指针的使用规则
1. void 指针可以指向任意类型的数据,就是说可以用任意类型的指针对 void 指针对 void 指针赋值。例如:
int *a; void *p; p=a;
如果要将 void 指针 p 赋给其他类型的指针,则需要强制类型转换,就本例而言:a=(int *)p。在内存的分配中我们可以见到 void 指针使用:内存分配函数 malloc 函数返回的指针就是 void * 型,用户在使用这个指针的时候,要进行强制类型转换,也就是显式说明该指针指向的内存中是存放的什么类型的数据 (int *)malloc(1024) 表示强制规定 malloc 返回的 void* 指针指向的内存中存放的是一个个的 int 型数据。
2. 在 ANSI C 标准中,不允许对 void 指针进行一些算术运算如 p++ 或 p+=1 等,因为既然 void 是无类型,那么每次算术运算我们就不知道该操作几个字节,例如 char 型操作 sizeof(char) 字节,而 int 则要操作 sizeof(int) 字节。而在 GNU 中则允许,因为在默认情况下,GNU 认为 void * 和 char * 一样,既然是确定的,当然可以进行一些算术操作,在这里sizeof(*p)==sizeof(char)。
void 几乎只有"注释"和限制程序的作用,因为从来没有人会定义一个 void 变量,让我们试着来定义:
void a;
这行语句编译时会出错,提示"illegal use of type 'void'"。即使 void a 的编译不会出错,它也没有任何实际意义。
众所周知,如果指针 p1 和 p2 的类型相同,那么我们可以直接在 p1 和 p2 间互相赋值;如果 p1 和 p2 指向不同的数据类型,则必须使用强制类型转换运算符把赋值运算符右边的指针类型转换为左边指针的类型。
float *p1; int *p2; p1 = p2; //其中p1 = p2语句会编译出错, //提示“'=' : cannot convert from 'int *' to 'float *'”,必须改为: p1 = (float *)p2;
而 void * 则不同,任何类型的指针都可以直接赋值给它,无需进行强制类型转换。
void *p1; int *p2; p1 = p2;
但这并不意味着,void * 也可以无需强制类型转换地赋给其它类型的指针。因为"无类型"可以包容"有类型",而"有类型"则不能包容"无类型"。
小心使用 void 指针类型:
按照 ANSI(American National Standards Institute) 标准,不能对 void 指针进行算法操作,即下列操作都是不合法的:
void * pvoid; pvoid++; //ANSI:错误 pvoid += 1; //ANSI:错误 //ANSI标准之所以这样认定,是因为它坚持:进行算法操作的指针必须是确定知道其指向数据类型大小的。 //例如: int *pint; pint++; //ANSI:正确
pint++ 的结果是使其增大 sizeof(int)。
但是 GNU 则不这么认定,它指定 void * 的算法操作与 char * 一致。因此下列语句在 GNU 编译器中皆正确:
pvoid++; //GNU:正确 pvoid += 1; //GNU:正确
pvoid++ 的执行结果是其增大了 1。
在实际的程序设计中,为迎合 ANSI 标准,并提高程序的可移植性,我们可以这样编写实现同样功能的代码:
void * pvoid; ((char *)pvoid)++; //ANSI:错误;GNU:正确 (char *)pvoid += 1; //ANSI:错误;GNU:正确
GNU 和 ANSI 还有一些区别,总体而言,GNU 较 ANSI 更"开放",提供了对更多语法的支持。但是我们在真实设计时,还是应该尽可能地迎合 ANSI 标准。 如果函数的参数可以是任意类型指针,那么应声明其参数为void *。
注:void 指针可以任意类型的数据,可以在程序中给我们带来一些好处,函数中形为指针类型时,我们可以将其定义为 void 指针,这样函数就可以接受任意类型的指针。如:
典型的如内存操作函数 memcpy 和 memset 的函数原型分别为:
void * memcpy(void *dest, const void *src, size_t len); void * memset ( void * buffer, int c, size_t num );
这样,任何类型的指针都可以传入 memcpy 和 memset 中,这也真实地体现了内存操作函数的意义,因为它操作的对象仅仅是一片内存,而不论这片内存是什么类型(参见 C 语言实现泛型编程)。如果 memcpy 和 memset 的参数类型不是 void *,而是 char *,那才叫真的奇怪了!这样的 memcpy 和 memset 明显不是一个"纯粹的,脱离低级趣味的"函数!void 的出现只是为了一种抽象的需要,如果你正确地理解了面向对象中"抽象基类"的概念,也很容易理解 void 数据类型。正如不能给抽象基类定义一个实例,我们也不能定义一个 void(让我们类比的称 void 为"抽象数据类型")变量。
2.线程间的协作
在一个进程中会出现多个线程会访问同一个内存区域,因此就需要使用一种线程间的协作手段来处理
线程的同步机制主要有:互斥量,信号量,条件变量
1)互斥量 :出现了mutex,就为互斥量,为锁机制
函数名称 | 说明 |
pthread_mutex_init() | 动态初始化临界(互斥)资源标识 |
pthread_mutex_destroy() | 销毁临界资源标识 |
pthread_mutex_lock() | 上锁 (阻塞) |
pthread_mutex_trylock() | 尝试上锁 (非阻塞) |
pthread_mutex_unlock() | 解锁 |
pthread_mutex_t 类型 | 互斥量 |
(1)在一个lock(加锁)和unlock(解锁)之间,形成的叫做:临界区域
线程同步:阻塞别人而完成自己(是不是就是让别人等等)。利用互斥量达到同步,使封锁区域最小化
(2)加锁后,没有解锁————>将发生阻塞(不能再进行加锁)
(3)利用互斥量,将程序执行的不确定顺序变为了确定性的顺序
3) 条件变量
i)静态初始化:
ii)动态初始化
分别调用pthread_mutex/cond_init,pthread_mutex/cond_destroy()初始化;
条件变量针对死锁情况(就是没有出现unlock),此时调用pthread_cond_wait()方法也可以进行解锁;也就是说wait()函数会在阻塞之时进行解锁。
pthread_cond_wait()方法是:在阻塞之时,自动解锁。
该方法在遇到pthread_cond_signal()时,唤醒等待的wait()方法,但是不直接执行wait()其后的语句,而是接着原先pthread_cond_signal()其后的方法继续执行,直到遇到pthread_mutex_lock()锁时,此时,转到wait()其后的方法执行。
在这里利用的是pthread_cond_wait()和pthread_cond_signal()方法
条件变量
条件变量是利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作:一个线程等待"条件变量的条件成立"而挂起;另一个线程使"条件成立"(给出条件成立信号)。为了防止竞争,条件变量的使用总是和一个互斥锁结合在一起。
条件变量是利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作:
1)一个线程等待"条件变量的条件成立"而挂起;
2)另一个线程使"条件成立"(给出条件成立信号)。
为了防止竞争,条件变量的使用总是和一个互斥锁结合在一起。
1.主要涉及到下面的函数:
int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr) ---动态创建条件变量
pthread_mutex_lock ---互斥锁上锁
pthread_mutex_unlock ----互斥锁解锁
pthread_cond_wait() / pthread_cond_timedwait -----等待条件变量,挂起线程,区别是后者,会有timeout时间,如 果到了timeout,线程自动解除阻塞,这个时间和 time()系统调用相同意义的。以1970年时间算起。
pthread_cond_signal ----激活等待列表中的线程,
pthread_cond_broadcast() -------激活所有等待线程列表中最先入队的线程
注意:1)上面这几个函数都是原子操作,可以为理解为一条指令,不会被其他程序打断
2)上面这个几个函数,必须配合使用。
3)pthread_cond_wait,先会解除当前线程的互斥锁,然后挂线线程,等待条件变量满足条件。一旦条件变 量满足条件,则会给线程上锁,继续执行pthread_cond_wait
2. 代码实例
编译:gcc thread_test.c -o thread_test -lpthread
------必须加上-lpthread,不然会报错,找不到线程的相关函数,gcc自身没有连接线程、
#include<pthread.h>
#include<unistd.h>
#include<stdio.h>
#include<stdlib.h>
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;/*初始化互斥锁*/
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;//init cond
void *thread1(void*);
void *thread2(void*);
int i = 1; //global
int main(void){
pthread_t t_a;
pthread_t t_b;//two thread
pthread_create(&t_a,NULL,thread2,(void*)NULL);
pthread_create(&t_b,NULL,thread1,(void*)NULL);//Create thread
printf("t_a:0x%x, t_b:0x%x:", t_a, t_b);
pthread_join(t_b,NULL);//wait a_b thread end
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&cond);
exit(0);
}
void *thread1(void *junk){
for(i = 1;i<= 9; i++){
pthread_mutex_lock(&mutex); //互斥锁
printf("call thread1 \n");
if(i%3 == 0)
{
pthread_cond_signal(&cond); //send sianal to t_b
printf("thread1:******i=%d\n", i);
}
else
printf("thread1: %d\n",i);
pthread_mutex_unlock(&mutex);
printf("thread1: sleep i=%d\n", i);
sleep(1);
printf("thread1: sleep i=%d******end\n", i);
}
}
void *thread2(void*junk){
while(i < 9)
{
pthread_mutex_lock(&mutex);
printf("call thread2 \n");
if(i%3 != 0)
pthread_cond_wait(&cond,&mutex); //wait
printf("thread2: %d\n",i);
pthread_mutex_unlock(&mutex);
printf("thread2: sleep i=%d\n", i);
sleep(1);
printf("thread2: sleep i=%d******end\n", i);
}
}
[tandd@localhost test]$ ./thread_test
call thread2
t_a:0xb76f6b70, t_b:0xb6cf5b70:call thread1
thread1: 1
thread1: sleep i=1
thread1: sleep i=1******end
call thread1
thread1: 2
thread1: sleep i=2
thread1: sleep i=2******end
call thread1
thread1:******i=3
thread1: sleep i=3
thread2: 3
thread2: sleep i=3
thread1: sleep i=3******end
call thread1
thread1: 4
thread1: sleep i=4
thread2: sleep i=4******end
call thread2
thread1: sleep i=4******end
call thread1
thread1: 5
thread1: sleep i=5
thread1: sleep i=5******end
call thread1
thread1:******i=6
thread1: sleep i=6
thread2: 6
thread2: sleep i=6
thread1: sleep i=6******end
call thread1
thread1: 7
thread1: sleep i=7
thread2: sleep i=7******end
call thread2
thread1: sleep i=7******end
call thread1
thread1: 8
thread1: sleep i=8
thread1: sleep i=8******end
call thread1
thread1:******i=9
thread1: sleep i=9
thread2: 9
thread2: sleep i=9
thread1: sleep i=9******end
[tandd@localhost test]$
3.线程间同步
锁机制:互斥锁、条件变量、信号量、读写锁
互斥锁:提供了以排他方式数据结构被并发修改的方法
读写锁:写锁优先抢占资源,读锁允许多个线程共同读共享数据,而写锁操作是互斥的
条件变量:以原子方式阻塞进程,直到某个特定条件为真为止
一般情况下:互斥锁起保护作用,条件变量和互斥锁一起使用
线程间通信的目的主要用于线程同步,所以线程没有像进程通信中的用于数据交换的通信机制
4.同步与互斥的区别
当有多个线程的时候,经常需要去同步这些线程以访问同一个数据或资源。例如,假设有一个程序,其中一个线程用于把文件读到内存,而另一个线程用于统计文件中的字符数。当然,在把整个文件调入内存之前,统计它的计数是没有意义的。但是,由于每个操作都有自己的线程,操作系统会把两个线程当作是互不相干的任务分别执行,这样就可能在没有把整个文件装入内存时统计字数。为解决此问题,你必须使两个线程同步工作。
所谓同步,是指散步在不同进程之间的若干程序片断,它们的运行必须严格按照规定的某种先后次序来运行,这种先后次序依赖于要完成的特定的任务。如果用对资源的访问来定义的话,同步是指在互斥的基础上(大多数情况),通过其它机制实现访问者对资源的有序访问。在大多数情况下,同步已经实现了互斥,特别是所有写入资源的情况必定是互斥的。少数情况是指可以允许多个访问者同时访问资源。
所谓互斥,是指散布在不同进程之间的若干程序片断,当某个进程运行其中一个程序片段时,其它进程就不能运行它们之中的任一程序片段,只能等到该进程运行完这个程序片段后才可以运行。如果用对资源的访问来定义的话,互斥某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。但互斥无法限制访问者对资源的访问顺序,即访问是无序的。
六、main函数是线程吗?
1.线程是如何创建起来的:
进程仅仅是一个容器,包含了线程运行中所需要的数据结构等信息。一个进程创建时,操作系统会创建一个线程,这就是主线程,而其他的从线程,却要主线程的代码来创建,也就是由程序员来创建。
当一个程序启动时,就有一个进程被操作系统(OS)创建,与此同时一个线程也立刻运行,该线程通常叫做程序的主线程(Main Thread),因为它是程序开始时就执行的,如果你需要再创建线程,那么创建的线程就是这个主线程的子线程。每个进程至少都有一个主线程,在Winform中,应该就是创建GUI的线程。 主线程的重要性体现在两方面:1.是产生其他子线程的线程;2.通常它必须最后完成执行比如执行各种关闭动作。
2.究竟main函数是进程还是线程呢:
因为它们都是以main()做为入口开始运行的。 是一个线程,同时还是一个进程。在现在的操作系统中,都是多线程的。但是它执行的时候对外来说就是一个独立的进程。这个进程中,可以包含多个线程,也可以只包含一个线程。当用c写一段程序的话,就是在操作系统中起一个进程它包含一个线程。而当用java等开发一个多线程的程序的话,它在操作系统中起了一个进程,但它可以包含多个同时运行的线程。