TCP/IP网络编程之多线程服务端的实现(二)

线程存在的问题和临界区

上一章TCP/IP网络编程之多线程服务端的实现(一)的thread4.c中,我们发现多线程对同一变量进行加减,最后的结果居然不是我们预料之内的。其实,如果多执行几次程序,会发现每次程序计算的结果都不一样。那么,造成这样的原因是什么呢?

现在,假设我们一个全局变量sum的值为99,我们创建两个线程,要对sum进行加1操作,那么理想情况下,sum的值应为101。那么要对sum加1并赋值给sum,我们可以简写为:sum+=1。于是多线程可以开始对sum进行操作,但因为是多线程,有可能在一个进程内,一个线程还没执行完,另外一个线程得到CPU时间开始执行。

所以,让我们把目光放回:sum+=1这行代码,这段代码其实有两个动作,第一个动作是sum+1,第二个是将之前相加的结果重新赋值给sum。那么,有两个线程并发给sum加1,有可能第一个线程执行完相加的结果,得到100,但在赋值之前便失去了CPU时间,轮到另外一个线程获得CPU时间,执行加1的操作,等到执行完相加的操作,第二个线程的CPU时间到头了,最后,两个线程都要执行赋值操作,最后我们看到sum只有100,而并不是我们预想中的101

线程同步

为了要解决这一问题,我们必须要求在从sum加1到完成sum的赋值这段临界区,只能有一个线程来完成。而就涉及到线程同步了,这里有两种技术可以实现线程同步,分别是“互斥量”(Mutex)和“信号量”(Semaphore)

互斥量

互斥量是“Mutual Exclusion”的简写,表示不允许多个线程同时访问。举个例子,临界区就好比是小房间里的取款机,现在大部分取款机都会装在一个小房间,当需要取款时一个一个人按顺序进入小房间,栓上门开始取款,取款完毕后离开小房间,让下一个人进来取款。把取款的人当做线程,如果多人同时进入小房间(临界区)那肯定会造成安全上的问题,如账号密码泄露。因此,门栓就是互斥量,当栓上门时,不允许其他人(线程)进入小房间(临界区)进行操作

#include <pthread.h>
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

从上述函数声明中可以看出,为了创建相当于锁系统的互斥量,需要声明如下pthread_mutex_t型变量

pthread_mutex_t mutex;

  

该变量的地址将传递给pthread_mutex_init函数,用来保存操作系统创建的互斥量(锁系统)。调用pthread_mutex_destroy函数时同样需要该信息,如果不需要配置特殊的互斥量属性,则向第二个参数传递NULL时,可以利用PTHREAD_MUTEX_INITALIZER宏进行如下声明:

pthread_mutex_t mutex = PTHREAD_MUTEX_INITALIZER;

  

推荐使用pthread_mutex_init函数进行初始化,因为通过宏进行初始化时很难发现发生的错误。接下来介绍利用互斥量锁住或释放临界区时使用的函数

#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t * mutex);
int pthread_mutex_unlock(pthread_mutex_t * mutex);
//成功时返回0,失败时返回其他值

  

通过函数名我们很容易理解函数的作用,进入临界区前调用pthread_mutex_lock,调用该函数时,如果发现有其他线程已进入临界区,则pthread_mutex_lock函数会陷入阻塞,直到进入里面的线程调用pthread_mutex_unlock函数退出临界区为止

mutex.c

#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;
}

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;
    for (i = 0; i < 50000000; i++)
    {
        pthread_mutex_lock(&mutex);
        num -= 1;
        pthread_mutex_unlock(&mutex);
    }
    return NULL;
}

  

  • 第11行:声明了保存互斥量读取值的变量,之所以声明全局变量是因为thread_inc函数和thread_des函数都需要访问互斥量
  • 第32行:销毁互斥量,不需要互斥量时应销毁
  • 第39、42行:实际临界区只是第41行,但此处连同第40行的循环语句一起用作临界区,调用了lock、unlock函数
  • 第50、52行:通过lock、unlock函数围住对应于临界区的第51行语句

编译mutex.c并运行

# gcc mutex.c -D_REENTRANT -o mutex -lpthread
# ./mutex 
result: 0 

  

从运行结果可以看出,已解TCP/IP网络编程之多线程服务端的实现(一)中thread4.c的问题。但确认运行结果需要等待比较长的时间,因为互斥量lock、unlock函数的调用过程耗时较久。首先 ,分析一下thread_inc函数的同步过程

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

  

 以上临界区划分范围较大,但可以最大限度减少互斥量lock、unlock函数的调用次数,上述示例中,thread_des函数比thread_inc函数多调用49,999,999次互斥量lock、unlock函数;但是thread_inc相比于thread_des也不是全无缺点,因为当循环完成之前,不允许任何线程访问   

#include <semaphore.h>
int sem_init(sem_t * sem, int pshared, unsigned int value); 
int sem_destroy(sem_t * sem); 
//成功时返回0,失败时返回其他值

  

  • sem:创建信号量时传递保存信号量的变量地址值,销毁时传递需要销毁的信号量变量地址值
  • pshared:传递其他值时,创建可由多个进程共享的信号量;传递0时,创建只允许一个进程内部使用的信号量,我们需要完成同一进程内的线程同步,故传0
  • value:指定新创建的信号量初始值

上述函数的pshared参数超出我们关注的范围,故默认向其传递0。稍后讲解通过value参数初始化的信号量值是多少,接下来介绍信号量中相当于互斥量lock、unlock的函数

#include <semaphore.h>
int sem_wait(sem_t *sem); 
int sem_post(sem_t *sem); 

  

  •  sem:传递保存信号量读取值的变量地址值,传递给sem_post时信号量加1,传递给sem_wait时信号量减1

调用sem_init函数时,操作系统将创建信号量对象,此对象中记录着“信号量值”(整数)。该值在调用sem_post函数时加1,调用sem_wait函数时减1。但信号量的值不能小于0。因此,在信号量为0的情况下调用sem_wait函数时,调用函数的线程将进入阻塞状态,如果此时有其他线程函数调用sem_post函数,信号量的值将变为1,而原本阻塞的线程可以将该信号量重新减为0并跳出阻塞状态。实际上就是通过这种特性完成临界区的同步操作,可以通过如下形式同步临界区

sem_wait(&sem);
//临界区开始
//……
//临界区结束
sem_post(&sem);

  

上述代码结构中,调用sem_wait函数进入临界区的线程在调用sem_post函数前不允许其他线程进入临界区。信号量的值在0和1之间跳转。因此,具有这种特性的机制称为“二进制信号量”

semaphore.c

#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;
}

  

  • 第14、15行:生成两个信号量,一个信号量的值为0,另一个为1
  • 第35、48行:利用信号量变量sem_two调用wait函数和post函数,这是为了防止在调用accu函数的线程还未取走数据的情况下,调用read函数的线程覆盖原值
  • 第37、46行:利用信号量变量sem_one调用wait和post函数,这是为了防止调用read函数的线程写入新值之前,accu函数再取走旧的数据

编译semaphore.c并运行

# gcc semaphore.c -D_REENTRANT -o semaphore -lpthread
# ./semaphore 
Input num: 1
Input num: 2
Input num: 3
Input num: 4
Input num: 5
Result: 15 

  

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

Linux线程并不是在首次调用的线程main函数返回时自动销毁,所以用如下两种方法之一加以明确,否则由线程创建的内存空间将一直存在

  • 调用pthread_join函数
  • 调用pthread_detach函数

之前调用过pthread_join函数,调用该函数时,不仅会等待线程终止,还会引导线程销毁。但该函数的问题是,线程终止前,调用该函数的线程将进入阻塞状态。因此,通常通过如下函数调用引导线程销毁

#include <pthread.h>
int pthread_detach(pthread_t thread);//成功时返回0,失败时返回其他值

  

  • thread:终止的同时需要销毁的线程ID

调用上述函数不会引起线程终止或进入阻塞状态,可以通过该函数引导销毁线程创建的内存空间。调用该函数后不能针对相应线程调用pthread_join函数,需要注意一下。

多线程并发服务端的实现

chat_serv.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.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;
    int 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;
        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++) // remove disconnected client
    {
        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) // send to all
{
    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);
}

  

  • 第17、18行:用于管理接入的客户端套接字的变量和数组,访问这两个变量的代码将构成临界区
  • 第51行:每当有新连接时,将相关信息写入变量clnt_cnt和clnt_socks
  • 第54行:创建线程向新接入的客户端提供服务,由该线程执行第62行定义的函数
  • 第55行:调用pthread_detach函数从内存中完全销毁已终止的线程
  • 第86行: 该函数负责向所连接的客户端发送信息

chat_clnt.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.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 *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 <IP> <port> <name>\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) // send thread main
{
    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) // read thread main
{
    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);
}

   

编译chat_serv.c并运行 

# gcc chat_serv.c -D_REENTRANT -o chat_serv -lpthread
# ./chat_serv 8500
Connected client IP: 127.0.0.1 
Connected client IP: 127.0.0.1 

  

编译chat_clnt.c并运行

# gcc chat_clnt.c -D_REENTRANT -o chat_clnt -lpthread
# ./chat_clnt 127.0.0.1 8500 Sam
Hi everyone~
[Sam] Hi everyone~
[Amy] Hi Sam!
Hello Amy!
[Sam] Hello Amy!

  

# ./chat_clnt 127.0.0.1 8500 Amy
[Sam] Hi everyone~
Hi Sam!
[Amy] Hi Sam!
[Sam] Hello Amy!

 

posted @ 2018-09-27 21:30  北洛  阅读(1407)  评论(0编辑  收藏  举报