标准I/O
常见标准I/O函数
fopen
fopen()函数主要用于对文件和终端的输入输出。
#include <stdio.h>
FILE *fopen(const char *filename, const char *mode);
-
filaname:指定的文件名,fopen会把它与一个文件流关联起来。
-
mode:指定文件的打开方式:
“r”/“rb”:只读方式打开
“w”/“wb”:只写方式打开,把文件长度截为0(你可把它想象为>)
“a”/“ab”:只写方式打开,新内容追加到文件尾(你可以想象成>>)
“r+”/“r+b”/“rb+”:以修改方式打开(读写)
“w+”/“w+b”/“wb+”:以修改方式打开,同时把文件内容截为0
“a+”/“a+b”/“ab+”:以修改方式打开,新内容追加到文件结尾
注意:linux会把所有文件都看成是二进制文件,所以那个"b"表示文件是二进制。
返回值:成功时返回一个FILE*指针,失败返回NULL值,NULL定义在stdio.h中。
fread
主要作用是从一个文件流里读取数据,数据从stream读到由ptr指定的数据缓冲区里面。
#include<stdio.h>
size_t fread(void *ptr, size_t size, size_t nitems, FILE *stream);
- ptr:数据读到ptr指定的缓冲区里面。
- size:每个数据记录的长度(类似与char,int,long,float之类,指代每次读取块的大小)。
- nitems:传输的记录个数。
- stream:指定要读取的数据缓冲区。
返回值:读到缓冲区的记录个数(非字节),如果读到文件尾,其返回值可能小于nitems,甚至可以是0(读取文件为空)。
fwrite
主要从stream获取数据记录写到ptr中,返回值是成功写入的记录个数。
#include<stdio.h>
size_t fwrite(const void *ptr, size_t size, size_t items, FILE *stream);
它的接口和fread很像,参考fread。
fclose
关闭指定的文件流stream,使所有未写出的内容全部写出。
#include<stdio.h>
int fclose(FILE *stream);
- stream:指定要关闭的文件流stream。
返回值:如果成功返回0,失败返回EOF,同时会向全局变量errorn写入错误信息码。
fflush
fflush()函数的主要作用是把文件流的所有未写出的内容立刻写出。
#include<stdio.h>
int fflush(FILE *stream);
接口同fclose。
fseek
主要作用是在文件流里面为下一次读写指定位置。
#include<stdio.h>
int fseek(FILE *stream, long int offset, int whence);
-
stream:指定操作的文件流。
-
offset:指定位置。
-
whence:指定偏移方式,具体形式类似系统调用lseek。
返回值:成功返回0,失败返回-1,同时设置errno指出错误。
fgetc,getc,getchar
fgetc()从文件流取出下一个字节并把它作为一个字符返回,如果到达文件结尾或者出现错误的时候返回EOF。其中getchar()从标准输入读取数据。
#include<stdio.h>
int fgetc(FILE *stream);
int getc(FILE *stream);
int getchar();
fputc,putc,putchar
fputc()函数把一个字符写道一个输出文件流中。如果成功返回写入的值,失败则返回EOF。其中puchar()函数将单个字符写道标准输出。
#include<stdio.h>
int fputc(int c, FILE *stream);
int putc(int c, FILE *stream);
int putchar();
fgets和gets
fgets()函数从输入文件流stream读取一个字符串,并把读到的字符写到ptr指向的缓冲区,当遇到如下情况停止:遇到换行符,已经传输n-1个字符,或者到达文件尾。它会把遇到的换行符也传递到接收字符串里去,再加上一个表示结尾的空字节\0。
gets()函数从标准输入读取数据并丢弃遇到的换行符,它在接受字符串的尾部加上NULL。
#include<stdio.h>
char *fgets(char *ptr, int n, FILE *stream);
char *gets(char *ptr);
printf,fprintf,sprintf
printf系列函数能够对各种不同类型的参数进行格式化编排和输出。
printf函数把自己的输出送到标准输出。
fprintf函数把自己的输出送到一个指定的文件流。
sprintf函数把自己的输出和一个结尾空字符写到s指向的缓冲区。
#include<stdio.h>
int printf(const char *format, ...);
int sprintf(char *s, const char *format, ...);
int fprintf(FILE *stream, const char *format, ...);
-
format:指定输出的格式。
-
s:字符串缓冲区。
-
stream:指定的文件流。
scanf,fscanf和sscanf
scanf系列函数从一个文件流读取数据并写到指定地址的变量。
scanf函数将读入的值保存在对应的变量里。
#include<stdio.h>
int scanf(const char *format, ...);
int fscanf(FILE *stream, const char *format, ...);
int ssacnf(const char *s, const char *format, ...);
文件流错误
通过检查文件流的状态来确定是否发生错误。
ferror函数测试一个文件流的错误标识,如果被设置返回一个非0值,否则返回0。
feof函数测试一个文件流的文件尾标识,如果被设置就返回一个非0值,否则返回0。
clearerr函数清楚有stream指定的文件流的文件尾标识和错误标识。
#include<stdio.h>
int ferror(FILE *stream);
int feof(FILE *stream);
void clearerr(FILE *stream);
文件流和文件描述符
每个文件流都有一个底层描述符相关联。
fileno函数可以确定文件流使用那个底层描述符。
fdopen函数在一个已经打开的文件描述符上创建一个新的文件流。
#include<stdio.h>
int fileno(FILE *stream);
FILE *fdopen(int fildes, const char *mode);
使用标准I/O函数
标准IO函数的两个优点:
- 标准I/O函数具有良好的移植性(Portability)。
- 标准I/O函数可以利用缓冲提高性能。
标准IO函数的两个缺点:
- 不容易进行双向通信。
- 有时可能频繁调用fflush函数。
- 需要以FILE结构体指针的形式返回文件描述符。
创建套接字时返回文件描述符,而为了使用标准I/O函数,只能将其转换为FILE结构体指针
,先介绍器转换方法。
利用fdopen函数转换为FILE结构体指针
#include <stdio.h>
FILE * fdopen(int fildes.const char * mode);
//成功时返回转换的FILE结构体指针,失败时返回NULL。
- fildes:需要转换的文件描述符。
- mode:将要创建的FILE结构体指针的模式(mode)信息。
上述函数的第二个参数与fopen函数中的打开模式相同。常用的参数有读模式"r"和写模式"w"。
示例说明
#include <stdio.h>#include <fcnt1.h>
int main(void)
{
FILE *fp;
int fd = open("data.dat", O_WRONLY|O_CREAT[O_TRUNC);
if(fd == -1)
{
fputs("file open error" , stdout);return -1;
}
fp = fdopen(fd,"w");
fputs("Network C programming ln", fp);
fclose(fp);
return 0;
}
利用fileno函数转换为文件描述符
#inc1ude <stdio.h>
int fileno(FILE * stream);
//成功时返回转换后的文件描述符,失败时返回-1。
示例说明
#include <stdio.h>
#include <fcnt1.h>
int main(void){
FILE *fp;
int fd=open("data.dat",o_WRONLYlO_CREATIO_TRUNC);
if(fd==-1)
{
fputs("file open error", stdout);
return -1;
}
printf("First file descriptor: %d \n", fd);
fp=fdopen(fd,"w");
fputs("TCP/IP SOCKET PROGRAMMING \n", fp);
printf("Second file descriptor: %d ln", fileno(fp));
fclose(fp);
return 0;
}
I/O分离问题
"流"分离带来的EOF问题
调用的fclose函数完全终止了套接字而不是半关闭。
解决办法:创建FILE指针前复制文件描述符即可,复制后另外创建1个文件描述符,然后利于各自的文件描述符生成读模式FILE指针和写模式FILE指针。这就为半关闭准备好了环境,因为套接字和文件描述符之间具有如下关系:
销毁所有文件描述符后才能销毁套接字。
但是这样做只是准备了半关闭环境,要进入真正的半关闭状态需要特殊处理,还剩余的一个文件描述符可以同时进行I/O。
复制文件描述符
#include <unistd.h>
int dup(int fildes);
int dup2(int fildes,int fildes2);
//成功时返回复制的文件描述符,失败时返回-1。
- fildes:需要复制的文件描述符。
- fildes2:明确指定的文件描述符整数值。
复制文件描述符后"流"的分离
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#tdefine BUF_SIZE 1024
int main(int argc,char *argv[]){
int serv_sock,c1nt_sock;
FILE* readfp;
FILE* writefp;
struct sockaddr_in serv_adr, clnt_adr;socklen_t clnt_adr_sz;
char buf[BUF_SIZE]={0,};
serv_sock=socket(PF_INET,sOCK_STREAM,0);
memset(&serv_adr,0, sizeof(serv_adr));
serv_adr.sin_family=AF_INET;
serv_adr.sin_addr.s_addr=hton1(INADDR_ANY);
serv_adr.sin_port=htons(atoi(argv[1]));
bind(serv_sock,(struct sockaddr*)&serv_adr,sizeof(serv_adr));
listen(serv_sock, 5);
clnt_adr_sz=sizeof(clnt_adr);
clnt_sock=accept(serv_sock,(struct sockaddr*)&c1nt_adr,&c1nt_adr_sz);
readfp=fdopen(clnt_sock,"r");
writefp=fdopen(dup(clnt_sock),"w");
fputs("FROM SERVER: Hi~ client? ln", writefp);
fputs("I love all of the world ln", writefp);
fputs( "You are awesome! ln", writefp);
fflush(writefp);
shutdown(fileno(writefp), SHUT_WR);
fclose(writefp);
fgets(buf, sizeof(buf),readfp);
fputs(buf,stdout);
fclose(readfp);
return 0;
}
- 调用fdopen函数生成FILE指针。特别是针对dup函数的返回值生成
FILE指针。 - 针对fileno函数返回的文件描述符调用shutdown函数。因此,服务器端进入半关闭状态,并向客户端发送EOF。这一行就是之前所说的发送EOF的方法。调用shutdown函数时,无论复制出多少文件描述符都进入半关闭状态,同时传递EOF。
优于select的epoll
select复用方法其实由来已久,因此,利用该技术后,无论如何优化程序性能也无法同时接人上百个客户端(当然,硬件性能不同,差别也很大)。这种select方式并不适合以Web服务器端开发为主流的现代开发环境,所以要学习Linux平台下的epoll。
基于select的I/O复用技术速度慢的原因
最主要的缺点是:
- 调用select函数后常见的针对所有文件描述符的循环语句;
- 每次调用select函数时都需要向该函数传递监视对象信息。
改进措施:
仅向操作系统传递1次监视对象,监视范围或内容发生变化时只通知发生变化的事项。
这种处理方式Linux支持方式是epoll,Windows的支持方式是IOCP。
select优点:
- 服务器端接入者少;
- 程序应具有兼容性。
实现epoll时必要的函数和结构体
能够克服select函数缺点的epoll函数具有如下优点,这些优点正好与之前的select函数缺点相反。
- 无需编写以监视状态变化为目的的针对所有文件描述符的循环语句;
- 调用对应于select函数的
epoll_wait
函数时无需每次传递监视对象信息。
epoll服务器端实现中需要的3个函数:
- epoll_create:创建保存epoll文件描述符的空间;
- epoll_ctl:向空间注册并注销文件描述符;
- epoll_wait:与select函数类似,等待文件描述符发生变化。
select方式和epoll方式的不同
-
select方式中为了保存监视对象文件描述符,直接声明了fd_set变量。但epoll方式下由操作系统负责保存监视对象文件描述符,因此需要向操作系统请求创建保存文件描述符的空间,此时使用的函数就是epoll_create。
-
为了添加和删除监视对象文件描述符,select方式中需要FD_SET、FD_CLR函数。但在epoll方式中,通过epoll_ctl函数请求操作系统完成。
-
select方式下调用select函数等待文件描述符的变化,而epoll中调用epoll_wait函数。
-
select方式中通过fd_set变量查看监视对象的状态变化(事件发生与否),而epoll方式中通过如下结构体epoll_event将发生变化的(发生事件的)文件描述符单独集中到一起。
结构体epoll_event
struct epol1_event{
__uint32_tevents;
epoll_data_t data;
}
typedef union epoll_data{
void * ptr;
int fd;
__uint32_t u32;
__uint64_t u64;
}epoll_data_t;
声明足够大的epoll_event结构体数组
后,传递给epoll_wait函数时,发生变化的文件描述符信息将被填入该数组。因此,无需像select函数那样针对所有文件描述符进行循环。
epoll_create
#include <sys/epoll.h>
int epoll_create(int size);
//成功时返回epol1文件描述符,失败时返回-1。
- size:epoll实例的大小。
调用epoll_create函数时创建的文件描述符保存空间称为"epoll例程",但有些情况下名称不同,需要稍加注意。通过参数size传递的值决定epoll例程的大小,但该值只是向操作系统提的建议。换言之,size并非用来决定epoll例程的大小,而仅供操作系统参考。Linux 2.6.8之后的内核将完全忽略传入epoll_create函数的size参数,因为内核会根据情况调整epoll例程的大小。
epoll_create函数创建的资源与套接字相同,也由操作系统管理。因此,该函数和创建套接字的情况相同,也会返回文件描述符。也就是说,该函数返回的文件描述符主要用与于区分epoll例程。需要终止时,与其他文件描述符相同,也要调用close函数。
epoll_ctl
生成epoll_ctl例程后,应在其内部注册监视对象文件描述符,使用epoll_ctl函数。
#include <sys/epoll.h>
int epoll_ctl(int epfd,int op, int fd,struct epoll_event * event);
//成功时返回0,失败时返回-1。
- epfd:用于注册监视对象的epoll例程的文件描述符。
- op:用于指定监视对象的添加、删除或更改等操作。
- fd:需要注册的监视对象文件描述符。
- event:监视对象的事件类型。
按照如下形式调用epoll_ctl函数:
epol1_ct1(A,EPOLL_CTL_ADD,B,C);
第二个参数EPOLL_CTL_ADD
意味着"添加",因此上述语句具有如下含义:
“epoll例程A中注册文件描述符B,主要目的是监视参数C中的事件。”
epoll_ct1(A,EPOLL_CTL_DEL,B,NULL);
上述语句中第二个参数EPOLL_CTL_DEL
指"删除",因此该语句具有如下含义:
“从epoll例程A中删除文件描述符B。”
因为删除不需要监视类型,顾可以传递NULL,Linux2.9之前的内核不允许传递NULL(传递epoll_event结构体地址就好)。
op传递常量及含义
- EPOLL_CTL_ADD:将文件描述符注册到epoll例程。
- EPOLL_CTL_DEL:从epoll例程中删除文件描述符。
- EPOLL_CTL_MOD:更改注册的文件描述符的关注事件发生情况。
event传递常量及含义
- EPOLLIN:需要读取数据的情况。
- EPOLLOUT:输出缓冲为空,可以立即发送数据的情况。
- EPOLLPRI:收到OOB数据的情况。
- EPOLLRDHUP:断开连接或半关闭的情况,这在边缘触发方式下非常有用。
- EPOLLERR:发生错误的情况。
- EPOLLET:以边缘触发的方式得到事件通知。
- EPOLLONESHOT:发生一次事件后,相应文件描述符不再收到事件通知。因此需要向
epoll_ctl函数的第二个参数传递EPOLL_CTL_MOD,再次设置事件。
注意:可以通过位或运算同时传递多个上述参数。
epoll_wait
#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event * events, int maxevents,int timeout);
//成功时返回发生事件的文件描述符数,失败时返回-1。
- epfd:表示事件发生监视范围的epoll例程的文件描述符。
- events:保存发生事件的文件描述符集合的结构体地址值。
- maxevents:第二个参数中可以保存的最大事件数。
- timeout:以1/1000秒为单位的等待时间,传递-1时,一直等待直到发生事件。
注意:第二个参数所指缓冲需要动态分配。
int event_cnt;
struct epoll_event * ep_events;
....
ep_events = malloc(sizeof(struct epoll_event)* EPOLL_SIZE); //EPOLL_SIZE是宏常量
....
event_cnt = epoll_wait(epfd,ep_events,EPOLL_SIZE,-1);
调用函数后,返回发生事件的文件描述符数,同时在第二个参数指向的缓冲中保存发生事件的文件描述符集合。因此,无需像select那样插入针对所有文件描述符的循环。
条件触发和边缘触发
条件触发中,只要输入缓冲有数据就会一直通知该事件(epoll默认以条件触发方式工作)。
边缘出发中输入缓冲收到数据时仅注册1次该事件,即使输入缓冲中还留有数据,也不会再进行注册。
边缘触发的服务端实现中必知的两点
- 通过error变量验证错误原因(验证输入缓冲是否为空);
- 为了完成非阻塞(Non-blocking)I/O,更改套接字特性(以阻塞方式工作的read & write函数有可能引起服务器端的长时间停顿)。
error变量验证错误
Linux的套接字相关函数一般通过返回-1通知发生了错误。虽然知道发生了错误,但仅凭这些内容无法得知产生错误的原因。因此,为了在发生错误时提供额外的信息.
Linux声明了如下全局变量:
int errno;
为了访问该变量,需要引入error.h
头文件,因为此头文件中有上述变量的extern
声明。另外,每种函数发生错误时,保存到errno
变量中的值都不同,没必要记住所有可能的值。学习每种函数
的过程中逐一掌握,并能在必要时参考即可。本节只介绍如下类型的错误:
“read函数发现输入缓冲中没有数据可读时返回-1,同时在errmo中保存EAGAIN常量。”
更改套接字特性
Linux提供更改或读取文件属性的如下方法:
include <fcnt1.h>
int fcntl(int filedes, int cmd,. .﹒ );
//成功时返回 cmd参数相关值,失败时返回-1。
- filedes:属性更改目标的文件描述符。
- cmd:表示函数调用的目的。
从上述声明中可以看到,fentl具有可变参数的形式。如果向第二个参数传递F_GETFL,可以获得第一个参数所指的文件描述符属性( int型)。反之,如果传递F_SETFL
可以更改文件描述符属性。若希望将文件(套接字)改为非阻塞模式,需要如下2条语句。
int flag = fcntl(fd,F_GETFL,0);
fcntl(fd,F_SETFL,flaglo_NONBLOCK);
通过第一条语句获取之前设置的属性信息,通过第二条语句在此基础上添加非阻塞O_NONBLOCK标志。调用read & write函数时,无论是否存在数据,都会形成非阻塞文件(套接字)。
条件触发和边缘触发孰优孰劣
边缘触发的优点:可以分离接受数据和处理数据的时间点!
从实现模型的角度看,边缘触发更有可能带来高性能,但不能简单地认为"只要使用边缘触发就一定能提高速度。"
多线程服务器端
线程概念
进程:在操作系统构成单独执行流的单位。
线程:在进程构成单独执行流的单位。
进程间独立的内存:
线程的内部结构:
线程的创建和运行
#include <pthread.h>
int pthread_create(
pthread_t * restrict thread, const pthread_attr_t * restrict attr,void *(*start_routine)(void *),void * restrict arg
);
//成功时返回0,失败时返回其他值
- thread:保存新创建线程ID的变量地址值。线程与进程相同,也需要用于区分不同线程的ID。
- attr:用于传递线程属性的参数,传递NULL时,创建默认属性的线程。
- start_routine:相当于线程main函数的、在单独执行流中执行的函数地址值(函数指针)。
- arg:通过第三个参数传递调用函数时包含传递参数信息的变量地址值。
线程示例
#include<stdio.h>
#include<pthread.h>
#include<unistd.h>
void * thread_main(void *arg);
int main(int argc,char *argv[])
{
pthread_t t_id;
int thread_param = 5;
if(pthread_create(&t_id,NULL,thread_main,(void*)&thread_param)!=0)
{
puts("pthread_create() error");
return -1;
};
sleep(10);
puts("end of main");
return 0;
}
void* thread_main(void *arg)
{
int i;
int cnt = *((int*)arg);
for(i = 0; i < cnt;i++)
{
sleep(1);
puts("running thread");
}
return NULL;
}
输出:
等待线程结果
#include <pthread.h>
itn pthread_join(pthread_t thread, void * status);
//成功时返回0,失败时返回其他值。
- thread:该参数ID的线程终止后才会从该参数返回。
- status:保存线程的main函数返回值的指针变量地址值。
注意:调用该函数的进程(或线程)将进入等待状态,直到第一个参数为ID的线程终止为止。
调用pthread_join函数:
可在临界区内调用的函数
多个线程同时执行一部分代码,可能会引起问题。因此根据临界区是否引起问题,函数可分为以下两类:
- 线程安全函数;
- 非线程安全函数
线程安全函数被多个线程同时调用时也不会引发问题。反之,非线程安全函数被同时调用时会引发问题。但这并非关于有无临界区的讨论,线程安全的函数中同样可能存在临界区。只是在线程安全函数中,同时被多个线程调用时可通过一些措施避免问题。
幸运的是,大多数标准函数都是线程安全的函数。更幸运的是,我们不用自己区分线程安全的函数和非线程安全的函数(在Windows程序中同样如此)。因为这些平台在定义非线程安全函数的同时,提供了具有相同功能的线程安全的函数。比如,gethostbyname函数就不是线程安全的函数:
struct hostent * gethostbyname( const char * hostname);
同时提供线程安全的同一功能的函数。
struct hostent * gethostbyname_r(
const char * name,struct hostent * result,char * buffer, intbuflen,
int * h_errnop);
线程安全函数的名称后缀通常为r
(这与Windows平台不同)。既然如此,多个线程同时访问的代码块中应该调用gethostbyname_r,而不是gethostbyname。可以通过如下方法自动将gethostbyname函数调用改为gethostbyname_r函数调用!
“声明头文件前定义_REENTRANT宏。”
另外,无需为了上述宏定义特意添加#define
语句,可以在编译时通过添加-D_REENTRANT
选项定义宏。
root@my_linux: /tcpip# gcc -D_REENTRANT mythread.c -o mthread -lpthread
线程存在的问题和临界区
多个线程访问同一变量是问题
由于值的改变需要CPU运算完成,CPU从内存中获取数据,计算后在把结果写回内存中的变量中;这其中多个线程操作同一变量时会发生问题。
临界区
函数内同时运行多个线程时引起问题的多条语句构成的代码块。
void * thread_inc(void * arg){
int i;
for(i = 0; i < 50088080; i++)
num += 1;//临界区
return NULL;
}
void * thread_des(void * arg){
int i;
for(i = 0; i< 50080000; i++)
num -= 1;//临界区
return NULL;
}
线程同步
同步的两面性
线程同步需要考虑的两个方面:
- 同时访问同一内存时发生的情况。
- 需要指定访问同一内存空间的线程执行顺序的情况。
互斥量
作用:主要用来解决线程同步访问的问题。
创建和销毁
#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t * mutex,const pthread_mutexattr_t * attr);
int pthread_mutex_destory(pthread_mutex_t * mutex);
//成功时返回0,失败时返回其他值。
- mutex:创建互斥量时传递保存互斥量的变量地址值,销毁时传递需要销毁的互斥量地址值。
- attr:传递即将创建的互斥量属性,没有特别需要指定的属性时传递NULL。
从上述函数声明中也可看出,为了创建相当于锁系统的互斥量,需要声明如下pthread_mutex_t型变量:
pthread_mutex_t mutex;
该变量的地址将传递给pthread_mutex_init函数,用来保存操作系统创建的互斥量(锁系统)。调用pthread_mutex_destroy函数时同样需要该信息。如果不需要配置特殊的互斥量属性,则向第二个参数传递NULL
时,可以利用PTHREAD_MUTEX_INITIALIZER
宏进行如下声明:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
注意:推荐尽可能使用pthread_mutex_init函数进行初始化,因为通过宏定义进行初始化时很难发现发生的错误。
锁住和释放
#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t*mutex);int pthread_mutex_unlock(pthread_mutex_t* mutex);
//成功时返回0,失败时返回其他值。
其调用位置如下:
pthread_mutex_lock(&mutex);
//临界区的开始
. . . . .
//临界区的结束
pthread_mutex_unlock(&mutex);
线程退出临界区是,如果忘了调用pthread_mutex_unlock函数,那么其他为了进入临界区而调用pthread_mutex_lock函数的线程就无法摆脱阻塞状态。这种情况被称为"死锁"。
优化方面:最大限度减少互斥量lock、unlock函数的调用次数。
互斥量示例
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<pthread.h>
#define NUM_THREAD 100
void * thread_inc(void * arg);
void * thread_des(void * arg);
long long num = 0; //long long int
pthread_mutex_t mutex;
int main(int argc,char * argv[])
{
pthread_t thread_id[NUM_THREAD];
int i;
pthread_mutex_init(&mutex,NULL);
printf("sizeof long long:%ld\n",sizeof(long long));
for(i = 0;i < NUM_THREAD;i++)
{
if(i%2)
pthread_create(&(thread_id[i]),NULL,thread_inc,NULL);
else
pthread_create(&(thread_id[i]),NULL,thread_des,NULL);
}
for(i = 0;i < NUM_THREAD;i++)
pthread_join(thread_id[i],NULL);
printf("result:%lld\n",num);
pthread_mutex_destroy(&mutex);
return 0;
}
void * thread_inc(void * arg)
{
int i;
pthread_mutex_lock(&mutex);
for(i = 0;i < 50000000;i++)
num += 1;
pthread_mutex_unlock(&mutex);
return NULL;
}
void * thread_des(void * arg)
{
int i;
pthread_mutex_lock(&mutex);
for(i = 0;i < 50000000;i++)
num -= 1;
pthread_mutex_unlock(&mutex);
return NULL;
}
输出:
信号量
作用:可以用来解决控制线程顺序的问题,其本身可用于负责数据操作过程中的互斥、同步等功能。使用"二进制信号量"完成"控制线程顺序"为中心的同步方法。
创建和销毁
#include <semaphore.h>
int sem_init(sem_t * sem, int pshared,unsigned int value);int sem_destroy(sem_t * sem);
//成功时返回0,失败时返回其他值。
- sem:创建信号量时传递保存信号量的变量地址值,销毁时传递需要销毁的信号量变量地址值。
- pshared:传递其他值时,创建可由多个进程共享的信号量;传递0时,创建只允许1个进程内部使用的信号量。我们需要完成同一进程内的线程同步,故传递0。
- value:指定新创建的信号量初始值。
锁住和释放
#include <semaphore.h>
int sem_post(sem_t * sem);
int sem_wait(sem_t * sem);
//成功时返回0,失败时返回其他值。
- sem:传递保存信号量读取值的变量地址值,传递给sem_post时信号量增1,传递给sem_wait时信号量减1。
调用sem_init函数时,操作系统将创建信号量对象,此对象中记录着"信号量值"整数。该值在调用sem_post函数时增1,调用sem_wait函数时减1。但信号量的值不能小于0,因此,在信号量为0的情况下调用sem_wait函数时,调用函数的线程将进入阻塞状态(因为函数未返回)。当然,此时如果有其他线程调用sem_post函数,信号量的值将变为1,而原本阻塞的线程可以将该信号量重新减为O并跳出阻塞状态。实际上就是通过这种特性完成临界区的同步操作,可以通过如下形式同步临界区(假设信号量的初始值为1)。
sem_wait( &sem);//信号量变为0...
//临界区的开始
// . . . . .
//临界区的结束
sem_post(&sem);//信号量变为1...
信号量示例
应用场景:
线程A从用户输入得到值后存入全局变量num,此时线程B将取走该值并累加,该过程共进行5次,完成后输出总和并退出程序。
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
void * read(void * arg);
void * accu(void * arg);
static sem_t sem_one;
static sem_t sem_two;
static int num;
int main(int argc,char * argv[])
{
pthread_t id_t1,id_t2;
sem_init(&sem_one,0,0);
sem_init(&sem_two,0,1);
pthread_create(&id_t1,NULL,read,NULL);
pthread_create(&id_t2,NULL,accu,NULL);
pthread_join(id_t1,NULL);
pthread_join(id_t2,NULL);
sem_destroy(&sem_one);
sem_destroy(&sem_two);
return 0;
}
void * read(void * arg)
{
int i;
for(i = 0; i < 5; i++)
{
fputs("Input num:",stdout);
sem_wait(&sem_two);
scanf("%d",&num);
sem_post(&sem_one);
}
return NULL;
}
void * accu(void * arg)
{
int sum = 0,i;
for(i = 0; i < 5; i++)
{
sem_wait(&sem_one);
sum += num;
sem_post(&sem_two);
}
printf("Result:%d\n",sum);
return NULL;
}
输出:
线程的销毁
Linux线程并不是在首次调用的线程main函数返回时自动销毁,所以用如下2种方法之一加以明确。否则由线程创建的内存空间将一直存在。
- 调用pthread _join函数。
- 调用pthread_detach函数。
之前调用过pthread_join函数。调用该函数时,不仅会等待线程终止,还会引导线程销毁。但该函数的问题是,线程终止前,调用该函数的线程将进人阻塞状态。因此,通常通过如下函数调用引导线程销毁。
#include <pthread.h>
int pthread_detach(pthread_t thread);
//成功时返回0,失败时返回其他值。
- thread:终止的同时需要销毁的线程ID。
调用上述函数不会引起线程终止或进入阻塞状态,可以通过该函数引导销毁线程创建的内存空间。调用该函数后不能再针对相应线程调用pthread_join函数,这需要格外注意。虽然还有方法在创建线程时可以指定销毁时机,但与pthread_detach方式相比,结果上没有太大差异,故省略其说明。