《TCP/IP网络编程》第18章 多线程服务器端的实现
线程
多线程模型缺点:
- 创建线程过程有一定开销
- 进程间数据交换,需要特殊的IPC技术
上下文切换(Context Switching)是进程的最大开销。
上下文切换,指分时CPU切换进程运行时,需要保存上一个进程相关信息。
线程(Thread),轻量级进程,最低限度降低进程的各种劣势。
线程相比进程优势:
- 线程创建和上下文切换更快
- 线程间交换数据无需特殊技术
线程与进程差异
每个进程拥有独立的内存空间,由数据区(保存全局变量)、堆(Heap,malloc等动态分配)、栈(Stack,函数运行使用)构成。
多个线程共享进程的数据区和堆,拥有不同的栈区域。
线程优势:
- 上下文切换不需要切换数据区和堆
- 可以利用数据区和堆交换数据
进程,在操作系统构成单独执行流的单位。
线程,在进程构成单独执行流的单位。
进程在操作系统内部生成多个执行流,线程在同一进程内部创建多条执行流。
线程创建及运行
POSIX(Portable Operating System Interface for Computer Environment),适用于计算机环境的可移植操作系统接口,提高UNIX系统操作系统间移植性的API规范。
线程创建和执行流程
需请求操作系统在单独的执行流中执行线程的main函数。
#include <pthread.h>
//成功0
int pthread_create(
pthread_t *restrict pthread, //保存新创建线程ID
const pthread_attr_t *restrict attr, //传递线程属性,默认用NULL
void *(*start_routine)(void *), //函数指针
void *srestrict arg //含参数信息的变量地址
);
//restrict, C99中定义
//用于限定和约束指针,
//表明指针是访问一个数据对象的唯一且初始的方式(只能通过这一个指针来修改指向内存中内容),
//帮助编译器优化代码。
//restrict,C语言中的一种类型限定符(Type Qualifiers),
//用于告诉编译器,对象已经被指针所引用,
//不能通过除该指针外所有其他直接或间接的方式修改该对象的内容。
18.thread1.c
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
void *thread_main(void *arg)
{
int cnt = *((int *)arg);
for (int i = 0; i < cnt; i++)
{
sleep(1);
puts("runnint thread");
}
return NULL;
}
int main(int argc, char *argv[])
{
int thread_param = 5;
pthread_t t_id;
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;
};
// gcc 18.thread1.c -o 18.thread1 && ./18.thread1
#include <pthread.h>
//成功0
int pthread_join(
pthread_t thread,
void ** status
);
18.thread2.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>
void *thread_main(void *arg)
{
int cnt = *(int *)arg;
char *msg = (char *)malloc(sizeof(char) * 50);
strcpy(msg, "Hello, I'am thread~\n");
for (int i = 0; i < cnt; i++)
{
sleep(1);
puts("runnint thread");
}
return (void *)msg;
}
int main(int argc, char *argv[])
{
int thread_param = 5;
pthread_t t_id;
if (pthread_create(&t_id, NULL, thread_main, (void *)&thread_param) != 0)
{
puts("pthread_create() error");
return -1;
}
void *thr_ret;
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;
};
// gcc 18.thread2.c -o 18.thread2 && ./18.thread2
非线程安全函数(Thread-safe function),多个线程同时调用可能引发问题。
struct hostent *gethostbyname(const char *hostname);
线程安全函数(Thread-safe function),多个线程同时调用不会引发问题。
struct hostent *gethostbyname_r(const char *name, struc hostent *result, char *buffer, int buflen, int *h_errnop);
_REENTRANT宏会自动调用线程安全函数。
gcc -D_REENTRANT mythread.c -o mthread -lpthread
工作(Worker)线程模型
18.thread3.c
#include <stdio.h>
#include <pthread.h>
int sum = 0;
void *thread_summation(void *arg)
{
int *p = (int *)arg;
int end = p[1];
for (int start = p[0]; start <= end; start++)
sum += start;
return NULL;
}
int main(int argc, char *argv[])
{
pthread_t id_t1;
int range1[] = {1, 500};
pthread_create(&id_t1, NULL, thread_summation, (void *)&range1);
pthread_t id_t2;
int range2[] = {501, 1000};
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;
};
// gcc 18.thread3.c -D_REENTRANT -o 18.thread3 && ./18.thread3
18.thread4.c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>
#define NUM_THREAD 100
long long num = 0;
void *thread_inc(void *arg)
{
for (int i = 0; i < 50000000; i++)
num += 1;
return NULL;
}
void *thread_des(void *arg)
{
for (int i = 0; i < 50000000; i++)
num -= 1;
return (void *)0;
}
int main(int argc, char *argv[])
{
printf("size long long: %ld\n", sizeof(long long));
pthread_t thread_id[NUM_THREAD];
for (int 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 (int i = 0; i < NUM_THREAD; i++)
pthread_join(thread_id[i], NULL);
printf("result: %lld\n", num);
return 0;
};
// gcc 18.thread4.c -D_REENTRANT -o 18.thread4 && ./18.thread4
线程问题和临界区
多线程访问(修改)同一变量可能发生问题。
临界区定义为函数内同时运行多个线程时引起问题的多条语句构成的代码块(访问同一变量,无法保证运行顺序)。
线程同步
同步两面性
线程同步用于解决线程访问顺序引发的问题。
需同步的情况:
-
同时访问同一内存空间
加锁锁定临界区 -
指定访问同一内存空间的线程执行顺序
比如a线程先写入数据,b线程后读取数据
互斥量
互斥量(Muutal Exclusion),表示不允许多个线程同时访问。互斥量就是一把锁。
#include <pthread.h>
//成功0
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
int pthread_mutex_destroy(pthread_mutex_t *mutex);
//attr互斥量属性,默认用NULL
//默认互斥量属性时,可以使用宏PTHREAD_MUTEX_INITIALIZER初始化互斥量,静态初始化(不需要pthread_mutex_destroy)
//pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
#include <pthread.h>
//成功0
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
死锁(Dead-lock),多个线程相互持有对方锁而等待对方释放锁,从而导致线程阻塞而无法运行下去的情况。
合理划定临界区有效范围。
18.mutex.c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>
#define NUM_THREAD 100
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
long long num = 0;
void *thread_inc(void *arg)
{
pthread_mutex_lock(&mutex);
for (int i = 0; i < 50000000; i++)
num += 1;
pthread_mutex_unlock(&mutex);
return NULL;
}
void *thread_des(void *arg)
{
pthread_mutex_lock(&mutex);
for (int i = 0; i < 50000000; i++)
num -= 1;
pthread_mutex_unlock(&mutex);
return (void *)0;
}
int main(int argc, char *argv[])
{
pthread_t thread_id[NUM_THREAD];
pthread_mutex_init(&mutex, NULL);
for (int 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 (int i = 0; i < NUM_THREAD; i++)
pthread_join(thread_id[i], NULL);
printf("result: %lld\n", num);
pthread_mutex_destroy(&mutex);
return 0;
};
// gcc 18.mutex.c -D_REENTRANT -o 18.mutex && ./18.mutex
信号量
#include <semaphore.h>
//成功0
//pshared为0时,创建只允许一个进程内部使用的信号量。
//pshared非0时,创建多个进程共享的信号量。
//value,信号量初始值
int sem_init(sem_t *sem, int pshared, unsigned int value);
int sem_destroy(sem_t *sem);
#include <semaphore.h>
//成功0
int sem_post(sem_t *sem);//信号量+1
int sem_wait(sem_t *sem);//信号量-1
信号量对象sem_t记录信号量值(Semaphore Value)整数,不能小于0。
信号量为0时,调用sem_wait函数,线程会阻塞。
18.semaphore.c
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
#define NUM_THREAD 100
static sem_t sem_one;
static sem_t sem_two;
static int num;
void *read(void *arg)
{
for (int 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;
for (int i = 0; i < 5; i++)
{
sem_wait(&sem_one);
sum += num;
sem_post(&sem_two);
}
printf("Result: %d\n", sum);
return (void *)0;
}
int main(int argc, char *argv[])
{
sem_init(&sem_one, 0, 0);
sem_init(&sem_two, 0, 1);
pthread_t id_t1;
pthread_create(&id_t1, NULL, read, NULL);
pthread_t id_t2;
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;
};
// gcc 18.semaphore.c -D_REENTRANT -o 18.semaphore && ./18.semaphore
线程销毁和多线程并发服务器
释放线程资源
线程在所在进程执行完成后(主线程main函数return)会自动销毁,线程创建的内存空间并不主动释放,形成僵尸线程。
以下方式释放线程资源。
-
pthread_join
阻塞等待线程执行完并释放线程的内存空间。 -
pthread_detach
#include <pthread.h>
//成功0
int pthread_detach(pthread_t thread);
线程分离,非阻塞,线程执行完后自动释放资源。
多线程并发服务器端
18.chat_server.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <pthread.h>
#define PORT 9999
#define BUF_SIZE 100
#define MAX_CLNT 256
pthread_mutex_t mutx = PTHREAD_MUTEX_INITIALIZER;
int clnt_cnt = 0;
int clnt_socks[MAX_CLNT];
void error_handling(const char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
void send_msg(int clnt_sock, char *msg, int len)
{
pthread_mutex_lock(&mutx);
for (int i = 0; i < clnt_cnt; i++)
if (clnt_socks[i] != clnt_sock)
write(clnt_socks[i], msg, len);
pthread_mutex_unlock(&mutx);
}
void *handle_clnt(void *arg)
{
int clnt_sock = *(int *)arg;
char msg[BUF_SIZE];
int str_len;
while ((str_len = read(clnt_sock, msg, sizeof(msg))) != 0)
send_msg(clnt_sock, msg, str_len);
close(clnt_sock);
pthread_mutex_lock(&mutx);
for (int i = 0; i < clnt_cnt; i++)
if (clnt_sock == clnt_socks[i])
{
for (; i < clnt_cnt - 1; i++)
clnt_socks[i] = clnt_socks[i + 1];
break;
}
clnt_cnt--;
pthread_mutex_unlock(&mutx);
return NULL;
}
int main(int argc, char *argv[])
{
pthread_mutex_init(&mutx, NULL);
int serv_sock = socket(PF_INET, SOCK_STREAM, 0);
if (serv_sock == -1)
error_handling("socket() error!");
int opt = 1;
if (setsockopt(serv_sock, SOL_SOCKET, SO_REUSEADDR, (const void *)&opt, sizeof(opt)) == -1)
error_handling("setsockopt() error!");
socklen_t addr_size = sizeof(struct sockaddr_in);
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, addr_size);
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port = htons(PORT);
if (bind(serv_sock, (struct sockaddr *)&serv_addr, addr_size) == -1)
error_handling("bind() error!");
if (listen(serv_sock, 5) == -1)
error_handling("listen() error!");
while (1)
{
struct sockaddr_in clnt_addr;
int clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_addr, &addr_size);
pthread_mutex_lock(&mutx);
clnt_socks[clnt_cnt++] = clnt_sock;
pthread_mutex_unlock(&mutx);
pthread_t t_id;
pthread_create(&t_id, NULL, handle_clnt, (void *)&clnt_sock);
pthread_detach(t_id);
printf("Connected client IP: %s:%d\n", inet_ntoa(clnt_addr.sin_addr), clnt_addr.sin_port);
}
close(serv_sock);
pthread_mutex_destroy(&mutx);
return 0;
}
// gcc 18.chat_server.c -D_REENTRANT -o 18.chat_server && ./18.chat_server
18.chat_client.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <pthread.h>
#define IP "127.0.0.1"
#define PORT 9999
#define BUF_SIZE 1024
#define NAME_SIZE 20
char *gen_random_str(char *str, int length)
{
srand((unsigned)time(NULL));
for (int i = 0; i < length; i++)
{
switch (rand() % 2)
{
case 0:
str[i] = 'A' + rand() % 26;
break;
case 1:
str[i] = 'a' + rand() % 26;
break;
}
}
str[length] = '\0';
return str;
}
void error_handling(const char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
void *send_msg(void *arg)
{
char name[NAME_SIZE] = "DEFAULT";
gen_random_str(name, 7);
int sock = *((int *)arg);
while (1)
{
char msg[BUF_SIZE] = {0};
fgets(msg, BUF_SIZE - 1, stdin);
if (!strcmp(msg, "q\n") || !strcmp(msg, "Q\n"))
{
close(sock);
// 半关闭
exit(0);
}
char name_msg[NAME_SIZE + BUF_SIZE + 3] = {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);
while (1)
{
char name_msg[NAME_SIZE + BUF_SIZE] = {0};
int str_len = read(sock, name_msg, NAME_SIZE + BUF_SIZE - 1);
if (str_len == -1)
return (void *)-1;
// if (str_len == 0) exit(0);
name_msg[str_len] = 0;
fputs(name_msg, stdout);
}
return NULL;
}
int main(int argc, char *argv[])
{
int sock = socket(PF_INET, SOCK_STREAM, 0);
if (sock == -1)
error_handling("socket() error!");
socklen_t addr_size = sizeof(struct sockaddr_in);
struct sockaddr_in addr;
memset(&addr, 0, addr_size);
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = inet_addr(IP);
addr.sin_port = htons(PORT);
if (connect(sock, (struct sockaddr *)&addr, addr_size) == -1)
error_handling("connect() error!");
pthread_t snd_thread;
pthread_create(&snd_thread, NULL, send_msg, (void *)&sock);
pthread_t rcv_thread;
pthread_create(&rcv_thread, NULL, recv_msg, (void *)&sock);
void *thread_return;
pthread_join(snd_thread, &thread_return);
pthread_join(rcv_thread, &thread_return);
close(sock);
return 0;
}
// gcc 18.chat_client.c -D_REENTRANT -o 18.chat_client && ./18.chat_client