1.引言
线程类似于进程。如同进程,线程由内核按时间分片进行管理。在单处理器系统中,内核使用时间分片来模拟线程的并发执行,这种方式和进程的相同。而在多处理器系统中,如同多个进程,线程实际上一样可以并发执行。
预备知识:
Linux Socket编程入门——浅显易懂
Linux Socket编程——多进程并发
2.多线程概述
那么为什么对于大多数合作性任务,多线程比多个独立的进程更优越呢?这是因为,线程共享相同的内存空间。不同的线程可以存取内存中的同一个变量。所以,程序中的所有线程都可以读或写声明过的全局变量。如果曾用 fork() 编写过重要代码,就会认识到这个工具的重要性。为什么呢?虽然 fork() 允许创建多个进程,但它还会带来以下通信问题: 如何让多个进程相互通信,这里每个进程都有各自独立的内存空间。对这个问题没有一个简单的答案。虽然有许多不同种类的本地 IPC (进程间通信),但它们都遇到两个重要障碍:强加了某种形式的额外内核开销,从而降低性能。
2.1 线程的创建
pthread_create()函数
#include<pthread.h>
int pthread_create(pthread_t* thread_out, pthread_attr_t const* attr,
void* (*start_routine)(void*), void* arg)
与fork()调用创建一个进程的方法不同,pthread_create()创建的线程并不具备与主线程(即调用pthread_create()的线程)同样的执行序列,而是使其运行start_routine(arg)函数。
PS:另外,在编译时注意加上-lpthread参数,以调用静态链接库。因为pthread并非Linux系统的默认库
参数:
- thread_out:创建线程后的标识符,下面会介绍。
- attr:设置线程属性。传NULL为默认属性(对大多数程序来说,使用默认属性就够了)。
- start_routine:线程运行函数的起始地址(简单说就是函数指针)。同时,start_routine()可以返回一个void *类型的返回值,而这个返回值也可以是其他类型,并由pthread_join()获取。
- arg:运行函数的参数,如果没有使用参数,就直接NULL。
返回值:
- 创建成功返回0。
- 若不为0则说明创建线程失败,常见的错误返回代码为EAGAIN和EINVAL。
- EAGAIN表示系统限制创建新的线程,例如线程数目过多了;
- EINVAL表示第二个参数代表的线程属性值非法。
3.线程的回收
Linux系统中程序的线程资源是有限的,表现为对于一个程序其能同时运行的线程数是有限的。而默认的条件下,一个线程结束后,其对应的资源不会被释放,于是,如果在一个程序中,反复建立线程,而线程又默认的退出,则最终线程资源耗尽,进程将不再能建立新的线程。
和进程一样,线程也需要回收。
回收方式:
linux线程执行和windows不同,Linux线程有两种类型:joinable和unjoinable。(可结的和分离的)。创建线程默认是joinable线程。
- 如果线程是joinable状态,当线程函数自己返回退出时或pthread_exit()时都不会释放线程所占用堆栈和线程描述符(总计8K多)。只有当你调用了pthread_join()之后这些资源才会被释放。
- 由于pthread_join()函数会阻塞等待指定线程退出,然后回收资源,这样就有同步的功能,使一个线程等待另一个线程退出,然后才继续运行,但是对于服务器程序如果主线程在新创建的线程工作时还需要做别的事情,这种方法不是很好。这时就需要由另一个线程来负责回收线程。(这里要注意,兄弟线程可以回收线程,进程不能回收进程!!!)
- 若是unjoinable状态的线程,这些资源在线程函数退出时或pthread_exit()时自动会被释放。unjoinable属性可以在pthread_create()时指定,或在线程创建后在线程中pthread_detach()自己,如:pthread_detach(pthread_self()),将状态改为unjoinable状态,确保资源的释放。
线程退出:在线程创建后,就开始运行相关的线程函数,在该函数运行完之后,该线程也就退出了,这也是线程退出的一种方法。另一种退出线程的方法是使用函数pthread_exit(),这是线程的主动行为。这里要注意的是,在使用线程函数时,不能随意使用exit()退出函数来进行出错处理。由于exit()的作用是使调用进程终止,而一个进程往往包含多个线程,因此,在使用exit()之后,该进程中的所有线程都终止了。在线程中就可以使用pthread_exit()来代替进程中的exit()。
4.代码实例
PS:在编译时注意加上-lpthread参数,以调用静态链接库。因为pthread并非Linux系统的默认库。
/***
Server端
Author:Liang jie
objective:服务端将客户端输入的小写转换为大写,再传回客户端。
*/
#include <stdio.h>
#include <ctype.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <strings.h>
#include <string.h>
#include <arpa/inet.h>
#include <errno.h>
#include <sys/wait.h>
#include<pthread.h>
#define SRV_PORT 10005
#define MAXLINE 8192
#define INET 100
//定义一个结构体,将地址结构和cfd捆绑在一块
struct s_info {
struct sockaddr_in cliaddr;
int connfd;
};
//线程函数
void *do_work(void *arg){
int n,i;
struct s_info *ts =(struct s_info*)arg;
char buf[MAXLINE];
char str[INET];
while(1){
n=read(ts->connfd,buf,MAXLINE); //读客户端
if (n==0){
printf("the client %d has been closed...\n",ts->connfd);
break;
}
printf("receive from %s at PORT %d\n",
inet_ntop(AF_INET,&(*ts).cliaddr.sin_addr.s_addr,str,sizeof(str)),
ntohs((*ts).cliaddr.sin_port)); //打印客户端信息 ,ip和端口号
for(i=0;i<n;i++)
buf[i]= toupper(buf[i]);
write(ts->connfd,buf,n); //回写到客户端
write(STDOUT_FILENO,buf,n); //写到桌面
printf("\n");
}
close(ts->connfd);
pthread_exit(0);
}
int main(int argc,char *argv[] ){
int lfd,cfd;
pthread_t tid;
int i=0;
struct s_info ts[256]; //创建结构体数组
struct sockaddr_in servaddr,cliaddr;
socklen_t cliaddr_len;
//创建一个socket
lfd = socket(AF_INET,SOCK_STREAM,0);
if(lfd<0)
{
printf("socket creation failed\n");
exit(-1);
}
//地址结构清0
bzero(&servaddr,sizeof(servaddr));
servaddr.sin_family=AF_INET;
servaddr.sin_port=htons(SRV_PORT);
servaddr.sin_addr.s_addr=htonl(INADDR_ANY);
//绑定
if(bind(lfd,(struct sockaddr *)&servaddr,sizeof(servaddr))==-1)
{
printf("Bind error.\n");
exit(-1);
}
//设置同一时刻链接服务器上限数
if(listen(lfd,10)==-1)
{
printf("Listen error!\n");
exit(-1);
}
printf("Start to listen!\n");
cliaddr_len=sizeof(cliaddr);
while(1)
{
//阻塞监听客户端请求
cfd=accept(lfd,(struct sockaddr *)&cliaddr,&cliaddr_len);
ts[i].cliaddr=cliaddr; //客户端的地址结构
ts[i].connfd=cfd;
//创建子线程,因为是只读类型,所以最后一个参数可以传地址
pthread_create(&tid,NULL,do_work,(void *)&ts[i]);
//子线程分离,防止僵尸线程的产生
pthread_detach(tid);
i++;
}
return 0;
}
Client端和上一篇文章一样,不用改动。
点击查看~
5.程序运行截图
使用XShell,链接虚拟机,模拟client端。
多个Client端:
Server端:
6.总结
文章从线程的概念开始讲解,到创建子线程,然后回收子线程。多线程相比于多进程来说,相对简单。回收也比较简单。
下一篇文章将会讲解Select()下的socket编程。
文章代码如有困难,可以联系博主~~