Linux并发处理方式——多线程pthread
前言:
简单来说,线程是进程的一条执行路径,在Unix系统下也叫做轻量级进程。所有的线程都在同一个进程下运行的,所以所有线程都可以共享进程里的资源,同时每个线程也有属于自身的栈空间、寄存器环境等等,每个线程执行着自己的任务。这时候又不得不考虑进程里面的资源分配问题了,如何解决死锁问题;父进程与子进程之间的关系,如何关闭子线程。
一、相关函数
1、父、子线程
主线程和子线程的默认关系是:无论子线程执行完毕与否,一旦主线程执行完毕退出,所有子线程执行都会终止。这时整个进程结束或僵死,部分线程保持一种终止执行但还未销毁的状态,而进程必须在其所有线程销毁后毁,这时进程处于僵死状态。线程函数执行完毕退出,或以其他非常方式终止,线程进入终止态,但是为线程分配的系统资源不一定释放,可能在系统重启之前,一直都不能释放,终止态的线程,仍旧作为一个线程实体存在于操作系统中,什么时候销毁,取决于线程属性。在这种情况下,主线程和子线程通常定义以下两种关系:
(1) 可会合(joinable):这种关系下,主线程需要明确执行等待操作,在子线程结束后,主线程的等待操作执行完毕,子线程和主线程会合,这时主线程继续执行等待操作之后的下一步操作。主线程必须会合可会合的子线程。在主线程的线程函数内部调用子线程对象的wait函数实现,即使子线程能够在主线程之前执行完毕,进入终止态,也必须执行会合操作,否则,系统永远不会主动销毁线程,分配给该线程的系统资源也永远不会释放。
(2)相分离(detached):表示子线程无需和主线程会合,也就是相分离的,这种情况下,子线程一旦进入终止状态,这种方式常用在线程数较多的情况下,有时让主线程逐个等待子线程结束,或者让主线程安排每个子线程结束的等待顺序,是很困难或不可能的,所以在并发子线程较多的情况下,这种方式也会经常使用。
2、死锁问题
死锁产生的4个必要条件
(1)互斥:某种资源一次只允许一个进程访问,即该资源一旦分配给某个进程,其他进程就不能再访问,直到该进程访问结束。
(2)占有且等待:一个进程本身占有资源(一种或多种),同时还有资源未得到满足,正在等待其他进程释放该资源。
(3)不可抢占:别人已经占有了某项资源,你不能因为自己也需要该资源,就去把别人的资源抢过来。
(4)循环等待:存在一个进程链,使得每个进程都占有下一个进程所需的至少一种资源。
当以上四个条件均满足,必然会造成死锁,发生死锁的进程无法进行下去,它们所持有的资源也无法释放。这样会导致CPU的吞吐量下降。所以死锁情况是会浪费系统资源和影响计算机的使用性能的。
互斥条件是非共享资源所必须的,不仅不能改变,还应加以保证,所以,主要是破坏产生死锁的其他三个条件。
a、破坏“占有且等待”条件
方法1:所有的进程在开始运行之前,必须一次性地申请其在整个运行过程中所需要的全部资源。
优点:简单易实施且安全。缺点:因为某项资源不满足,进程无法启动,而其他已经满足了的资源也不会得到利用,严重降低了资源的利用率,造成资源浪费。使进程经常发生饥饿现象。
方法2:该方法是对第一种方法的改进,允许进程只获得运行初期需要的资源,便开始运行,在运行过程中逐步释放掉分配到的已经使用完毕的资源,然后再去请求新的资源。这样的话,资源的利用率会得到提高,也会减少进程的饥饿问题。
b、破坏“不可抢占”条件
当一个已经持有了一些资源的进程在提出新的资源请求没有得到满足时,它必须释放已经保持的所有资源,待以后需要使用的时候再重新申请。这就意味着进程已占有的资源会被短暂地释放或者说是被抢占了。该种方法实现起来比较复杂,且代价也比较大。释放已经保持的资源很有可能会导致进程之前的工作实效等,反复的申请和释放资源会导致进程的执行被无限的推迟,这不仅会延长进程的周转周期,还会影响系统的吞吐量。
c、破坏“循环等待”条件
可以通过定义资源类型的线性顺序来预防,可将每个资源编号,当一个进程占有编号为i的资源时,那么它下一次申请资源只能申请编号大于i的资源。
二、函数介绍
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void
*arg);
第一个参数thread是一个pthread_t类型的指针,他用来返回该线程的线ID。每个线程都能够通过pthread_self()来获取自己的线程ID(pthread_t类型)。
第二个参数是线程的属性。
第三个参数start_routine是一个函数指针,它指向的函数原型void*func(void *),这是所创建的子线程要执行的任务(函数)。
第四个参数arg就是传给了所调用的函数的参数,如果有多个参数需要传递给子线程则需要封装到一个结构体里传进去。
三、具体代码
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <getopt.h>
#include <pthread.h>
#include <ctype.h>
void *thread_worker (void *data);
//#define PORT 9988
void printf_usage(char *progname)
{
printf("%s usage: \n",progname);
printf("-p(--port): sepcify server port \n");
printf("-h(--help): print this help information \n");
}
int main(int argc, char **argv)
{
int socket_fd = -1, client_fd = -1;
struct sockaddr_in serveraddr, clientaddr;
socklen_t clientaddr_len = sizeof(struct sockaddr);
int port;
int ch;
int index;
int on = 1;
pthread_t tid;
pthread_attr_t thread_attr;
struct option opts[] = {
{"port", required_argument, NULL, 'p'},
{"help", no_argument, NULL, 'h'},
{NULL, 0, NULL, 0}
};
while((ch = getopt_long(argc, argv, "p:h", opts, NULL)) != -1)
{
switch(ch)
{
case 'p':
port = atoi(optarg);
break;
case 'h':
printf_usage(argv[0]);
return 0;
}
}
while(argc < 2)
{
printf("please input %s [port] \n",argv[0]);
return -1;
}
socket_fd = socket(AF_INET, SOCK_STREAM, 0);
if(socket_fd < 0)
{
printf("create socket failure : %s \n", strerror(errno));
return -2;
}
setsockopt(socket_fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
memset(&serveraddr, 0, sizeof(serveraddr));
//port = atoi(argv[1]);
serveraddr.sin_family = AF_INET;
serveraddr.sin_port = htons(port); //操作系统的字节序为小端,所以需要转换成网络大端字节序
serveraddr.sin_addr.s_addr = htonl(INADDR_ANY); //ip地址为4个字节,用htonl;INADDR_ANY(实际上是0)可以自动选择服务器的ip,使用有线网卡就用有线ip,使用无线网卡就用无线ip。
if(bind(socket_fd, (struct sockaddr *)&serveraddr, sizeof(serveraddr)) < 0)
{
printf("create server failure : %s \n", strerror(errno));
return -3;
}
printf("socket [%d] bind on port[%d] for all IP address succesfully !", socket_fd, port);
listen(socket_fd, 10); //监听socket,等待客户端连接,10为正在连接服务端的最大数,超过就不能连入服务器。
while(1)
{
printf("\n Start waiting and accept new client connect...\n");
client_fd = accept(socket_fd, (struct sockaddr *)&clientaddr, &clientaddr_len); //一般来说不关心addr的长度大小,这个数据不重要;地址为ipv4类型,需要保持一致;内核返回的值为新的socket描述符,用来服务某个接入的客户端。
if(client_fd < 0)
{
printf("accept new socket failture : %s\n", strerror(errno));
continue;
}
printf("accept new client[%d] socket[%s:%d]\n",client_fd, inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port));//inet_ntoa函数为整型转字符型,ntohs函数为大端转小端。
if(pthread_attr_init(&thread_attr))
{
printf("pthread init failture : %s \n", strerror(errno));
pthread_attr_destroy(&thread_attr);
continue;
}
if(pthread_attr_setstacksize(&thread_attr, 120*1024))
{
printf("pthread set stack size failure : %s \n",strerror(errno));
pthread_attr_destroy(&thread_attr);
continue;
}
if(pthread_attr_setdetachstate(&thread_attr, PTHREAD_CREATE_DETACHED))
{
printf("pthread set detachstate failure : %s \n", strerror(errno));
pthread_attr_destroy(&thread_attr);
continue;
}
if(pthread_create(&tid, &thread_attr, thread_worker, (void *)client_fd))
{
printf("Create thread failure: %s\n", strerror(errno));
pthread_attr_destroy(&thread_attr);
continue;
}
}
close(socket_fd);
return 0;
}
void *thread_worker (void *data)
{
int rv = -1, rv1 = -1;
int client_fd;
char buf[8192];
int i;
client_fd = (int)data;
while(1)
{
memset(buf, 0, sizeof(buf));
rv = read(client_fd, buf, sizeof(buf));
if(rv < 0)
{
printf("read data from client [%d] failure : %s\n", client_fd, strerror(errno));
close(client_fd);
pthread_exit(NULL);
}
else if(rv == 0)
{
printf("client [%d] disconnected \n", client_fd);
close(client_fd);
pthread_exit(NULL);
}
printf("pthread id[%lu] read %d bytes data from client[%d] and the data is : %s \n", pthread_self(), rv , client_fd, buf);
for(i=0; i<rv; i++)
{
buf[i]=toupper(buf[i]);
}
rv1 = write(client_fd, buf, rv);
if(rv1 < 0)
{
printf("write data back to client[%d] failure: %s \n", client_fd, strerror(errno));
close(client_fd);
pthread_exit(NULL);
}
sleep(1);
}
}
运行效果:
四、总结
多线程相对比于多进程而言,线程减少了系统内存的占用,更加合理地利用cpu资源,线程与线程之间的通信相对简单一些。在这里主要了解了线程创建,线程的资源设置,死锁问题,线程之间的关系。