线程
进程模型在处理用户请求的过程中,进程切换上下文的代价比较高,而,一种轻量级的模型可以处理多用户连接请求,那就是线程模型。
线程(thread)是运行在进程中的一个“逻辑流”,现代操作系统都允许在单进程中运行多个线程。线程由操作系统内核管理。每个线程都有自己的上下文(context),包括一个可以唯一标识线程的ID(thread ID,或叫tid)、栈、程序计数器、寄存器等。在同一个进程中,所有的线程共享该进程的整个虚拟地址空间,包括代码、数据、堆、共享库等。
每个进程一开始就会产生一个线程,一般称为主线程,主线程可以再产生子线程,这样的主线程-子线程对可以叫做一个对等线程。
有多进程处理并发,为什么还需要多线程处理并发?
简单来说,就是在同一个进程下,线程上下文切换的开销要比进程小的多
如何理解上下文呢?
我们的代码被CPU执行的时候,是需要一些数据支持的,比如程序计数器告诉CPU代码执行到哪里了,寄存器里存了当前计算的一些中间值,内存里放置了一些当前用到的变量等,从一个计算场景,切换到另一个计算场景,程序计数器、寄存器等这些值重新载入新场景的值,就是线程的上下文切换。
主要线程函数
创建线程
pthread_create函数用来创建一个线程。
int pthread_create(pthread_t *tid, const pthread_attr_t *attr,
void *(*func)(void *), void *arg);
返回:若成功则为0,若出错则为正的Exxx值
每个线程都有一个线程ID(tid)唯一标识,其数据类型为pthread_t,一般是unsigned int。pthread_create函数的第一个输出参数tid就代表了线程ID,如果创建成功,tid就返回正确的线程ID。
第二个参数:每个线程都会有很多属性,比如优先级,是否应该称为一个守护进程等,可通过pthread_attr_t来描述,一般不会特殊设置,可以指定这个参数为NULL。
第三个参数:为新线程的入口函数,该函数可以接收一个参数arg,类型为指针,如果想给线程入口函数传入多个值,那么需要把这些值包装成一个结构体,再把结构体的地址作为pthread_create的第四个参数,在线程入口函数内,再将该地址转为该结构体的指针对象。
在新线程的入口函数内,可以调用pthread_self函数返回线程的tid
pthread_t pthread_self(void)
终止线程
终止一个线程最直接的方法就是在父线程内调用pthread_exit函数
void pthread_exit(void *status)
当调用这个函数之后,父线程会等待其他所有的子线程终止,之后父线程自己终止。
也可以通过调用pthread_cancel来主动终止一个子线程,和pthread_exit不同的是,它可以指定某个子线程终止。
int pthread_cancel(pthread_t tid)
回收已终止线程的资源
通过调用pthread_join回收已终止线程的资源。
int pthread_join(pthread_t tid, void ** thread_return)
当调用pthread_join时,主线程会阻塞,直到对应tid的子线程自然终止。和pthread_cancel不同的是,它不会强迫子线程终止。
分离线程
一个线程的重要属性就是可结合的,或者是可分离的。一个可结合的线程是能够被其他线程杀死和回收资源的;而一个分离的线程不能被其他线程杀死或回收资源。一般来说,默认的属性是可结合的。
可通过调用pthread_detach函数来分离一个线程:
int pthread_detach(pthread_t tid)
在高并发的例子里,每个连接都由一个线程单独处理,在这种情况下,服务器程序并不需要对每个子线程进行终止,这样的话,每个子线程可以在入口函数开始的地方,把自己设置为分离的,这样就能在它终止后自动回收相关的线程资源了,就不需要调用 pthread_join 函数了。
每个连接一个线程处理
每次有新的连接到达后,就创建一个新线程
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>
#include <sys/poll.h>
#include <sys/socket.h>
#include <netinet/in.h>
#define SERV_PORT 43211
#define LISTENQ 1024
#define INIT_SIZE 128
#define MAXLINE 1024
#define MAX_LINE 16384
extern void loop_echo(int);
int tcp_server_listen(int port) {
int listenfd;
listenfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in server_addr;
bzero(&server_addr, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_port = htons(port);
int on = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
int rt1 = bind(listenfd, (struct sockaddr *) &server_addr, sizeof(server_addr));
if (rt1 < 0) {
perror( "bind failed ");
return -1;
}
int rt2 = listen(listenfd, LISTENQ);
if (rt2 < 0) {
perror("listen failed ");
return -1;
}
signal(SIGPIPE, SIG_IGN);
return listenfd;
}
void pthread_run(void *arg)
{
pthread_detach(pthread_self());
int fd = (int)arg;
loop_echo(fd);
}
int main(int argc, char* argv[])
{
int listen_fd = tcp_server_listen(SERV_PORT);
pthread_t tid;
while(1)
{
struct sockaddr_storage ss;
socklen_t slen = sizeof(ss);
int fd = accept(listen_fd, (struct sockaddr *)&ss, &slen);
if(fd < 0)
{
perror("accept failed");
return -1;
}
else
{
pthread_create(&tid, NULL, &pthread_run, (void *)fd);
}
}
}
在新线程入口函数thread_run里,使用了pthread_detach方法,将子线程转变为分离的,意味着子线程独自负责线程资源的回收。
loop_ehco程序如下,在接收客户端的数据后,再编码回送回去
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>
#include <sys/poll.h>
#include <sys/socket.h>
#include <netinet/in.h>
#define SERV_PORT 43211
#define LISTENQ 1024
#define INIT_SIZE 128
#define MAXLINE 1024
#define MAX_LINE 16384
char rot13_char(char c) {
if ((c >= 'a' && c <= 'm') || (c >= 'A' && c <= 'M'))
return c + 13;
else if ((c >= 'n' && c <= 'z') || (c >= 'N' && c <= 'Z'))
return c - 13;
else
return c;
}
void loop_echo(int fd) {
char outbuf[MAX_LINE + 1];
size_t outbuf_used = 0;
ssize_t result;
while (1) {
char ch;
result = recv(fd, &ch, 1, 0);
//断开连接或者出错
if (result == 0) {
break;
} else if (result == -1) {
perror("read error");
break;
}
if (outbuf_used < sizeof(outbuf)) {
outbuf[outbuf_used++] = rot13_char(ch);
}
if (ch == '\n') {
send(fd, outbuf, outbuf_used, 0);
outbuf_used = 0;
continue;
}
}
}
构建线程池处理多个连接
上述程序虽可以正常工作,但如果并发连接过多,就会引起线程的频繁创建和销毁,虽然说线程切换上下文开销不大,但这般频繁的创建销毁也还是会带来不小的开销。
可以使用预创建线程池的方式进行优化。在服务器启动时,可以先按照固定大小预创建多个线程,当有新连接建立时,往连接字队列里放置这个新连接描述字,线程池里的线程负责从连接字队列中取出连接描述字进行处理。
程序的关键在于连接字队列的设计,因为既有往队列中放置描述符的操作,也有从队列中取出描述符的操作。
需要引入两个重要概念,一个是锁mutex,一个是条件变量condition。加锁就是其他线程不能进入;条件变量则是在多个线程需要交互的情况下,用来线程间同步的原语。
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>
#include <sys/poll.h>
#include <sys/socket.h>
#include <netinet/in.h>
#define SERV_PORT 43211
#define THREAD_NUMBER 4
#define BLOCK_QUEUE_SIZE 100
#define LISTENQ 1024
extern void loop_echo(int);
typedef struct
{
/* data */
pthread_t thread_tid; //thread ID
long thread_count; // connections handled
}Thread;
Thread *thread_array;
typedef struct {
int number; //队列里的描述字最大个数
int *fd; //数组指针
int front; //当前队列的头位置
int rear; //当前队列的尾位置
pthread_mutex_t mutex; //锁
pthread_cond_t cond; //条件变量
}block_queue;
int tcp_server_listen(int port) {
int listenfd;
listenfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in server_addr;
bzero(&server_addr, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_port = htons(port);
int on = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
int rt1 = bind(listenfd, (struct sockaddr *) &server_addr, sizeof(server_addr));
if (rt1 < 0) {
perror( "bind failed ");
return -1;
}
int rt2 = listen(listenfd, LISTENQ);
if (rt2 < 0) {
perror("listen failed ");
return -1;
}
signal(SIGPIPE, SIG_IGN);
return listenfd;
}
//初始化队列
void block_queue_init(block_queue *blockQueue, int number)
{
blockQueue->number = number;
blockQueue->fd = calloc(number,sizeof(int));
blockQueue->front = blockQueue->rear = 0;
pthread_mutex_init(&blockQueue->mutex, NULL);
pthread_cond_init(&blockQueue->cond, NULL);
}
//往队列里放置一个描述字fd
void block_queue_push(block_queue *blockQueue, int fd)
{
//一定要先加锁,因为有多个线程需要读写队列
pthread_mutex_lock(&blockQueue->mutex);
//将描述字放到队列尾的位置
blockQueue->fd[blockQueue->rear] = fd;
//如果已经到最后,重置尾的位置
if(++blockQueue->rear == blockQueue->number)
{
blockQueue->rear = 0;
}
printf("push fd %d\n",fd);
//通知其他等待度的线程,有新的连接字符等待处理
pthread_cond_signal(&blockQueue->cond);
//解锁
pthread_mutex_unlock(&blockQueue->mutex);
}
//从队列里独处描述字进行处理
int block_queue_pop(block_queue *blockQueue)
{
//加锁
pthread_mutex_lock(&blockQueue->mutex);
//判断队列里没有新的连接字可以处理,就一直条件等待,直到有新的连接字入队列
while(blockQueue->front == blockQueue->rear)
{
pthread_cond_wait(&blockQueue->cond, &blockQueue->mutex);
}
//取出队列头的连接字
int fd = blockQueue->fd[blockQueue->front];
//如果已经到最后,重置头的位置
if(++blockQueue->front == blockQueue->number)
{
blockQueue->front = 0;
}
printf("pop fd %d",fd);
//解锁
pthread_mutex_unlock(&blockQueue->mutex);
//返回连接字
return fd;
}
void thread_run(void *arg)
{
pthread_t tid = pthread_self();
pthread_detach(tid);
block_queue *blockQueue = (block_queue*)arg;
while(1)
{
int fd = block_queue_pop(blockQueue);
printf("get fd in thread, fd = %ld, tid = %ld\n",fd, tid);
loop_echo(fd);
}
}
int main(int argc, char *argv[])
{
int listen_fd = tcp_server_listen(SERV_PORT);
block_queue blockQueue;
block_queue_init(&blockQueue, BLOCK_QUEUE_SIZE);
thread_array = calloc(THREAD_NUMBER, sizeof(Thread));
int i;
for(i = 0; i < THREAD_NUMBER; i++)
{
pthread_create(&(thread_array[i].thread_tid), NULL, &thread_run, (void *)&blockQueue);
}
while(1)
{
struct sockaddr_storage ss;
socklen_t slen = sizeof(ss);
int fd = accept(listen_fd, (struct sockaddr *)&ss, &slen);
if(fd < 0)
{
perror("accept failed");
return -1;
}
else
{
block_queue_push(&blockQueue, fd);
}
}
return 0;
}
PS:
记得对操作进行加锁和解锁,通过pthread_mutex_lock和pthread_mutex_unlock来完成。
当工作线程没有描述字可用时,需要等待,通过调用pthread_cond_wait,所有的工作线程等待有新的描述字可达。