《TCP IP网络编程》第十八章 多线程服务器端的实现

第十八章 多线程服务器端的实现

18.1 理解线程的概念

引入线程的背景

多进程模型的缺点:

  1. 创建进程的过程会带来一定的开销
  2. 为了完成进程间数据交换,需要特殊的IPC技术
  3. 但最主要的是:每秒少则数十次,多则数千次的‘上下文切换’,这个是创建进程时最大的开销。

上下文切换:
运行程序前需要将相应的进程信息读入内存,如果运行进程A后需要紧接着运行进程B,就应该将进程A的相关信息移出内存,并读入进程B相关信息,这就是上下文切换。此时进程A的数据将被移动到硬盘,上下文切换需要很长时间。

引入线程,是为了将进程的各种劣势降低到最低限度(并非消除)而设计的一种“轻量级进程”,较之进程,线程有如下优点:

  1. 线程的创建和上下文切换比进程的创建和上下文切换更快
  2. 线程间交换数据时无需特殊技术

线程和进程的差异

每个进程的内存空间you三个部分构成:

  1. 数据区:用于保存全局变量
  2. 堆区域:用于向malloc等函数的动态分配提供空间
  3. 栈区域:函数运行时会使用

为了得到多条代码流而复制整个内存区域的负担太重了,所以有了线程的出现。不应完全分离内存结构,而只需要分离栈区域,只分离栈区域优点如下:

  1. 上下文切换时不需要切换数据区和堆区域
  2. 可以利用数据区和堆区域交换数据

线程的内存结构:
线程共享堆区域——线程共享数据区——线程i栈区域

为了保持这种结构,线程将在进程内创建并运行。线程和进程的定义:

  1. 进程:在操作系统构成单独执行流的单位,操作系统内可以有多个进程构成的执行流
  2. 线程:在进程构成单独执行流的单位,进程内可以有多个线程构成的执行流

18.2 线程创建及运行

POSIX是为了提高UNIX系列操作系统间的移植性而制定的API规范。

线程的创建和执行流程

#include
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			通过第三个参数传递调用函数时包含传递参数信息的变量地址值

线程创建的例子:

[root@VM_0_10_centos pthread]# cat thread1.c
#include
#include
#include
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(2);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;
}

如何准确预测thread_main函数的运行时间,并让main函数等待这么长时间?

#include
int pthread_join(pthread_t thread, void ** status);
	成功时返回0,失败时返回其他值
	thread 该参数值ID的线程终止后才会从该函数返回
	status	保存线程的main函数返回值的指针变量地址值

调用该函数的进程(或线程)将进入等待状态,直到第一个参数为ID的线程终止为止。
另外,还能得到线程的main函数返回值。

这个pthread_join函数用法:

[root@VM_0_10_centos pthread]# cat thread2.c
#include
#include
#include
#include
void * thread_main(void * arg);

int main(int argc, char ** argv){
        pthread_t t_id;
        int thread_param = 5;
        void * thr_ret;

        if(pthread_create(&t_id, NULL, thread_main, (void*)&thread_param)!=0){
                puts("pthread_create() error");
                return -1;
        }
        if(pthread_join(t_id, &thr_ret)!=0){
                puts("pthread_join() error");
                return -1;
        }
        printf("Thread return message: %s \n",(char*)thr_ret);
        free(thr_ret);
        return 0;
}

void * thread_main(void * arg){
        int i;
        int cnt = *((int*)arg);
        char * msg = (char*)malloc(sizeof(char) * 50);//分配50个字符长度
        strcpy(msg, "Hello, I am thread. \n");
        for(i=0; i<cnt; ++i){
                sleep(1);puts("running thread");
        }
        return (void*)msg;
}

运行结果:

[root@VM_0_10_centos pthread]# gcc thread2.c -o tr2 -lpthread
[root@VM_0_10_centos pthread]# ll
total 32
-rw-r--r-- 1 root root  475 Jul 26 13:55 thread1.c
-rw-r--r-- 1 root root  757 Jul 26 15:34 thread2.c
-rwxr-xr-x 1 root root 8504 Jul 26 13:55 tr1
-rwxr-xr-x 1 root root 8712 Jul 26 15:34 tr2
[root@VM_0_10_centos pthread]# ./tr2
running thread
running thread
running thread
running thread
running thread
Thread return message: Hello, I am thread.

可在临界区内调用的函数

若创建了多个线程,要考虑多个线程同时调用同一个函数时会引发的问题。

这类函数内部存在临界区,多个线程同时执行这部分代码时可能会引起问题。

根据临界区是否引起问题,函数分为2类:

  1. 线程安全函数
  2. 非线程安全函数

编译线程相关代码时,均要添加-D_REENTRANT选项:

gcc -D_REENTRANT mythread.c -o mthread -lpthread
	宏									链接库

工作线程模型

一个进程中创建两个线程,完成计算1-10的和,线程1计算1-5的和,线程2计算6-10的和,最后main函数输出运算结果。

[root@VM_0_10_centos pthread]# cat thread3.c
#include
#include
void * thread_summation(void * arg);
int sum = 0;

int main(int argc, char** argv){
        pthread_t id_t1, id_t2;
        int range1[] = {1, 5};
        int range2[] = {6, 10};

        pthread_create(&id_t1, NULL, thread_summation, (void*)range1);
        pthread_create(&id_t2, NULL, thread_summation, (void*)range2);

        pthread_join(id_t1, NULL);
        pthread_join(id_t2, NULL);

        printf("result: %d \n", sum);
        return 0;
}
void* thread_summation(void * arg){
        int start = ((int*)arg)[0];
        int end = ((int*)arg)[1];
        while(start <= end){
                sum += start;
                ++start;
        }
        return NULL;
}

运行结果:

[root@VM_0_10_centos pthread]# gcc -D_REENTRANT thread3.c  -o tr3 -lpthread
[root@VM_0_10_centos pthread]# ./tr3
result: 55

如果创建了100个线程,全局变量num=0,num在一半线程中加1,另一半线程中减1,最终应该为0才对,但是结果可能并非如此。代码如下:

[root@VM_0_10_centos pthread]# cat thread4.c
#include
#include
#include
#include
#define NUM_THREAD 100

void * thread_inc(void * arg);
void * thread_des(void * arg);

long long num = 0;

int main(int argc, char** argv){
        pthread_t thread_id[NUM_THREAD];
        int i;
        printf("sizeof long long : %d \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);
        return 0;
}

void * thread_inc(void* arg){
        int i;
        for(i=0; i<50000000; ++i){
                num += 1;
        }
        return NULL;
}

void * thread_des(void * arg){
        int i;
        for(i=0; i<50000000; ++i){
                num -= 1;
        }
        return NULL;
}

运行结果如下。每次运行得到的num都不是0,而且都不相同。

[root@VM_0_10_centos pthread]# gcc thread4.c  -D_REENTRANT -o tr4 -lpthread
[root@VM_0_10_centos pthread]# ./tr4
sizeof long long : 8
result:8327160
[root@VM_0_10_centos pthread]# tr4
-bash: tr4: command not found
[root@VM_0_10_centos pthread]# ./tr4
sizeof long long : 8
result:-33709097
[root@VM_0_10_centos pthread]#

18.3 线程存在的问题和临界区

多个线程访问同一变量是问题

线程1访问变量num时应该阻止其他线程访问,直到线程1完成运算。
否则可能会出现:线程1获取了num值,并放入CPU完成了+1的计算,但还未写回num中时,执行流程跳转到了线程2,线程2完成了读取num,加1,并写回num。后来线程1再写回num,就出错了。

临界区位置

临界区定义:函数内,同时运行多个线程时,引起问题的多条语句构成的代码块。

临界区通常位于由线程运行的函数内部。

18.4 线程同步

同步的两面性

需要同步的情况:

  1. 同时访问同一内存空间时发生的情况
  2. 需要指定访问同一内存空间的线程执行顺序的情况

互斥量

互斥量就是一把锁,把临界区锁起来,不允许多个线程同时访问。

#include
int pthread_mutex_init(pthread_mutex_t * mutex, const pthread_mutexattr_t * attr);
int pthread_mutex_destroy(pthread_mutex_t * mutex);
	成功时返回0,失败时返回其他值
	mutex	创建互斥量时传递保存互斥量的变量地址值,销毁时传递需要销毁的互斥量地址值
	attr	传递时将创建的互斥量属性,没有特别需要指定的属性时传递NULL

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);

利用lock和unlock函数围住临界区的两端,此时互斥量相当于一把锁,组织多个线程同时访问。

但是,线程退出临界区时,如果忘了调用unlock函数,那么其他为了进入临界区而调用lock函数的线程就无法摆脱阻塞状态。这就是死锁

利用互斥量解决thread4.c中的问题:

[root@VM_0_10_centos pthread]# cat thread4_mutex.c
#include
#include
#include
#include
#define NUM_THREAD 100

void * thread_inc(void * arg);
void * thread_des(void * arg);

long long num = 0;
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 : %d \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;
}

运行结果:

[root@VM_0_10_centos pthread]# gcc thread4_mutex.c -D_REENTRANT -o tr4_mutex -lpthread
[root@VM_0_10_centos pthread]# ./tr4_mutex
sizeof long long : 8
result:0

上例中,锁加载了for循环外部,临界区的划分范围比较大,运行结果要等较长时间才能出来。但是这是考虑到如下优点所做的决定:
最大限度减少互斥量lock、unlock函数的调用次数。

信号量

这里只讲了利用“二进制信号量0和1”完成“控制线程顺序”为中心的同步方法。

#include
int sem_init(sem_t * sem, int pshared, unsigned int value);
int sem_destroy(sem_t * sem);
	成功时返回0,失败返回其他值
	sem		创建信号量时传递保存信号量的变量地址值,销毁时传递需要销毁的信号量变量地址值
	pshared	传递其他值时,创建可由多个进程共享的信号量;传递0时,创建只允许1个进程内部使用的信号量。
			我们需要完成同一个进程内的线程同步,故传递0
	value	指定新创建的信号量初始值

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,原本阻塞的进程可以将该信号量重新减为零并跳出阻塞状态。

sem_wait(&sem);//假设信号量初始值为1,信号量变为0
// 临界区开始
...
// 临界区结束
sem_post(&sem);// 信号量变为1

信号量的值在0和1之间跳转,故称为二进制信号量。

示例:该示例并非关于同时访问的同步,而是关于控制访问顺序的同步:线程A从用户输入得到值后存入全局变量num,此时线程B将取走该值并累加。该过程共进行5次,完成后输出num。

[root@VM_0_10_centos pthread]# cat semaphore.c
#include
#include
#include

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); // 防止调用accu函数的线程未取走数据的情况下,调用read函数的线程覆盖原值
                scanf("%d", &num);
                sem_post(&sem_one);// 防止调用read函数的线程写入新值前,accu函数取走数据(再次取走旧值)
        }
        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;
}

运行结果:

[root@VM_0_10_centos pthread]# gcc semaphore.c  -D_REENTRANT -o sema -lpthread
[root@VM_0_10_centos pthread]# ./sema
Input num:1
Input num:2
Input num:3
Input num:4
Input num:5
Result: 15

需要好好琢磨,这两个信号量的用法

18.5 线程的销毁和多线程并发服务器端的实现

线程销毁的3个方法

  1. 调用pthread_join方法:调用该函数时,不仅会等待捆绑的线程终止,还会引导线程销毁。但问题是,捆绑的线程终止之前,调用者的线程会进入阻塞状态。
  2. 调用pthread_detach函数:常用的引导线程销毁的函数
#include
int pthread_detach(pthread_t thread);
	成功时返回0,失败时返回其他值
	thread	终止的同时需要销毁的线程ID

多线程并发服务器端的实现

服务器端:

[root@VM_0_10_centos pthread]# cat chat_server.c
#include
#include
#include
#include
#include
#include
#include

#define BUF_SIZE 100
#define MAX_CLNT 256

void * handle_clnt(void * arg);
void send_msg(char * msg, int len);
void error_handling(char* msg);

int clnt_cnt = 0;
int clnt_socks[MAX_CLNT];
pthread_mutex_t mutx;

int main(int argc, char ** argv){
        int serv_sock, clnt_sock;
        struct sockaddr_in serv_adr, clnt_adr;
        int clnt_adr_sz;
        pthread_t t_id;
        if(argc != 2){
                printf("Usage : %s \n", argv[0]);
                exit(1);
        }
        pthread_mutex_init(&mutx, NULL);
        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 = htonl(INADDR_ANY);
        serv_adr.sin_port = htons(atoi(argv[1]));

        if(bind(serv_sock, (struct sockaddr *)& serv_adr, sizeof(serv_adr)) == -1){
                error_handling("bind() error");
        }

        if(listen(serv_sock, 5) == -1){
                error_handling("listen() error");
        }
        while(1){
                clnt_adr_sz = sizeof(clnt_adr);
                clnt_sock = accept(serv_sock, (struct sockaddr*)& clnt_adr, &clnt_adr_sz);
                pthread_mutex_lock(&mutx);
                clnt_socks[clnt_cnt ++ ] = clnt_sock;
                pthread_mutex_unlock(&mutx);

                pthread_create(&t_id, NULL, handle_clnt, (void*) & clnt_sock);
                pthread_detach(t_id);
                printf("Connected client IP : %s \n", inet_ntoa(clnt_adr.sin_addr));

        }
        close(serv_sock);
        return 0;
}

void * handle_clnt(void * arg){
        int clnt_sock = *((int *) arg);
        int str_len = 0, i;
        char msg[BUF_SIZE];

        while((str_len = read(clnt_sock, msg, sizeof(msg))) != 0){
                send_msg(msg, str_len);
        }
        pthread_mutex_lock(&mutx);
        for(i=0; i<clnt_cnt; ++i){
                if(clnt_sock == clnt_socks[i]){
                        while(i++ < clnt_cnt - 1){
                                clnt_socks[i] = clnt_socks[i+1];
                        }
                        break;
                }
        }
        clnt_cnt--;
        pthread_mutex_unlock(&mutx);
        close(clnt_sock);
        return NULL;
}

void send_msg(char * msg, int len){
        int i;
        pthread_mutex_lock(&mutx);
        for(i=0; i<clnt_cnt; ++i){
                write(clnt_socks[i], msg, len);
        }
        pthread_mutex_unlock(&mutx);
}

void error_handling(char * msg){
        fputs(msg,stderr);
        fputc('\n',stderr);
        exit(1);
}

客户端:

[root@VM_0_10_centos pthread]# cat chat_clnt.c
#include
#include
#include
#include
#include
#include
#include
#define NAME_SIZE 20
#define BUF_SIZE 100

void * send_msg(void * arg);
void * recv_msg(void * arg);
void error_handling(char * msg);

char name[NAME_SIZE] = "[DEFAULT]";
char msg[BUF_SIZE];

int main(int argc, char * argv[]){
        int sock;
        struct sockaddr_in serv_addr;
        pthread_t snd_thread, rcv_thread;
        void* thread_return;
        if(argc != 4){
                printf("Usage : %s   \n",argv[0]);
                exit(1);
        }
        sprintf(name, "[%s]", argv[3]);
        sock = socket(PF_INET, SOCK_STREAM, 0);
        memset(&serv_addr, 0, sizeof(serv_addr));
        serv_addr.sin_family = AF_INET;
        serv_addr.sin_addr.s_addr = inet_addr(argv[1]);
        serv_addr.sin_port = htons(atoi(argv[2]));

        if(connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1){
                error_handling("connect() error");
        }
        pthread_create(&snd_thread, NULL, send_msg, (void *)&sock);
        pthread_create(&rcv_thread, NULL, recv_msg, (void *)&sock);
        pthread_join(snd_thread, &thread_return);
        pthread_join(rcv_thread, &thread_return);
        close(sock);
        return 0;
}

void * send_msg(void * arg){
        int sock = *( (int *) arg );
        char name_msg[NAME_SIZE + BUF_SIZE];
        while(1){
                fgets(msg, BUF_SIZE, stdin);
                if(!strcmp(msg,"q\n") || !strcmp(msg, "Q\n")){
                        close(sock);
                        exit(0);
                }
                sprintf(name_msg, "%s %s", name, msg);
                write(sock, name_msg, strlen(name_msg));
        }
        return NULL;
}

void * recv_msg(void * arg){
        int sock = *((int *)arg);
        char name_msg[NAME_SIZE + BUF_SIZE];
        int str_len;
        while(1){
                str_len = read(sock, name_msg, NAME_SIZE + BUF_SIZE - 1);
                if(str_len == -1){
                        return (void *)-1;
                }
                name_msg[str_len] = 0;
                fputs(name_msg, stdout);
        }
        return NULL;
}

void error_handling(char * msg){
        fputs(msg, stderr);
        fputc('\n', stderr);
        exit(1);
}

运行结果:
《TCP IP网络编程》第十八章 多线程服务器端的实现_第1张图片

18.6 习题

(1)单CPU系统中如何同时执行多个进程?请解释该过程中发生的上下文切换

因为系统将CPU切分成多个微小的块后分配给多个进程,为了分时使用CPU,需要”上下文切换“过程。”上下文切换“是指,在CPU改变运行对象时,执行准备的过程,将之前执行的进程数据换出内存,并将待执行进程数据传到内存的工作区域。

(2)为何线程的上下文切换速度相对更快?线程间数据交换为何不需要类似IPC的特别技术?

因为线程进行上下文切换时不需要切换数据区和堆区。同时,可以利用数据区和堆区进行数据交换

(3)请从执行流角度说明进程和线程的区别

进程:在操作系统中构成单独执行流的单位
线程:在进程内构成单独执行流的单位

(6)请说明完全销毁Linux线程的两种方法

pthread_join函数和pthread_detach函数

(7)利用多线程实现回声客户端,要求所有线程共享保存客户端消息的内存空间

//echo_thrserv.c
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define BUF_SIZE 100
void * handle_clnt(void * arg);
void error_handling(char *buf);

char buf[BUF_SIZE];
pthread_mutex_t mutx;

int main(int argc, char *argv[])
{
	int serv_sock, clnt_sock;
	struct sockaddr_in serv_adr, clnt_adr;
	int clnt_adr_sz;
	pthread_t t_id;

	if(argc!=2) {
		printf("Usage : %s \n", argv[0]);
		exit(1);
	}

	pthread_mutex_init(&mutx, NULL);
	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=htonl(INADDR_ANY);
	serv_adr.sin_port=htons(atoi(argv[1]));
	
	if(bind(serv_sock, (struct sockaddr*) &serv_adr, sizeof(serv_adr))==-1)
		error_handling("bind() error");
	if(listen(serv_sock, 5)==-1)
		error_handling("listen() error");

	while(1)
	{
		clnt_adr_sz=sizeof(clnt_adr);
		clnt_sock=accept(serv_sock, (struct sockaddr*)&clnt_adr,&clnt_adr_sz);
		pthread_create(&t_id, NULL, handle_clnt, (void*)&clnt_sock);
		pthread_detach(t_id);
		printf("Connected client IP: %s \n", inet_ntoa(clnt_adr.sin_addr));
	}

	close(serv_sock);
	return 0;
}

void * handle_clnt(void * arg)
{
	int clnt_sock=*((int*)arg);
	int str_len=0;
	
	while(1)
	{
		pthread_mutex_lock(&mutx);
		str_len=read(clnt_sock, buf, sizeof(buf));
		if(str_len<=0)
			break;
		else
			write(clnt_sock, buf, str_len);
		pthread_mutex_unlock(&mutx);
	}
	
	close(clnt_sock);
	return NULL;
}
void error_handling(char *buf)
{
	fputs(buf, stderr);
	fputc('\n', stderr);
	exit(1);
}

(8)上一题有什么问题?

如果不同步,两个以上线程会同时访问内存空间,从而引发问题。相反,如果同步,由于read函数中调用了临界区的参数,可能会发生无法读取其他客户端发送过来的字符串而必须等待的情况

你可能感兴趣的:(Linux网络编程,linux,多线程,网络,c++)