0%

thread

Thread

“线程”的起源

“线程”一词于 1967 年左右被首次提出,是计算机硬件和软件发展过程中诞生的产物。

一台计算机所能利用的资源总是有限的,比如 CPU 在 1 秒钟之内最多执行 1 亿条指令,计算机一共有 1GB 的内存空间等等。因此,“如何提高计算机资源的利用率”是人们一直思考的问题,这个问题也一直带动着计算机硬件和软件的发展。

计算机诞生初期,任何安装任何操作系统和软件,只能运行机器指令,完成一些简单的数学运算。受到当时价格因素的制约,计算机并不普及,拥有者主要是政府、大型机构和公司,一台计算机往往由多个用户共同使用。计算机由专人负责操控,如果有用户想让计算机运行一段指令,必须先将指令输入到打孔卡(一种存储设备)中,然后交给计算机管理员,由计算机管理员负责将指令输入到计算机中执行。

随着对计算机资源利用率的要求不断提升,人们逐渐发现,计算机资源的利用率受管理员的影响非常大。例如,计算机每执行完一个任务,都要等待管理员输入下一个任务,期间很多硬件资源(比如 CPU、某些输入输出设备)都处于空闲状态。

为此,人们设计出了批处理操作系统,由它代替计算机管理员完成任务的切换工作。当计算机执行完某一任务时,批处理系统会自动将下一个要执行的任务输入到计算机中,缩减了任务切换所花费的时间,提高了计算机资源的利用率。

渐渐地人们又发现,批处理系统操控计算机执行的过程中,计算机的 CPU 资源仍经常处于空闲状态。举个例子,当执行中的程序进行 I/O 操作时,CPU 只能等待其 I/O 操作完成后继续工作,这段时间内 CPU 就处于空闲状态。

在批处理系统(又称单道批处理操作系统)的基础上,人们又设计出了功能更强大的多道批处理操作系统。和先前的系统相比,多道批处理系统主要有以下两点优势:

  • 它将计算机的内存分成很多区域,每个区域都可以存储一个程序;
  • 当执行的程序执行 I/O 操作时,操作系统会将 CPU 资源分配给其它等待执行的程序。

也就是说,多道批处理操作系统可以“同时”执行多个程序,这样的操作系统又称多任务操作系统。为了使多任务系统更高效地完成计算机资源的分配和回收,便于管理各个程序的执行过程,人们提出了“进程”的概念。

所谓进程,指的就是正在执行的应用程序。多任务操作系统可以控制各个进程的执行状态,例如终止某个正在执行的进程,启动某个暂停执行的进程等。操作系统负责为每个进程分配独立的内存空间和其它所需资源(例如 I/O 设备、文件等),进程执行完毕后,操作系统会将进程占用的资源全部回收。

早期的多任务操作系统,以进程为单位管理各个程序的运行以及计算机资源的分配和回收,进一步提高了计算机资源的利用率。但随着计算机硬、软件的发展,人们发现还可以做进一步优化,例如:

  • 操作系统将 CPU 资源从一个进程分配给另一个进程时,开销较大;
  • 各个进程占用的内存空间是相互独立的,大大增加了进程间通信的实现难度;
  • 一个进程可能会执行多个任务,当某个任务因 I/O 操作暂停执行时,其他任务将无法执行。

在计算机软、硬件快速发展,人们计算机运行效率的要求越来越高的大背景下,“线程”应运而生。

什么是线程

我们知道,一个进程指的是一个正在执行的应用程序。线程对应的英文名称为“thread”,它的功能是执行应用程序中的某个具体任务,比如一段程序、一个函数等。

线程和进程之间的关系,类似于工厂和工人之间的关系,进程好比是工厂,线程就如同工厂中的工人。一个工厂可以容纳多个工人,工厂负责为所有工人提供必要的资源(电力、产品原料、食堂、厕所等),所有工人共享这些资源,每个工人负责完成一项具体的任务,他们相互配合,共同保证整个工厂的平稳运行。

每个进程执行前,操作系统都会为其分配所需的资源,包括要执行的程序代码、数据、内存空间、文件资源等。一个进程至少包含 1 个线程,可以包含多个线程,所有线程共享进程的资源,各个线程也可以拥有属于自己的私有资源。

进程仅负责为各个线程提供所需的资源,真正执行任务的是线程,而不是进程。

下图描述了进程和线程之间的关系:

如图所示,所有线程共享的进程资源有:

  • 代码:即应用程序的代码;
  • 数据:包括全局变量、函数内的静态变量、堆空间的数据等;
  • 进程空间:操作系统分配给进程的内存空间;
  • 打开的文件:各个线程打开的文件资源,也可以为所有线程所共享,例如线程 A 打开的文件允许线程 B 进行读写操作。

各个线程也可以拥有自己的私有资源,包括寄存器中存储的数据、线程执行所需的局部变量(函数参数)等。

什么是多线程

所谓多线程,即一个进程中拥有多(≥2)个线程,线程之间相互协作、共同执行一个应用程序。

我们通常将以“多线程”方式编写的程序称为“多线程程序”,将编写多线程程序的过程称为“多线程编程”,将拥有多个线程的进程称为“多线程进程”。

当进程中仅包含 1 个执行程序指令的线程时,该线程又称“主线程”,这样的进程称为“单线程进程”。

如今,很多应用程序(软件)都是多线程程序,例如 QQ 具备同时和多人聊天的能力、迅雷具备同时下载多个资源的能力、很多杀毒软件可以同时开启杀毒、清理垃圾、电脑加速等功能。

C++ 多线程

多线程是多任务处理的一种特殊形式,多任务处理允许让电脑同时运行两个或两个以上的程序。一般情况下,两种类型的多任务处理:基于进程和基于线程

  • 基于进程的多任务处理是程序的并发执行。
  • 基于线程的多任务处理是同一程序的片段的并发执行。

多线程程序包含可以同时运行的两个或多个部分。这样的程序中的每个部分称为一个线程,每个线程定义了一个单独的执行路径。

Windows线程的创建和使用

std::thread

C++ 11 之后添加了新的标准线程库 std::threadstd::thread 头文件中声明,因此使用 std::thread 时需要包含 头文件。

之前一些编译器使用 C++ 11 的编译参数是 -std=c++11:

g++ -std=c++11 test.cpp 

std::thread 默认构造函数,创建一个空的 std::thread 执行对象。

#include<thread>
std::thread thread_object(callable)

一个可调用对象可以是以下三个中的任何一个:

  • 函数指针
  • 函数对象
  • lambda 表达式

定义 callable 后,将其传递给 std::thread 构造函数 thread_object

// 演示多线程的CPP程序
// 使用三个不同的可调用对象
#include <iostream>
#include <thread>
using namespace std;
 
// 一个虚拟函数
void foo(int Z)
{
    for (int i = 0; i < Z; i++) {
        cout << "线程使用函数指针作为可调用参数\n";
    }
}
 
// 可调用对象
class thread_obj {
public:
    void operator()(int x)
    {
        for (int i = 0; i < x; i++)
            cout << "线程使用函数对象作为可调用参数\n";
    }
};
 
int main()
{
    cout << "线程 1 、2 、3 "
         "独立运行" << endl;
 
    // 函数指针
    thread th1(foo, 3);
 
    // 函数对象
    thread th2(thread_obj(), 3);
 
    // 定义 Lambda 表达式
    auto f = [](int x) {
        for (int i = 0; i < x; i++)
            cout << "线程使用 lambda 表达式作为可调用参数\n";
    };
 
    // 线程通过使用 lambda 表达式作为可调用的参数
    thread th3(f, 3);
 
    // 等待线程完成
    
    // 等待线程 t1 完成
    th1.join();
 
    // 等待线程 t2 完成
    th2.join();
 
    // 等待线程 t3 完成
    th3.join();
 
    return 0;
}
线程 1 、2 、3 独立运行
线程使用函数指针作为可调用参数
线程使用函数指针作为可调用参数
线程使用函数指针作为可调用参数
线程使用 lambda 表达式作为可调用参数
线程使用 lambda 表达式作为可调用参数
线程使用 lambda 表达式作为可调用参数
线程使用函数对象作为可调用参数
线程使用函数对象作为可调用参数
线程使用函数对象作为可调用参数

--------------------------------
Process exited after 0.02723 seconds with return value 0
请按任意键继续. . .




C++11 标准中,<thread>头文件提供了 thread 类(位于 std 命令空间中),专门用来完成线程的创建和使用。

创建线程

一个线程可以用 thread 类的对象来表示,thread类中重载了多种构造函数,最常用的有以下两个:

//1、Fn 表示线程要执行的函数,args 表示向 Fn 传递的多个参数,此构造函数支持泛型
template <class Fn, class... Args>
explicit thread (Fn&& fn, Args&&... args);
//2、移动构造函数
thread (thread&& x) noexcept;
explicit关键字用来修饰类的构造函数,被修饰的构造函数的类,不能发生相应的隐式类型转换,只能以显示的方式进行类型转换。
explicit使用注意事项:

      explicit 关键字只能用于类内部的构造函数声明上。
      explicit 关键字作用于单个参数的构造函数。
跟它相对应的另一个关键字是implicit, 意思是隐藏的,类构造函数默认情况下即声明为implicit(隐式).


 explicit关键字只需用于类内的单参数构造函数前面。由于无参数的构造函数和多参数的构造函数总是显示调用,这种情况在构造函数前加explicit无意义。

 google的c++规范中提到explicit的优点是可以避免不合时宜的类型变换,缺点无。所以google约定所有单参数的构造函数都必须是显示的,只有极少数情况下拷贝构造函数可以不声明称explicit。例如作为其他类的透明包装器的类。
  effective c++中说:被声明为explicit的构造函数通常比其non-explicit兄弟更受欢迎。因为它们禁止编译器执行非预期(往往也不被期望)的类型转换。除非我有一个好理由允许构造函数被用于隐式类型转换,否则我会把它声明为explicit,鼓励大家遵循相同的政策。

thread 类只提供了移动构造函数,未提供拷贝构造函数。这意味着,我们不能直接将一个事先定义好的 thread 对象赋值给另一个 thread 对象,但可以将临时的(匿名的)thread 对象赋值给另一个 thread 对象。

#include <iostream>
#include <thread>
using namespace std;
void threadFun1(int n) {
    cout << "---thread1 running\n";
    cout << "n=" << n << endl;
}
void threadFun2(const char * url) {
    cout << "---thread2 running\n";
    cout << "url=" << url << endl;
}
int main() {
    //调用第 1 种构造函数
    thread thread1(threadFun1,10);
    //调用移动构造函数
    thread thread2 = std::thread(threadFun2,"http://google.com");
    //阻塞主线程,等待 thread1 线程执行完毕
    thread1.join();
    //阻塞主线程,等待 thread2 线程执行完毕
    thread2.join();
    return 0;
}
---thread1 running
n=10
---thread2 running
url=http://google.com

--------------------------------
Process exited after 0.02249 seconds with return value 0
请按任意键继续. . .


---thread1 running
n=---thread2 running
url=http://google.com10


--------------------------------
Process exited after 0.02315 seconds with return value 0
请按任意键继续. . .

程序执行结果为(不唯一):为什么?

程序中,分别调用两种构造函数创建了两个线程,它们分别执行 threadFun1() 和 threadFun2() 函数。我们在主线程(main() 函数)中调用了 thread 类提供的 join() 成员函数,以 thread1.join() 为例,它的功能是阻塞主线程,直至 thread1 线程执行完毕后,主线程才能继续执行。

线程的使用

除了 join() 成员函数外,thread 类还提供有很多实用的成员函数,表 1 给大家列出了几个最常用的函数:

成员函数 功 能
get_id() 获取当前 thread 对象的线程 ID。
joinable() 判断当前线程是否支持调用 join() 成员函数。
join() 阻塞当前 thread 对象所在的线程,直至 thread 对象表示的线程执行完毕后,所在线程才能继续执行。
detach() 将当前线程从调用该函数的线程中分离出去,它们彼此独立执行。
swap() 交换两个线程的状态。

注意,每个thread 对象在调用析构函数销毁前,要么调用 join() 函数令主线程等待子线程执行完成,要么调用 detach() 函数将子线程和主线程分离,两者比选其一,否则程序可能存在以下两个问题:

  • 线程占用的资源将无法全部释放,造成内存泄漏;
  • 当主线程执行完成而子线程未执行完时,程序执行将引发异常。
#include <iostream>
#include <thread>
//#include <unistd.h> //Linux
#include <Windows.h> //Windows
using namespace std;
void threadFun1(int n) {
//    sleep(5);//Linux
    Sleep(5);//Windows
    cout << "---thread1 running\n";
    cout << "n=" << n << endl;
}
void threadFun2(const char * url) {
    cout << "---thread2 running\n";
    cout << "url=" << url << endl;
}
int main() {
    //调用第 1 种构造函数
    thread thread1(threadFun1, 10);
    //输出 thread1 线程的 ID
    cout << "thread1 ID:" << thread1.get_id() << endl;
    //调用移动构造函数
    thread thread2 = std::thread(threadFun2, "http://mozhenahi.com");
    //输出 thread2 线程的 ID
    cout << "thread2 ID:" << thread2.get_id() << endl;
    //将 thread1 与主线程分离开,thread1 线程独立执行。
    thread1.detach();
    //判断 thread2 线程是否可以调用 join() 函数
    if (thread2.joinable()) {
        //阻塞主线程,直至 thread2 线程执行完毕。
        thread2.join();
    }
    cout << "main finished" << endl;
    return 0;
}
thread1 ID:2
thread2 ID:3
---thread2 running
url=http://mozhenahi.com
main finished

--------------------------------
Process exited after 0.02273 seconds with return value 0
请按任意键继续. . .


程序中创建了 2 个线程,通过调用 get_id() 成员函数分别获得了它们的线程 ID,其中 thread1 线程独立执行,thread2 线程先于主线程执行完成。通过执行结果可以看到,thread1 线程的执行结果并没有显示到屏幕上,这是因为 thread1 线程还未执行输出语句,主线程就已经执行结束(整个进程也执行结束),thread1 线程无法将执行结果输出到屏幕上。

<thread>头文件中不仅定义了 thread 类,还提供了一个名为 this_thread 的命名空间,此空间中包含一些功能实用的函数,如表 2

函数 功 能
get_id() 获得当前线程的 ID。
yield() 阻塞当前线程,直至条件成熟。
sleep_until() 阻塞当前线程,直至某个时间点为止。
sleep_for() 阻塞当前线程指定的时间(例如阻塞 5 秒)。

实现线程同步

C++ 11 标准为解决“线程间抢夺公共资源”提供了多种方案,其中就包括我们前面讲到的互斥锁和条件变量。

互斥锁

考虑到不同场景的需要,C++ 11 标准提供有多种互斥锁,比如递归互斥锁、定时互斥锁,自动“加锁”和“解锁”的互斥锁等。本节我们以普通的互斥锁为例,给大家讲解互斥锁的基本用法。

C++11标准规定,互斥锁用 mutex 类(位于 std 命名空间中)的对象表示,该类定义在<mutex>头文件中。mutex 类提供有 lock() 和 unlock() 成员函数,分别完成“加锁”和“解锁”功能。

#include <mutex>          // std::mutex
#include <chrono>         // std::chrono::seconds()
#include <thread>
#include <iostream>
using namespace std;
int  n = 0;
std::mutex mtx;           // 定义一个 mutex 类对象,创建一个互斥锁
void threadFun() {
    while(n<10){
        //对互斥锁进行“加锁”
        mtx.lock();
        n++;
        cout << "ID" << std::this_thread::get_id() << " n = "<< n << endl;
        //对互斥锁进行“解锁”
        mtx.unlock();
        //暂停 1 秒
        std::this_thread::sleep_for(std::chrono::seconds(1));
    }
}
int main()
{
    thread th1(threadFun);
    thread th2(threadFun);
    th1.join();
    th2.join();
    return 0;
}

程序执行结果为(不唯一):

ID2 n = 1
ID3 n = 2
ID3 n = 3
ID2 n = 4
ID3 n = 5
ID2 n = 6
ID3 n = 7
ID2 n = 8
ID3 n = 9
ID2 n = 10

--------------------------------
Process exited after 5.044 seconds with return value 0
请按任意键继续. . .




ID2 n = 1
ID3 n = 2
ID2 n = 3
ID3 n = 4
ID2 n = 5
ID3 n = 6
ID2 n = 7
ID3 n = 8
ID3 n = 9
ID2 n = 10

--------------------------------
Process exited after 5.044 seconds with return value 0
请按任意键继续. . .

程序中,访问公共变量 n 的线程有 2 个,为了避免它们之间竞争资源,对 threadFun() 函数中访问 n 变量的过程引入了互斥锁机制。

条件变量

C++ 11标准提供了两种表示条件变量的类,分别是 condition_variable 和 condition_variable_any,它们都定义在<condition_variable>头文件中。我们知道,为了避免线程间抢夺资源,条件变量通常和互斥锁搭配使用,condition_variable 类表示的条件变量只能和 unique_lock 类表示的互斥锁(可自行加锁和解锁)搭配使用,而 condition_variable_any 类表示的条件变量可以和任意类型的互斥锁搭配使用(例如递归互斥锁、定时互斥锁等)。

condition_variable_any 类的对象都表示一个条件变量,该类提供的成员函数如表

成员函数 功 能
wait() 阻塞当前线程,等待条件成立。
wait_for() 阻塞当前线程的过程中,该函数会自动调用 unlock() 函数解锁互斥锁,从而令其他线程使用公共资源。当条件成立或者超过了指定的等待时间(比如 3 秒),该函数会自动调用 lock() 函数对互斥锁加锁,同时令线程继续执行。
wait_until() 和 wait_for() 功能类似,不同之处在于,wait_until() 函数可以设定一个具体时间点(例如 2021年4月8日 的某个具体时间),当条件成立或者等待时间超过了指定的时间点,函数会自动对互斥锁加锁,同时线程继续执行。
notify_one() 向其中一个正在等待的线程发送“条件成立”的信号。
notify_all() 向所有等待的线程发送“条件成立”的信号。
#include <iostream>
#include <thread>             // std::thread
#include <mutex>              // std::mutex, std::unique_lock
#include <condition_variable> // std::condition_variable_any
#include <chrono>         // std::chrono::seconds()
//创建一个互斥锁
std::mutex mtx;
//创建一个条件变量
std::condition_variable_any cond;
void print_id() {
    mtx.lock();
    //阻塞线程,直至条件成立
    cond.wait(mtx);
    std::cout << "----threadID " << std::this_thread::get_id() <<" run" << std::endl;
    //等待 2 秒
    std::this_thread::sleep_for(std::chrono::seconds(2));
    mtx.unlock();
}
void go() {
    std::cout << "go running\n";
    //阻塞线程 2 秒钟
    std::this_thread::sleep_for(std::chrono::seconds(2));
    //通知所有等待的线程条件成立
    cond.notify_all();
}
int main()
{
    //创建 4 个线程执行 print_id() 函数
    std::thread threads[4];
    for (int i = 0; i < 4; ++i)
        threads[i] = std::thread(print_id);
    //创建 1 个线程执行 go() 函数
    std::thread goThread(go);
    //等待所有线程执行结果后,主线程才能继续执行
    goThread.join();
    for (auto& th : threads) {
        th.join();
    }
    return 0;
}
go running
----threadID 2 run
----threadID 4 run
----threadID 5 run
----threadID 3 run

--------------------------------
Process exited after 10.04 seconds with return value 0
请按任意键继续. . .

在Windows平台,Windows API提供了对多线程的支持。前面进程和线程的概念中我们提到,一个程序至少有一个线程,这个线程称为主线程(main thread),如果我们不显示地创建线程,那我们产的程序就是只有主线程的间线程程序。

Linux线程的创建和使用

<pthread.h>

pthread_create

功能:
    创建一个线程
原型:
    int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
                          void *(*start_routine) (void *), void *arg);
参数:  
    thread : 返回线程ID
    attr : 设置线程属性,为NULL时表示使用默认属性
    start_routine : 是个函数地址,线程启动后要执行的函数
    arg : 传给线程启动函数的参数
返回值:
    成功 :0
    失败 : 返回错误码

pthread_exit

功能:
    线程终止
原型:
    void pthread_exit(void *retval);
参数:  
    retval :retval不要指向一个局部变量 
返回值:
    无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(自身)

pthread_join

功能:
    等待线程结束
原型:
    int pthread_join(pthread_t thread, void **retval);
参数:  
    thread :线程ID
    retval : 它指向一个指针,指向线程的返回值
    
返回值:
    成功 : 0
    失败 : 返回错误码

pthread_detach

功能:
    分离线程
原型:
    int pthread_detach(pthread_t thread);
参数:  
    thread :线程ID
    
返回值:
    成功 : 0
    失败 : 返回错误码

pthread_self

功能:
    返回线程的ID
原型:
    int pthread_self(void);    
返回值:
    总是成功,返回调用此函数的线程ID

pthread_cancle

功能:
    取消一个执行中的线程
原型:
    int pthread_cancel(pthread_t thread); 
参数:
    thread : 线程ID
返回值:
    成功 : 0
    失败 : 返回错误码

错误检查

  • 传统的一些函数是,成功返回0, 失败返回-1,并且对全局变量errno赋值以指示错误
  • pthread函数出错时不会设置全局变量errno(而大部分其它POSIX函数会这样做)。而是将错误代码通过返回值返回
  • pthread同样也提供了线程内的errno变量,以支持其它使用errno的代码。对于pthread函数的错误设置,建议通过返回值判定,因为读取返回值要比读取线程内的errno变量的开销更小

进程线程对比

属性 进程 线程
ID pid_t pthread_t
创建 fork pthread_create
等待 waitpid pthread_join
僵尸 waitpid pthread_join、pthread_detach
退出(自杀) exit,return pthread_exit,return
他杀 kill pthread_cancel

用线程实现回射客户端、服务器端

pserver.c

include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>


#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <pthread.h>

#define ERR_EXIT(m)\
    do\
{\
    perror(m);\
    exit(EXIT_FAILURE);\
}while(0)


void do_service(int conn)
{
    char recvbuf[1024] = {0};
    while(1)
    {   
        memset(recvbuf,0,sizeof(recvbuf));
        int ret = read(conn,recvbuf,sizeof(recvbuf));
        if(ret == 0)
        {
            printf("client close\n");
            break;
        }
        else if(ret == -1)
            ERR_EXIT("read");
        fputs(recvbuf,stdout);
        write(conn,recvbuf,ret);
    }
}

void* thread_routine(void *arg)
{
    pthread_detach(pthread_self());

    // 使用取地址的方式获取
    //int conn = *((int*)arg);
    
    // 使用强制转换的方式获取
    // int conn = (int)arg;

    // 使用取地址的方式获取,释放空间
    int conn = *((int*)arg);
    free(arg);

    do_service(conn);
    printf("exit thread %lu ...\n",pthread_self());
    return NULL;
}

int main(void)
{
    int listenfd;
    if((listenfd = socket(PF_INET,SOCK_STREAM,IPPROTO_TCP)) < 0)
        ERR_EXIT("socket");

    struct sockaddr_in serv_addr;
    memset(&serv_addr,0,sizeof(serv_addr)); 
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(5188);
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    //  serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
    //  inet_aton("127.0.0.1",&serv_addr.sin_addr);

    int on = 1;
    if(setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR,&on,sizeof(on)) < 0)
        ERR_EXIT("setsockopt");
    if(bind(listenfd, (struct sockaddr*)(&serv_addr), sizeof(serv_addr)) < 0)
        ERR_EXIT("bind");

    if(listen(listenfd, SOMAXCONN) < 0)
        ERR_EXIT("listen");

    struct sockaddr_in cli_addr;
    socklen_t cli_len = sizeof(cli_addr);
    int conn;

    while(1)
    {
        if((conn = accept(listenfd, (struct sockaddr *)(&cli_addr), &cli_len)) < 0)
            ERR_EXIT("accept");

        printf("ip : %s port : %d\n",inet_ntoa(cli_addr.sin_addr),ntohs(cli_addr.sin_port));

        pthread_t tid;
        int ret;
        // 使用这种方式存在一定的问题,因为是&conn,如果accept返回后,thread_routine还没有来得及处理上一个conn,conn将被改变,导致上一次连接无法处理
        // 最好不要使用指针传递,应使用值传递;
        //if( (ret = pthread_create(&tid, NULL, thread_routine, (void*)&conn)) != 0 )

        //使用这种方式,将int类型装换成无类型指针,int是4个字节,指针也是4个字节;但是这种做法是不可移植的,不同的操作系统,指针所占的字节数不一样
        // if( (ret = pthread_create(&tid, NULL, thread_routine, (void*)conn)) != 0 )

        //申请一块单独的内存放conn,取出后释放掉
        int *p = malloc(sizeof(int));
        *p = conn; 
        if( (ret = pthread_create(&tid, NULL, thread_routine, p)) != 0 )
        {
            fprintf(stderr, "pthread_create : %s\n", strerror(ret));
            exit(EXIT_FAILURE);
        }

    }

    return 0;
}

pclient.c

#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#define ERR_EXIT(m)\
    do\
{\
    perror(m);\
    exit(EXIT_FAILURE);\
}while(0)


void handler(int sig)
{
    printf("recv a sig :%d\n",sig);
    exit(EXIT_SUCCESS);
}

int main(void)
{
    int sock;
    //  socket(PF_INET,SOCK_STREAM,0);
    if((sock = socket(PF_INET,SOCK_STREAM,IPPROTO_TCP)) < 0)
        ERR_EXIT("socket");

    struct sockaddr_in serv_addr;
    memset(&serv_addr,0,sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(5188);
    serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1");

    if(connect(sock,(struct sockaddr*)&serv_addr,sizeof(serv_addr)) < 0 )
        ERR_EXIT("connect");

    pid_t pid;
    pid = fork();
    if(pid < 0)
        ERR_EXIT("fork");

    if(pid == 0)
    {
        char recvbuf[1024] = {0};
        while(1)
        {
            memset(recvbuf,0,sizeof(recvbuf));
            int ret = read(sock,recvbuf,sizeof(recvbuf));
            if (ret == -1)
                ERR_EXIT("read");
            else if(ret == 0)
            {
                printf("peer close\n");
                break;
            }
            fputs(recvbuf,stdout);
        }
        close(sock);
        kill(getppid(),SIGUSR1);
    }
    else
    {
        signal(SIGUSR1,handler);
        char sendbuf[1024] = {0};
        while(fgets(sendbuf,sizeof(sendbuf),stdin) != NULL)
        {   
            write(sock,sendbuf,strlen(sendbuf));
            memset(sendbuf,0,sizeof(sendbuf));
        }
        close(sock);
    }
    return 0;
}