线程基本概念
前面我们讲过多进程服务器,但我们知道它开销很大,因此我们才引入线程,我们可以把它看成是一种轻量级进程。它相比进程有如下几个优点:
- 线程的创建和上下文切换开销更小且速度更快。
- 线程间交换数据时无需特殊技术。
进程:在操作系统构成单独执行流的单位。
线程:在进程构成单独执行流的单位。
它们的包含关系是,操作系统 > 进程 > 线程。进程与线程具体差异其实是这样的,每个进程都有独立的完整内存空间,它包括全局数据区,堆区,栈区,而多进程服务器之所以开销大是因为只是为了区分栈区里的不同函数流执行而把数据区,堆区,栈区内存全部复制了一份。而多线程就高效多了,它只把栈区分离出来,进程中的数据区,堆区则共享。具体内存结构示例图如下:
线程创建及运行
线程具有单独的执行流,因此需要单独定义线程的入口函数,而且还需要请求操作系统在单独的执行流中执行该函数,完成这个功能的函数如下:
#include <pthread.h>
int pthread_create(
pthread_t * restrict thread,//保存线程ID
const pthread_attr_t * restrict attr,//线程属性,NULL默认属性
void * (* start_routine)(void *), //线程入口函数,函数指针
void * restrict arg //传递给入口函数的参数
);
实例代码:
#include <stdio.h>
#include <unistd.h>
#include <pthread.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;
}
上面实例是用sleep延迟来控制线程的执行的,如果主线程不做延迟那么执行到return 0;时,进程就结束了,相应的线程也会销毁。而明显用sleep这种方式控制线程执行流是不合理的,下面我们来看看一个更好的延迟函数,调用该函数的进程(或线程)将进入等待状态,直到第一个参数为ID的线程终止为止。而且可以得到线程的入口函数返回值。
#include <pthread.h>
int pthread_join(pthread_t thread, void **status);
参数1:线程ID
参数2:保存线程入口函数的返回值
实例代码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
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);
strcpy(msg, "Hello, I am thread ~ \n");
for (i = 0; i < cnt; i++)
{
puts("running thread");
}
return (void *)msg;
}
线程存在的问题和临界区
前面我们知道了怎么创建线程,但我们都是只创建了一个线程,下面我们再来看看这样一个实例,创建100个线程,它们都访问了同一变量,其中一半对这个变量进行加1操作,一半进行减1操作,按道理其结果会等于0.
#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类型是64位整数型,多线程共同访问
int main(int argc, char *argv[])
{
pthread_t thread_id[NUM_THREAD];
int i;
//创建100个线程,一半执行thread_inc,一半执行thread_des
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); //+1,-1按道理结果是0
return 0;
}
//线程入口函数1
void * thread_inc(void * arg)
{
for (int i = 0; i < 50000000; i++)
num += 1;//临界区(引起问题的语句就是临界区位置)
return NULL;
}
//线程入口函数2
void * thread_des(void * arg)
{
for (int i = 0; i < 50000000; i++)
num -= 1;//临界区
return NULL;
}
从运行结果看并不是0,而且每次运行的结果都不同。那这是什么原因引起的呢? 是因为每个线程访问一个变量是这样一个过程:先从内存取出这个变量值到CPU,然后CPU计算得到改变后的值,最后再将这个改变后的值写回内存。因此,我们可以很容易看出,多个线程访问同一变量,如果某个线程还只刚从内存取出数据,还没来得及写回内存,这时其它线程又访问了这个变量,所以这个值就会不正确了。
接下来我们再来讲讲怎么解决这个问题:线程同步
线程同步
线程同步用于解决线程访问顺序引发的问题,一般是如下两种情况:
- 同时访问同一内存空间时发生的情况
- 需要指定访问同一内存空间的线程执行顺序的情况
针对这两种可能引发的情况,我们分别使用的同步技术是:互斥量和信号量。
- 互斥量
互斥量技术从字面也可以理解,就是临界区有线程访问,其它线程就得排队等待,它们的访问是互斥的,实现方式就是给临界区加锁与释放锁。
#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr); //创建互斥量
int pthread_mutex_destroy(pthread_mutex_t *mutex);//销毁互斥量
int pthread_mutex_lock(pthread_mutex_t *mutex);//加锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);//释放锁
简言之,就是利用lock和unlock函数围住临界区的两端。当某个线程调用pthread_mutex_lock进入临界区后,如果没有调用pthread_mutex_unlock释放锁退出,那么其它线程就会一直阻塞在临界区之外,我们把这种情况称之为死锁。所以临界区围住一定要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;
pthread_mutex_t mutex;
int main(int argc, char *argv[])
{
pthread_t thread_id[NUM_THREAD];
int i;
//互斥量的创建
pthread_mutex_init(&mutex, NULL);
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;
}
/*扩展临界区,减少加锁,释放锁调用次数,但这样变量必须加满到50000000次后其它线程才能访问.
这样是延长了线程的等待时间,但缩短了加锁,释放锁函数调用的时间,这里没有定论,自己酌情考虑*/
void * thread_inc(void * arg)
{
pthread_mutex_lock(&mutex); //互斥量锁住
for (int i = 0; i < 1000000; i++)
num += 1;
pthread_mutex_unlock(&mutex); //互斥量释放锁
return NULL;
}
/*缩短了线程等待时间,但循环创建,释放锁函数调用时间增加*/
void * thread_des(void * arg)
{
for (int i = 0; i < 1000000; i++)
{
pthread_mutex_lock(&mutex);
num -= 1;
pthread_mutex_unlock(&mutex);
}
return NULL;
}
- 信号量
信号量与互斥量类似,只是互斥量是用锁来控制线程访问而信号量是用二进制0,1来完成控制线程顺序。sem_post信号量加1,sem_wait信号量减1,当信号量为0时,sem_wait就会阻断,因此通过这样让信号量加1减1就能控制线程的执行顺序了。
注释:mac上测试信号量函数返回-1失败,以后还是Linux上整吧,也许这些接口已经过时了…
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);//创建信号量
int sem_destroy(sem_t *sem);//销毁信号量
int sem_post(sem_t *sem);//信号量加1
int sem_wait(sem_t *sem);//信号量减1,为0时阻塞
实例代码:线程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;
}
补充:线程的销毁,线程创建后并不是其入口函数返回后就会自动销毁,需要手动销毁,不然线程创建的内存空间将一直存在。一般手动销毁有如下两种方式:1,调用pthread_join函数,其返回后同时销毁线程 ,是一个阻断函数,服务端一般不用它销毁,因为服务端主线程不宜阻断,还要实时监听客服端连接。2,调用pthread_detach函数,不会阻塞,线程返回自动销毁线程,不过要注意调用它后不能再调用pthread_join函数,它与pthread_join主要区别就是一个是阻塞函数,一个不阻塞。
多线程并发服务端的实现
使用多线程实现了一个简单的聊天程序,并对临界区(clnt_cnt,clnt_socks)进行加锁访问.
- 服务端:
//
// main.cpp
// hello_server
//
// Created by app05 on 15-10-22.
// Copyright (c) 2015年 app05. All rights reserved.
//临界区是:clnt_cnt和clnt_socks访问处
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <pthread.h>
#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;
socklen_t clnt_adr_sz;
pthread_t t_id;
if (argc != 2) {
printf("Usage : %s <port> \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; //新连接的客服端保存到clnt_socks数组里
pthread_mutex_unlock(&mutx); //释放锁
//创建线程
pthread_create(&t_id, NULL, handle_clnt, (void*) &clnt_sock);
pthread_detach(t_id); //销毁线程,线程return后自动调用销毁,不阻断
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 *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
- 客服端
//
// main.cpp
// hello_client
//
// Created by app05 on 15-10-22.
// Copyright (c) 2015年 app05. All rights reserved.
//
//
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <pthread.h>
#define BUF_SIZE 100
#define NAME_SIZE 20
void * send_msg(void * arg);
void * recv_msg(void * arg);
void error_handling(char *message);
char name[NAME_SIZE] = "[DEFAULT]";
char msg[BUF_SIZE];
int main(int argc, const char * argv[]) {
int sock;
struct sockaddr_in serv_addr;
pthread_t snd_thread, rcv_thread;
void * thread_return;
if(argc != 4)
{
printf("Usage: %s <IP> <port> \n", argv[0]);
exit(1);
}
sprintf(name, "[%s]", argv[3]); //聊天人名字,配置到编译器参数里
sock = socket(PF_INET, SOCK_STREAM, 0);
if(sock == -1)
error_handling("socket() error");
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 *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}