多线程与socket编程
一、pthread基本概念
1. 创建线程
int pthread_create(pthread_t *restrict ptid,
const pthread_attr_t *restrict attr,
void *(*start_routine)(void*), void *restrict arg);
ptid是一个pthread_t *类型的指针,pthread_t是类似pid_t的数据结构,表示线程ID;attr指明线程创建属性,如果为NULL就使用系统默认属性;start_routine是线程的主函数,它的参数是void *类型的指针,返回值也是void *类型的指针;arg是线程创建者传递给新建线程的参数,也就是start_routine的参数。
注意,线程创建者和新建线程之间没有fork()调用那样的父子关系,它们是对等关系。调用pthread_create()创建线程后,线程创建者和新建线程哪个先运行是不确定的,特别是在多处理机器上。
(1). pthread_create()中的attr参数是一个结构指针,结构中的元素分别对应着新线程的运行属性,主要包括以下几项:
__detachstate,表示新线程是否与进程中其他线程脱离同步,如果置位则新线程不能用pthread_join()来同步,且在退出时自行释放所占用的资源。缺省为PTHREAD_CREATE_JOINABLE状态。这个属性也可以在线程创建并运行以后用pthread_detach()来设置,而一旦设置为PTHREAD_CREATE_DETACH状态(不论是创建时设置还是运行时设置)则不能再恢复到PTHREAD_CREATE_JOINABLE状态。
__schedpolicy,表示新线程的调度策略,主要包括SCHED_OTHER(正常、非实时)、SCHED_RR(实时、轮转法)和SCHED_FIFO(实时、先入先出)三种,缺省为SCHED_OTHER,后两种调度策略仅对超级用户有效。运行时可以用过pthread_setschedparam()来改变。
__schedparam,一个struct sched_param结构,目前仅有一个sched_priority整型变量表示线程的运行优先级。这个参数仅当调度策略为实时(即SCHED_RR或SCHED_FIFO)时才有效,并可以在运行时通过pthread_setschedparam()函数来改变,缺省为0。
__inheritsched,有两种值可供选择:PTHREAD_EXPLICIT_SCHED和PTHREAD_INHERIT_SCHED,前者表示新线程使用显式指定调度策略和调度参数(即attr中的值),而后者表示继承调用者线程的值。缺省为PTHREAD_EXPLICIT_SCHED。
__scope,表示线程间竞争CPU的范围,也就是说线程优先级的有效范围。POSIX的标准中定义了两个值:PTHREAD_SCOPE_SYSTEM和PTHREAD_SCOPE_PROCESS,前者表示与系统中所有线程一起竞争CPU时间,后者表示仅与同进程中的线程竞争CPU。目前LinuxThreads仅实现了PTHREAD_SCOPE_SYSTEM一值。
pthread_attr_t结构中还有一些值,但不使用pthread_create()来设置。
为了设置这些属性,POSIX定义了一系列属性设置函数,包括pthread_attr_init()、pthread_attr_destroy()和与各个属性相关的pthread_attr_get---/pthread_attr_set---函数。
2.终止线程
void pthread_exit(void *value_ptr);
线程调用pthread_exit()结束自己,参数value_ptr被调用pthread_join的线程使用。
3.pthread_self()得到线程ID
4.pthread_equal()比较线程ID,线程ID的大小没有意义。
3.取消线程
int pthread_cancel(pthread_t thread);
向线程thread发送取消请求,默认情况下线程thread自己调用pthread_exit(PTHREAD_CANCELED),可以在创建线程时通过attr改变默认行为。pthread_cancel并不阻塞调用者,总是立即返回。
4.连接线程 阻塞
int pthread_join(pthread_t thread, void **value_ptr);
等待线程thread结束,并设置*value_ptr为thread的返回值。pthread_join阻塞调用者,一直到线程thread结束为止。
线程终止有一下几种方法:1.从主函数返回,2.自己调用pthread_exit(),3.其他线程调用pthread_cancel(),4.线程所属的进程中任何线程调用exit()导致所有线程结束。
5.分离线程
int pthread_detach(pthread_t thread);
分离线程的语意是,线程thread结束后系统可以回收它的私有数据。
6.进程与线程创建的区别(linux环境):
我们知道,Linux的线程实现是在核外进行的,核内提供的是创建进程的接口do_fork()。内核提供了两个系统调用__clone()和fork(),最终都用不同的参数调用do_fork()核内API。当然,要想实现线程,没有核心对多进程(其实是轻量级进程)共享数据段的支持是不行的,因此,do_fork()提供了很多参数,包括CLONE_VM(共享内存空间)、CLONE_FS(共享文件系统信息)、CLONE_FILES(共享文件描述符表)、CLONE_SIGHAND(共享信号句柄表)和CLONE_PID(共享进程ID,仅对核内进程,即0号进程有效)。当使用fork系统调用时,内核调用do_fork()不使用任何共享属性,进程拥有独立的运行环境,而使用pthread_create()来创建线程时,则最终设置了所有这些属性来调用__clone(),而这些参数又全部传给核内的do_fork(),从而创建的"进程"拥有共享的运行环境,只有栈是独立的,由__clone()传入。
二、socket与线程并用
使用socket的Linux上的C语言helloworld多线程服务器和客户端测试程序 -- 转载
服务器端程序的编译
gcc -o multi_thread_server multi_thread_server.c -lpthread
客户端程序的编译
gcc -o multi_thread_client multi_thread_client.c -lpthread
服务器程序和客户端程应当分别运行在2台计算机上.
服务器端程序的运行,在一个计算机的终端执行
./multi_thread_server
客户端程序的运行,在另一个计算机的终端中执行
./multi_thread_client 运行服务器程序的计算机的IP地址
在实际编程和测试中,可以用2个终端代替2个计算机,这样就可以在一台计算机上测试网络程序,
服务器端程序的运行,在一个终端执行
./multi_thread_server
客户端程序的运行,在另一个终端中执行
./multi_thread_client 127.0.0.1
说明: 任何计算机都可以通过127.0.0.1访问自己. 也可以用计算机的实际IP地址代替127.0.0.1
///////////////////////////////////////////////////////////////////////////////////
//multi_thread_server.c
///////////////////////////////////////////////////////////////////////////////////
//本文件是多线程并发服务器的代码
#include <netinet/in.h> // for sockaddr_in
#include <sys/types.h> // for socket
#include <sys/socket.h> // for socket
#include <stdio.h> // for printf
#include <stdlib.h> // for exit
#include <string.h> // for bzero
#include <pthread.h>
#include <sys/errno.h> // for errno
#include <unistd.h> //define close
#define HELLO_WORLD_SERVER_PORT 6666
#define LENGTH_OF_LISTEN_QUEUE 20
#define BUFFER_SIZE 1024
#define THREAD_MAX 5
void * talk_to_client(void *data)
{
//void* 数据可以转换为任意类型数据
int new_server_socket = (int)data;
char buffer[BUFFER_SIZE];
bzero(buffer, BUFFER_SIZE);
strcpy(buffer,"Hello,World! 从服务器来!");
strcat(buffer,"\n"); //C语言字符串连接
//发送buffer中的字符串到new_server_socket,实际是给客户端
send(new_server_socket,buffer,BUFFER_SIZE,0);
bzero(buffer,BUFFER_SIZE);
//接收客户端发送来的信息到buffer中
if((int length = recv(new_server_socket,buffer,BUFFER_SIZE,0)) < 0)
{
printf("Server Recieve Data Failed!\n");
exit(1);
}
printf("\nSocket Num: %d \t %s",new_server_socket, buffer);
//关闭与客户端的连接
close(new_server_socket);
pthread_exit(NULL);
}
int main(int argc, char **argv)
{
//设置一个socket地址结构server_addr,代表服务器internet地址, 端口
struct sockaddr_in server_addr;
bzero(&server_addr,sizeof(server_addr)); //把一段内存区的内容全部设置为0
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_port = htons(HELLO_WORLD_SERVER_PORT);
//创建用于internet的流协议(TCP)socket,用server_socket代表服务器socket
if ((int server_socket = socket(AF_INET,SOCK_STREAM,0) == -1)
{
printf("Create Socket Failed!");
exit(1);
}
//把socket和socket地址结构联系起来
if( bind(server_socket,(struct sockaddr*)&server_addr,sizeof(server_addr)))
{
printf("Server Bind Port : %d Failed!", HELLO_WORLD_SERVER_PORT);
exit(1);
}
//server_socket用于监听
if ( listen(server_socket, LENGTH_OF_LISTEN_QUEUE) )
{
printf("Server Listen Failed!");
exit(1);
}
int i;
while(1) //服务器端要一直运行
{
//定义客户端的socket地址结构client_addr
struct sockaddr_in client_addr;
socklen_t length = sizeof(client_addr);
//接受一个到server_socket代表的socket的一个连接
//如果没有连接请求,就等待到有连接请求--这是accept函数的特性
//accept函数返回一个新的socket,这个socket(new_server_socket)用于同连接到的客户的通信
//new_server_socket代表了服务器和客户端之间的一个通信通道
//accept函数把连接到的客户端信息填写到客户端的socket地址结构client_addr中
if ((int new_server_socket = accept(server_socket,(struct sockaddr*)&client_addr,&length)) == -1)
{
printf("Server Accept Failed!\n");
break;
}
pthread_t child_thread;
pthread_attr_t child_thread_attr;
pthread_attr_init(&child_thread_attr);
pthread_attr_setdetachstate(&child_thread_attr,PTHREAD_CREATE_DETACHED);
if( pthread_create(&child_thread,&child_thread_attr,talk_to_client, (void *)new_server_socket) < 0 )
printf("pthread_create Failed : %s\n",strerror(errno));
}
//关闭监听用的socket
close(server_socket);
return 0;
}
///////////////////////////////////////////////////////////////////////////////////
// multi_thread_client.c
///////////////////////////////////////////////////////////////////////////////////
//本文件是客户机多线程多次重复与服务交互的代码
#include <netinet/in.h> // for sockaddr_in
#include <sys/types.h> // for socket
#include <sys/socket.h> // for socket
#include <stdio.h> // for printf
#include <stdlib.h> // for exit
#include <string.h> // for bzero
#include <pthread.h>
#include <sys/errno.h> // for errno
#include <unistd.h> //define close
#define HELLO_WORLD_SERVER_PORT 6666
#define BUFFER_SIZE 1024
char * server_IP = NULL;
void * talk_to_server(void * thread_num)
{
//设置一个socket地址结构client_addr,代表客户机internet地址, 端口
struct sockaddr_in client_addr;
bzero(&client_addr,sizeof(client_addr)); //把一段内存区的内容全部设置为0
client_addr.sin_family = AF_INET; //internet协议族
client_addr.sin_addr.s_addr = htons(INADDR_ANY);//INADDR_ANY表示自动获取本机地址
client_addr.sin_port = htons(0); //0表示让系统自动分配一个空闲端口
//创建用于internet的流协议(TCP)socket,用client_socket代表客户机socket
if ((int client_socket = socket(AF_INET,SOCK_STREAM,0)) == -1)
{
printf("Create Socket Failed!\n");
exit(1);
}
//把客户机的socket和客户机的socket地址结构联系起来
if( bind(client_socket,(struct sockaddr*)&client_addr,sizeof(client_addr)))
{
printf("Client Bind Port Failed!\n");
exit(1);
}
//设置一个socket地址结构server_addr,代表服务器的internet地址, 端口
struct sockaddr_in server_addr;
bzero(&server_addr,sizeof(server_addr));
server_addr.sin_family = AF_INET;
//inet_aton比起inet_addr来,可以通过返回值检测IP是否有效
if(inet_aton(server_IP,&server_addr.sin_addr) == 0) //服务器的IP地址来自程序的参数
{
printf("Server IP Address Error!\n");
exit(1);
}
server_addr.sin_port = htons(HELLO_WORLD_SERVER_PORT);
socklen_t server_addr_length = sizeof(server_addr);
//向服务器发起连接,连接成功后client_socket代表了客户机和服务器的一个socket连接
if(connect(client_socket,(struct sockaddr*)&server_addr, server_addr_length) < 0)
{
printf("Can Not Connect To %s!\n",server_IP);
exit(1);
}
char buffer[BUFFER_SIZE];
bzero(buffer,BUFFER_SIZE);
//从服务器接收数据到buffer中
if ((int length = recv(client_socket,buffer,BUFFER_SIZE,0)) < 0)
{
printf("Recieve Data From Server %s Failed!\n", server_IP);
exit(1);
}
printf("From Server %s :\t%s",server_IP,buffer);
bzero(buffer,BUFFER_SIZE);
sprintf(buffer,"Hello, World! From Client Thread NUM :\t%d\n",(int)thread_num);
//向服务器发送buffer中的数据
send(client_socket,buffer,BUFFER_SIZE,0);
//关闭socket
close(client_socket);
pthread_exit(NULL);
}
int main(int argc, char **argv)
{
if (argc != 2)
{
printf("Usage: ./%s ServerIPAddress\n",argv[0]);
exit(1);
}
server_IP = argv[1];
pthread_t child_thread;
pthread_attr_t child_thread_attr;
pthread_attr_init(&child_thread_attr);
pthread_attr_setdetachstate(&child_thread_attr,PTHREAD_CREATE_DETACHED);
int i=0;
for(i=0; i<10000; i++)
{
if( pthread_create(&child_thread,&child_thread_attr,talk_to_server, (void *)i) < 0 )
printf("pthread_create Failed : %s\n",strerror(errno));
}
return 0;
}
三、并发编程概念
1、并发(Concurrency)编程的两种途径:并行编程和分布式编程。
2、并行编程和分布式编程通常会杂糅在一起,互相交叉;
3、MPP(Massively Parallel Processor,大规模并行处理器),快速和强力的结合,使得大规模并行计算成为可能,为某些领域解决问题提供可行的方案;
4、PRAM(Parrllel Random Access Machine,并行随即访问计算机),是并行编程中最简单的理论模型,但它的能力可不小。
5、最流行的分布式编程是C/S模式的Web服务器和客户端编程,也是最容易理解的;
6、多Agent分布式模型是一种对等模型,它的每一个Agent都是自治的,并且在某种程度上是出于同等地位的。
7、并发编程需要解决的问题包括分解、通行和同步(DCS),这在现代编程中的许多领域都会遇到;
8、分解(Decomposition),是指对问题进行分解成许多子问题,使各个子问题之间可以并行计算或分布在不同的计算机器上计算,这些子问题组成并发执行的元素;
9、通信(Communication)),是指各个元素之间的交流,诸如所需的资源、计算的进度、计算过程中遇到的问题等,通常情况下,并行计算中的通信通过系统总线或共享存储器来进行,而分布式计算通常使用网络来进行通信;
10、同步(Synchornization),指各个并发执行的元素之间对资源的获得、执行的顺序、解决计算过程中所遇到的问题等进行协调的过程。
11、软件并发的层次包括指令级(Instruction level)并发,例程级(Routine level)并发,对象级(Object level)并发和应用程序级(Application level)并发四种;
12、指令级并发,通常由编译器和系统设计实现,《计算机体系结构——量化研究方法》(《Computer Architecture: A Quantitative Approach》)中讲到多种指令并行流水线(Pipeline)的方法,包括托马索罗方法(Tomasulo‘s Approach),循环展开方法(the Loop Unrolling Approach)和超长指令字方法(The VLIW(Very long instructure word) Approach)等方法;
13、例程级并发,即将不同的工作设计成一个个函数,再将函数分配给不同的线程,从而得到并发执行(可以是超线程或多处理器等);
14、对象级并发,将每个对象分配给不同的进程或线程,从而得到并发执行;
15、CORBA(Common Object Request Broker Architecture, 公共对象请求代理体系结构)是将每个对象分配给网络上的不同计算机的标准;
16、应用程序级并发,是指两个或多个应用程序一起协同解决一个问题,代码的重用使得应用程序得依相互协调而并发执行;
17、WBS(Work Breakdown Structure,工作分解结构)是项目管理中的一个模型,而上述的不同层次的并发的划分在一定程度上是根据WBS来进行的。
18、并发程序需要解决的问题包括数据竞争、无限延迟、死锁等协调问题;
19、数据竞争发生在共享数据时,多个任务同时对数据进行读写操作;
20、无限延迟发生在某些任务的调度执行需要等待某个事件或某些条件的发生,而该事件和条件迟迟不能到来,以致这些任务的发生遥遥无期;
21、两个或多个任务之间循环地互为执行的条件,即A等待B的条件,B等待C的条件,C又在等待A的条件,从而在A、B和C之间造成死锁。
22通信问题,在并行算机和分布式计算中常常会遇到通信成为瓶颈的问题,对这些问题的解决,需要协调好通信量的问题,选择合适的并发编程系统。
23、并行或分布式计算过度而造成性能的下降,由于诸如同步、通信等等的问题的存在,并非是处理器越多越好,一般处理器的数量有一个最佳值,在系统实现的过程中应该找出该最佳值,这可以通过实验得到,另外,理论上的最佳值和实际实现的最佳值并不完全一致。
24、可以使用UML图在程序员中交流。