• 多线程服务器端的实现


    理解线程的概念

    引入线程的背景

    多进程模型的缺点

    ①、创建进程的过程会给操作系统带来相当沉重的负担

    ②、为了完成进程间数据交换,需要特殊的IPC技术

    ③、每秒少则数十次、多则数千次的“上下文切换”是创建进程时最大的开销(主要)

    线程和进程的差异 

     如果以获得多个代码执行流为主要目的,只需分离栈区域,即通过线程。这种方法的优势如下:

    ①、上下文切换时不需要切换数据区和堆

    ②、可以利用数据区和堆交换数据

    线程为了保持多条代码执行流而隔开了栈区域:

      进程是资源分配的最小单位,线程是操作系统调度执行的最小单位。CPU分配时间片的单位是线程。

    线程创建及运行

    线程的创建和执行流程

    每一个线程都有一个唯一的线程 ID,ID 类型为 pthread_t,这个 ID 是一个无符号长整形数,如果想要得到当前线程的线程 ID,可以调用如下函数:

    pthread_t pthread_self(void);	// 返回当前线程的线程ID

     在一个进程中调用线程创建函数,可以得到一个子线程,需要给每一个创建出的线程指定一个处理函数,否则这个线程无法工作。

    1. #include
    2. int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
    3. void *(*start_routine) (void *), void *arg);
    4. // Compile and link with -pthread, 线程库的名字叫pthread, 全名: libpthread.so libptread.a

    参数:

    thread: 传出参数,是无符号长整形数,线程创建成功,会将线程 ID 写入到这个指针指向的内存中

    attr: 线程的属性,一般情况下使用默认属性即可,写 NULL

    start_routine: 函数指针,创建出的子线程的处理动作,也就是该函数在子线程中执行。

    arg: 作为实参传递到 start_routine 指针指向的函数内部

    返回值:线程创建成功返回 0,创建失败返回对应的错误号

    示例:thread1.c:

    1. #include
    2. #include
    3. #include
    4. void* thread_main(void *arg);
    5. int main(int argc, char *argv[])
    6. {
    7. pthread_t t_id;
    8. int thread_param=5;
    9. if(pthread_create(&t_id, NULL, thread_main, (void*)&thread_param)!=0)
    10. {
    11. puts("pthread_create() error");
    12. return -1;
    13. };
    14. sleep(10); puts("end of main");
    15. return 0;
    16. }
    17. void* thread_main(void *arg)
    18. {
    19. int i;
    20. int cnt=*((int*)arg);
    21. for(i=0; i
    22. {
    23. sleep(1); puts("running thread");
    24. }
    25. return NULL;
    26. }

    线程相关代码在编译时需要添加-lpthread选项声明需要连接线程库,只有这样才能调用头文件pthread.h中声明的函数。

    进程终止时会终止内部创建的线程,所以代码中增加了sleep语句

    pthread_exit线程退出

    在编写多线程程序的时候,如果想要让线程退出,但是不会导致虚拟地址空间的释放(针对于主线程),我们就可以调用线程库中的线程退出函数,只要调用该函数当前线程就马上退出了,并且不会影响到其他线程的正常运行,不管是在子线程或者主线程中都可以使用。

    1. #include
    2. void pthread_exit(void *retval);

    参数:线程退出的时候携带的数据,当前子线程的主线程会得到该数据。如果不需要使用,指定为 NULL 

    pthread_join线程回收

    线程和进程一样,子线程退出的时候其内核资源主要由主线程回收,线程库中提供的线程回收函叫做 pthread_join(),这个函数是一个阻塞函数,如果还有子线程在运行,调用该函数就会阻塞,子线程退出函数解除阻塞进行资源的回收,函数被调用一次,只能回收一个子线程,如果有多个子线程则需要循环进行回收。

    另外通过线程回收函数还可以获取到子线程退出时传递出来的数据

    1. #include
    2. // 这是一个阻塞函数, 子线程在运行这个函数就阻塞
    3. // 子线程退出, 函数解除阻塞, 回收对应的子线程资源, 类似于回收进程使用的函数 wait()
    4. int pthread_join(pthread_t thread, void **retval);

    参数:

    thread: 要被回收的子线程的线程 ID

    retval: 二级指针,指向一级指针的地址,是一个传出参数,这个地址中存储了 pthread_exit () 传递出的数据,如果不需要这个参数,可以指定为 NULL

    返回值:线程回收成功返回 0,回收失败返回错误号。

    在子线程退出的时候可以使用 pthread_exit() 的参数将数据传出,在回收这个子线程的时候可以通过 phread_join() 的第二个参数来接收子线程传递出的数据。接收数据有很多种处理方式:

    线程传出数据

    使用子线程栈

      此种方式无法正确接收子线程传递出的数据。原因:如果多个线程共用一个虚拟地址空间,每个线程在栈区都有一块属于自己的内存,相当于栈区被这几个线程平分了,当线程退出时线程在栈区的数据也就被回收了,因此随着子线程的退出,写入到栈区的数据也就被释放了。例如:

    1. // pthread_join.c
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. // 定义结构
    8. struct Persion
    9. {
    10. int id;
    11. char name[36];
    12. int age;
    13. };
    14. // 子线程的处理代码
    15. void* working(void* arg)
    16. {
    17. printf("我是子线程, 线程ID: %ld\n", pthread_self());
    18. for(int i=0; i<9; ++i)
    19. {
    20. printf("child == i: = %d\n", i);
    21. if(i == 6)
    22. {
    23. struct Persion p;
    24. p.age =12;
    25. strcpy(p.name, "tom");
    26. p.id = 100;
    27. // 该函数的参数将这个地址传递给了主线程的pthread_join()
    28. pthread_exit(&p);
    29. }
    30. }
    31. return NULL; // 代码执行不到这个位置就退出了
    32. }
    33. int main()
    34. {
    35. // 1. 创建一个子线程
    36. pthread_t tid;
    37. pthread_create(&tid, NULL, working, NULL);
    38. printf("子线程创建成功, 线程ID: %ld\n", tid);
    39. // 2. 子线程不会执行下边的代码, 主线程执行
    40. printf("我是主线程, 线程ID: %ld\n", pthread_self());
    41. for(int i=0; i<3; ++i)
    42. {
    43. printf("i = %d\n", i);
    44. }
    45. // 阻塞等待子线程退出
    46. void* ptr = NULL;
    47. // ptr是一个传出参数, 在函数内部让这个指针指向一块有效内存
    48. // 这个内存地址就是pthread_exit() 参数指向的内存
    49. pthread_join(tid, &ptr);
    50. // 打印信息
    51. struct Persion* pp = (struct Persion*)ptr;
    52. printf("子线程返回数据: name: %s, age: %d, id: %d\n", pp->name, pp->age, pp->id);
    53. printf("子线程资源被成功回收...\n");
    54. return 0;
    55. }

    使用全局变量、静态变量或堆内存

      位于同一虚拟地址空间中的线程,虽然不能共享栈区数据,但是可以共享全局数据区和堆区的数据,因此在子线程退出的时候可以将传出数据存储到全局变量、静态变量或者堆内存中。

    使用全局变量的例子如下:

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. // 定义结构
    7. struct Persion
    8. {
    9. int id;
    10. char name[36];
    11. int age;
    12. };
    13. struct Persion p; // 定义全局变量
    14. // 子线程的处理代码
    15. void* working(void* arg)
    16. {
    17. printf("我是子线程, 线程ID: %ld\n", pthread_self());
    18. for(int i=0; i<9; ++i)
    19. {
    20. printf("child == i: = %d\n", i);
    21. if(i == 6)
    22. {
    23. // 使用全局变量
    24. p.age =12;
    25. strcpy(p.name, "tom");
    26. p.id = 100;
    27. // 该函数的参数将这个地址传递给了主线程的pthread_join()
    28. pthread_exit(&p);
    29. }
    30. }
    31. return NULL;
    32. }
    33. int main()
    34. {
    35. // 1. 创建一个子线程
    36. pthread_t tid;
    37. pthread_create(&tid, NULL, working, NULL);
    38. printf("子线程创建成功, 线程ID: %ld\n", tid);
    39. // 2. 子线程不会执行下边的代码, 主线程执行
    40. printf("我是主线程, 线程ID: %ld\n", pthread_self());
    41. for(int i=0; i<3; ++i)
    42. {
    43. printf("i = %d\n", i);
    44. }
    45. // 阻塞等待子线程退出
    46. void* ptr = NULL;
    47. // ptr是一个传出参数, 在函数内部让这个指针指向一块有效内存
    48. // 这个内存地址就是pthread_exit() 参数指向的内存
    49. pthread_join(tid, &ptr);
    50. // 打印信息
    51. struct Persion* pp = (struct Persion*)ptr;
    52. printf("name: %s, age: %d, id: %d\n", pp->name, pp->age, pp->id);
    53. printf("子线程资源被成功回收...\n");
    54. return 0;
    55. }

    使用主线程栈

      虽然每个线程都有属于自己的栈区空间,但是位于同一个地址空间的多个线程是可以相互访问对方的栈空间上的数据的。由于很多情况下还需要在主线程中回收子线程资源,所以主线程一般都是最后退出,基于这个原因在下面的程序中将子线程返回的数据保存到了主线程的栈区内存中。

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. // 定义结构
    7. struct Persion
    8. {
    9. int id;
    10. char name[36];
    11. int age;
    12. };
    13. // 子线程的处理代码
    14. void* working(void* arg)
    15. {
    16. struct Persion* p = (struct Persion*)arg;
    17. printf("我是子线程, 线程ID: %ld\n", pthread_self());
    18. for(int i=0; i<9; ++i)
    19. {
    20. printf("child == i: = %d\n", i);
    21. if(i == 6)
    22. {
    23. // 使用主线程的栈内存
    24. p->age =12;
    25. strcpy(p->name, "tom");
    26. p->id = 100;
    27. // 该函数的参数将这个地址传递给了主线程的pthread_join()
    28. pthread_exit(p);
    29. }
    30. }
    31. return NULL;
    32. }
    33. int main()
    34. {
    35. // 1. 创建一个子线程
    36. pthread_t tid;
    37. struct Persion p;
    38. // 主线程的栈内存传递给子线程
    39. pthread_create(&tid, NULL, working, &p);
    40. printf("子线程创建成功, 线程ID: %ld\n", tid);
    41. // 2. 子线程不会执行下边的代码, 主线程执行
    42. printf("我是主线程, 线程ID: %ld\n", pthread_self());
    43. for(int i=0; i<3; ++i)
    44. {
    45. printf("i = %d\n", i);
    46. }
    47. // 阻塞等待子线程退出
    48. void* ptr = NULL;
    49. // ptr是一个传出参数, 在函数内部让这个指针指向一块有效内存
    50. // 这个内存地址就是pthread_exit() 参数指向的内存
    51. pthread_join(tid, &ptr);
    52. // 打印信息
    53. printf("name: %s, age: %d, id: %d\n", p.name, p.age, p.id);
    54. printf("子线程资源被成功回收...\n");
    55. return 0;
    56. }

    在临界区内调用的函数

    临界区的概念

      临界区是指包含有共享数据的一段代码,这些代码可能被多个线程访问或修改。临界区的存在就是为了保证当有一个线程在临界区执行的时候,不能有其他任何线程被允许在临界区执行。

    每个临界区都有相应的进入区和退出区。

    为了保证临界资源的正确使用,可以把临界资源的访问过程分成四个部分:

    进入区。为了进入临界区使用临界资源,在进入区要检查可否进入临界区,如果可以进入临界区,则应设置正在访问临界区的标志,以阻止其他进程同时进入临界区。

    临界区。进程中访问临界资源的那段代码,又称临界段。

    退出区。将正在访问临界区的标志清除。

    剩余区。代码中的其余部分。

    1. do {
    2. entry section; //进入区
    3. critical section; //临界区
    4. exit section; //退出区
    5. remainder section;
    6. //剩余区
    7. } while (true)

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

    ①线程安全函数

    ②非线程安全函数

     可以在编译时通过添加-D_REENTRANT选项定义宏。

       _REENTRANT的作用之一是对部分函数重新定义它们的可安全重入的版本,这些函数名字一般不会发生改变,只是会在函数名后面添加_r字符串,如函数名gethostbyname变成gethostbyname_r。

    线程存在的问题和临界区

    加法运算的理想情况

    理想情况下每个线程轮流对变量进行加法运算

     

    实际上可能出现的问题

     可能在线程1完全增加num值之前,线程2有可能通过切换得到CPU资源

         

    临界区位置

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

    全局变量num是否应该视为临界区?不是!因为它不是引起问题的语句。该变量并非同时运行的语句,只是代表内存区域的声明。临界区通常位于由线程运行的函数内部。如下是两个由线程执行的函数:

    1. void * thread_inc(void * arg)
    2. {
    3. int i;
    4. for(i=0; i<50000000; i++)
    5. num+=1;
    6. return NULL;
    7. }
    8. void * thread_des(void * arg)
    9. {
    10. int i;
    11. for(i=0; i<50000000; i++)
    12. num-=1;
    13. return NULL;
    14. }


    线程同步

    线程同步用于解决线程访问顺序引发的问题,需要同步的情况从如下两方面考虑:

    ①同时访问同一内存空间

    ②需要指定访问同一内存空间的线程执行顺序

    互斥量

     线程退出临界区时,如果忘了调用pthread_mutex_unlock函数,那么其他为了进入临界区而调用pthread_mutex_unlock函数的线程就无法摆脱阻塞状态,这种情况称为“死锁”。

    1. #include
    2. #include
    3. #include
    4. #include
    5. #define NUM_THREAD 100
    6. void * thread_inc(void * arg);
    7. void * thread_des(void * arg);
    8. long long num=0;
    9. pthread_mutex_t mutex;
    10. int main(int argc, char *argv[])
    11. {
    12. pthread_t thread_id[NUM_THREAD];
    13. int i;
    14. pthread_mutex_init(&mutex, NULL);
    15. for(i=0; i
    16. {
    17. if(i%2)
    18. pthread_create(&(thread_id[i]), NULL, thread_inc, NULL);
    19. else
    20. pthread_create(&(thread_id[i]), NULL, thread_des, NULL);
    21. }
    22. for(i=0; i
    23. pthread_join(thread_id[i], NULL);
    24. printf("result: %lld \n", num);
    25. pthread_mutex_destroy(&mutex);
    26. return 0;
    27. }
    28. void * thread_inc(void * arg)
    29. {
    30. int i;
    31. pthread_mutex_lock(&mutex);
    32. for(i=0; i<50000000; i++)
    33. num+=1;
    34. pthread_mutex_unlock(&mutex);
    35. return NULL;
    36. }
    37. void * thread_des(void * arg)
    38. {
    39. int i;
    40. for(i=0; i<50000000; i++)
    41. {
    42. pthread_mutex_lock(&mutex);
    43. num-=1;
    44. pthread_mutex_unlock(&mutex);
    45. }
    46. return NULL;
    47. }

    信号量

             上述代码结构中,调用sem_wait函数进入临界区的线程在调用sem_post函数前不允许其他线程进入临界区,信号量的值在0和1之前,具有此种特性的机制称为“二进制信号量”。以下代码通过两个信号量控制线程对临界区的访问顺序:

    1. #include
    2. #include
    3. #include
    4. void * read(void * arg);
    5. void * accu(void * arg);
    6. static sem_t sem_one;
    7. static sem_t sem_two;
    8. static int num;
    9. int main(int argc, char *argv[])
    10. {
    11. pthread_t id_t1, id_t2;
    12. sem_init(&sem_one, 0, 0);
    13. sem_init(&sem_two, 0, 1);
    14. pthread_create(&id_t1, NULL, read, NULL);
    15. pthread_create(&id_t2, NULL, accu, NULL);
    16. pthread_join(id_t1, NULL);
    17. pthread_join(id_t2, NULL);
    18. sem_destroy(&sem_one);
    19. sem_destroy(&sem_two);
    20. return 0;
    21. }
    22. void * read(void * arg)
    23. {
    24. int i;
    25. for(i=0; i<5; i++)
    26. {
    27. fputs("Input num: ", stdout);
    28. sem_wait(&sem_two);
    29. scanf("%d", &num);
    30. sem_post(&sem_one);
    31. }
    32. return NULL;
    33. }
    34. void * accu(void * arg)
    35. {
    36. int sum=0, i;
    37. for(i=0; i<5; i++)
    38. {
    39. sem_wait(&sem_one);
    40. sum+=num;
    41. sem_post(&sem_two);
    42. }
    43. printf("Result: %d \n", sum);
    44. return NULL;
    45. }

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

    pthread_join()、pthread_exit()、pthread_detach()三者关系

    ·pthread有两种状态joinable状态(属性)和unjoinable状态,如果线程是joinable状态,当线程函数自己返回退出时或pthread_exit时都不会释放线程所占用堆栈和线程描述符。只有当你调用了pthread_join之后这些资源才会被释放。若是unjoinable状态的线程,这些资源在线程函数退出时或pthread_exit时自动会被释放。
    ·unjoinable属性可以在pthread_create时指定,或在线程创建后在线程中pthread_detach自己, 如:pthread_detach(pthread_self()),将状态改为unjoinable状态,确保资源的释放。或者将线程置为 joinable,然后适时调用pthread_join.
    ·其实简单的说就是在线程函数头加上 pthread_detach(pthread_self())的话,线程状态改变,在函数尾部直接 pthread_exit线程就会自动退出。省去了给线程擦屁股的麻烦。
    ·pthread_exit实际就类似于进程的exit,线程会直接退出, 而其资源不会释放.

    ①pthread_join()即是子线程合入主线程,主线程阻塞等待子线程结束,然后回收子线程资源

    ②pthread_detach()即主线程与子线程分离,子线程结束后,资源自动回收(并不终结线程)

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

    书上的代码有一些问题,做了些修改,放在这里作个参考

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. #include
    8. #include
    9. #define BUF_SIZE 100
    10. #define MAX_CLNT 256
    11. void * handle_clnt(void * arg);
    12. void send_msg(char * msg, int len);
    13. void error_handling(char * msg);
    14. int clnt_cnt=0;
    15. int clnt_socks[MAX_CLNT];
    16. pthread_mutex_t mutx;
    17. int main(int argc, char *argv[])
    18. {
    19. int serv_sock, clnt_sock;
    20. struct sockaddr_in serv_adr, clnt_adr;
    21. int clnt_adr_sz;
    22. pthread_t t_id;
    23. if(argc!=2) {
    24. printf("Usage : %s \n", argv[0]);
    25. exit(1);
    26. }
    27. pthread_mutex_init(&mutx, NULL);
    28. serv_sock=socket(PF_INET, SOCK_STREAM, 0);
    29. memset(&serv_adr, 0, sizeof(serv_adr));
    30. serv_adr.sin_family=AF_INET;
    31. serv_adr.sin_addr.s_addr=htonl(INADDR_ANY);
    32. serv_adr.sin_port=htons(atoi(argv[1]));
    33. if(bind(serv_sock, (struct sockaddr*) &serv_adr, sizeof(serv_adr))==-1)
    34. error_handling("bind() error");
    35. if(listen(serv_sock, 5)==-1)
    36. error_handling("listen() error");
    37. while(1)
    38. {
    39. clnt_adr_sz=sizeof(clnt_adr);
    40. clnt_sock=accept(serv_sock, (struct sockaddr*)&clnt_adr,&clnt_adr_sz);
    41. pthread_mutex_lock(&mutx);
    42. clnt_socks[clnt_cnt++]=clnt_sock;
    43. pthread_mutex_unlock(&mutx);
    44. pthread_create(&t_id, NULL, handle_clnt, (void*)&clnt_sock);
    45. pthread_detach(t_id);
    46. printf("Connected client IP: %s \n", inet_ntoa(clnt_adr.sin_addr));
    47. }
    48. close(serv_sock);
    49. return 0;
    50. }
    51. void * handle_clnt(void * arg)
    52. {
    53. int clnt_sock=*((int*)arg);
    54. int str_len=0, i;
    55. char msg[BUF_SIZE];
    56. while((str_len=read(clnt_sock, msg, sizeof(msg)))!=0)
    57. send_msg(msg, str_len);
    58. pthread_mutex_lock(&mutx);
    59. for(i=0; i// remove disconnected client
    60. {
    61. if(clnt_sock==clnt_socks[i])
    62. {
    63. // while(i++
    64. // clnt_socks[i]=clnt_socks[i+1];
    65. while(i-1) {
    66. clnt_socks[i] = clnt_socks[i + 1];
    67. i++;
    68. }
    69. break;
    70. }
    71. }
    72. clnt_cnt--;
    73. pthread_mutex_unlock(&mutx);
    74. close(clnt_sock);
    75. return NULL;
    76. }
    77. void send_msg(char * msg, int len) // send to all
    78. {
    79. int i;
    80. pthread_mutex_lock(&mutx);
    81. for(i=0; i
    82. write(clnt_socks[i], msg, len);
    83. pthread_mutex_unlock(&mutx);
    84. }
    85. void error_handling(char * msg)
    86. {
    87. fputs(msg, stderr);
    88. fputc('\n', stderr);
    89. exit(1);
    90. }

  • 相关阅读:
    家庭收支记账管理系统(Java+Web+MySQL)
    电力电子转战数字IC——IC笔试面试Verilog合集(持续更新)
    绕任意轴旋转矩阵推导
    HTTP头部信息解释分析(详细整理)(转载)
    apt remove purge的区别 删除包的同时删除配置文件
    day26 java lambda
    scipy.optimize.minimize函数介绍
    给力心理平台项目开发介绍
    案例篇:Python爬虫的多重领域使用
    5G:HARQ协议
  • 原文地址:https://blog.csdn.net/weixin_45767431/article/details/127841908