学习博客链接:https://blog.csdn.net/caoshangpa/article/details/52829747
同步异步的区别:https://blog.csdn.net/ideality_hunter/article/details/53453285
我在开头列出博客的并发学习的基础上,理解作者介绍的关于多线程的一些概念,自己写了一遍博客上博主po出的代码,然后原理解释大部分是摘抄作者的解释,当然不是ctrl+v,是在理解的基础上手打上来的,十分感谢这个博主的C++并发系列文章,同时也会十分尊重作者的原创部分。
atomic: 该头文主要声明了两个类, std::atomic和std::atomic_flag,另外还声明了一套C风格的原子类型和与C兼容的原
子操作的函数。
thread:该头文件主要声明了 std::thread 类,另外 std::this_thread 命名空间也在该头文件中。
mutex:该头文件主要声明了与互斥量(mutex)相关的类,包括std::mutex系列类, std::lock_guard,std::unique_lock,以及其他的类型和函数。
condition_variable:该头文件主要声明了与条件变量相关的类,包括std::condition_variable和std::condition_variable_any。
future:该头文件主要声明了std::promise,std::package_task两个Provider类,以及std::future和std::shared_future两个Future类,另外还有一些与之相关的类型和函数,std::async()函数就声明在此头文件中。
#include
#include
#include
using namespace std;
void test(){
cout << "worker thread ID:" << this_thread::get_id() << endl;
cout << "fuck" << endl;
}
int main(){
cout << "main thread ID:" << this_thread::get_id() << endl;
thread workerThread(test);
workerThread.join();
return 0;
}
输出结果为:
main thread ID:1
worker thread ID:2
fuck
因此在整个程序的主线程(threadID=1)运行的过程中,重新开了一个线程(threadID=2)并输出了一句话。
#include
#include
#include
using namespace std;
void test(){
cout << "worker thread ID:" << this_thread::get_id() << endl;
cout << "fuck" << endl;
}
class Mytest{
public:
void operator()(){
test();
}
};
int main(){
cout << "main thread ID:" << this_thread::get_id() << endl;
Mytest mytest;
thread workerThread(mytest);
workerThread.join();
return 0;
}
#include
#include
#include
using namespace std;
void test(int i, string const &s){
cout << "worker thread ID:" << this_thread::get_id() << endl;
cout << "Fuck " << s << i << endl;
}
int main(){
cout << "main Thread ID:" << this_thread::get_id() << endl;
string now = "on ";
//注意这里第二个参数的写法
//为何不直接传str:虽然函数func的第二个参数期待传入一个引用,但是std::thread得构造函数
//并不知晓;构造函数无视函数期待的数据类型,并盲目的拷贝已提供的变量。当线程调用func函数
//时,传递给函数的参数是str变量内部拷贝的引用,而非数据本身的引用。使用std::ref可以解决
//这个问题,将参数转换成引用的形式。
thread workerThread(test, 20180821, ref(now));
workerThread.join();
return 0;
}
#include
#include
#include
#include
using namespace std;
void test(int i, string const &s){
cout << "worker thread ID:" << this_thread::get_id() << endl;
cout << "Fuck " << s << i << endl;
}
int main(){
cout << "main Thread ID:" << this_thread::get_id() << endl;
string now = "on ";
thread workerThread(test, 20180821, ref(now));
//使用detach()会让线程在后台运行,也就是说主线程不会等待workerThread结束。如果线程detach(),
//不可能有std::thread对象能引用它,而且不能再调用该线程的join()方法。
workerThread.detach();
//workerThread.joinable()为false
assert(!workerThread.joinable());
//延时10秒,否则然函数test函数还未执行,main函数就退出了
this_thread::sleep_for(chrono::seconds(10));
return 0;
}
这里补充一下chrono的用法,测当前时间的代码如下:
#include
#include
#include
#include
using namespace std;
string getSysytemTime(){
auto tt = chrono::system_clock::to_time_t(std::chrono::system_clock::now());
struct tm* ptm = localtime(&tt);
char date[60] = {0};
sprintf(date, "%d-%02d-%02d %02d:%02d:%02d", (int)ptm->tm_year+1900, (int)ptm->tm_mon+1, (int)ptm->tm_mday,
(int)ptm->tm_hour,(int)ptm->tm_min,(int)ptm->tm_sec);
return string(date);
}
int main(){
string time = getSysytemTime();
cout << time << endl;
return 0;
}
需要补充
Mutex又称互斥量,C++11中与 Mutex 相关的类(包括锁类型)和函数都声明在 <mutex> < m u t e x > 头文件中,所以如果你需要使用 std::mutex,就必须包含 <mutex> < m u t e x > 头文件。
(1)Mutex系列类(四种)
std::mutex,最基本的 Mutex 类。
std::recursive_mutex,递归 Mutex 类。
std::time_mutex,定时 Mutex 类。
std::recursive_timed_mutex,定时递归 Mutex 类。
(2)Lock系列类(两种)
std::lock_guard,与 Mutex RAII 相关,方便线程对互斥量上锁。
std::unique_lock,与 Mutex RAII 相关,方便线程对互斥量上锁,但提供了更好的上锁和解锁控制。
(3)其他类型(结构体)
std::adopt_lock_t——它的常量对象定义为constexpr adopt_lock_t adopt_lock {};//constexpr 是C++11 中的新关键字)
std::defer_lock_t——它的常量对象定义为constexpr defer_lock_t defer_lock {}; //constexpr 是C++11 中的新关键字)
std::try_to_lock_t——它的常量对象定义为constexpr try_to_lock_t try_to_lock {};//constexpr 是C++11 中的新关键字)
(4)函数
std::try_lock,尝试同时对多个互斥量上锁。
std::lock,可以同时对多个互斥量上锁。
std::call_once,如果多个线程需要同时调用某个函数,call_once 可以保证多个线程对该函数只调用一次。
(1)std::mutex类
☆构造函数,std::mutex不允许拷贝构造,也不允许 move 拷贝,最初产生的 mutex 对象是处于 unlocked 状态的。
☆lock(),调用线程将锁住该互斥量。线程调用该函数会发生下面 3 种情况:①如果该互斥量当前没有被锁住,则调用线程将该互斥量锁住,直到调用 unlock之前,该线程一直拥有该锁。②如果当前互斥量被其他线程锁住,则当前的调用线程被阻塞住。③如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)。
☆unlock(), 解锁,释放对互斥量的所有权。
☆try_lock(),尝试锁住互斥量,如果互斥量被其他线程占有,则当前线程也不会被阻塞。线程调用该函数也会出现下面 3 种情况:① 如果该互斥量当前没有被锁住,则该线程锁住互斥量,直到该线程调用 unlock 释放互斥量。②如果当前互斥量被其他线程锁住,则当前调用线程返回 false,而并不会被阻塞掉。③如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)。
不论是lock()还是try_lock()都需要和unlock()配套使用,下面举例说明lock()和try_lock()的区别。
#include
#include
#include
using namespace std;
mutex mtx;
int cnt = 0;
void work(){
for(int i=0; i<10000; i++){
mtx.lock();
++cnt;
mtx.unlock();
}
}
int main(){
thread workThreads[10];
for(int i=0; i<10; i++){
workThreads[i] = thread(work);
}
for(auto& workThread : workThreads){
workThread.join();
}
cout << cnt << endl;
return 0;
}
结果输出为100000,这是因为lock()的阻塞特性,所以每个线程都统计了10000次,一共是10*10000=100000次。
#include
#include
#include
using namespace std;
mutex mtx;
int cnt = 0;
void work(){
for(int i=0; i<10000; i++){
//mtx.lock();
++cnt;
mtx.unlock();
}
}
int main(){
thread workThreads[10];
for(int i=0; i<10; i++){
workThreads[i] = thread(work);
}
for(auto& workThread : workThreads){
workThread.join();
}
cout << cnt << endl;
return 0;
}
输出为随机值,这是因为try_lock()的非阻塞特性,如果当前互斥量被其他线程锁住,则当前try_lock()返回 false,此时counter并不会增加1。所以这十个线程的统计结果具有随机性。
(2)std::lock_guard和std::unique_lock类std::lock_guard和std::unique_lock类
std::lock_guard使用起来比较简单,除了构造函数外没有其他成员函数。
std::unique_lock除了lock_guard的功能外,提供了更多的成员函数,相对来说更灵活一些。这些成员函数包括lock,try_lock,try_lock_for,try_lock_until、unlock等。
std::unique_lock::lock——用它所管理的Mutex对象的 lock 函数。
std::unique_lock::try_lock——用它所管理的Mutex对象的 try_lock函数。
std::unique_lock::unlock——用它所管理的Mutex对象的 unlock函数。
这两个类相比使用std::mutex的优势在于不用配对使用,无需担心忘记调用unlock而导致的程序死锁。
#include
#include
#include
using namespace std;
mutex mtx;
int cnt = 0;
void work(){
for(int i=0; i<10000; i++){
//将std::lock_guard替换成std::unique_lock,效果是一样的
std::lock_guard <std::mutex> lck(mtx);
++cnt;
}
}
int main(){
thread workThreads[10];
for(int i=0; i<10; i++){
workThreads[i] = thread(work);
}
for(auto& workThread : workThreads){
workThread.join();
}
cout << cnt << endl;
return 0;
}
输出为100000
std::uniqure_lock构造函数的第二个参数可以是std::defer_lock,std::try_to_lock或std::adopt_lock
#include
#include
#include
using namespace std;
mutex mtx;
int cnt = 0;
void work(){
for(int i=0; i<10000; i++){
//将std::lock_guard替换成std::unique_lock,效果是一样的
mtx.lock();
//注意此时Tag参数为std::adopt_lock表明当前线程已经获得了锁,
//此后mtx对象的解锁操作交由unique_lock对象lck来管理,在lck的生命周期结束之后,
//mtx对象会自动解锁。
unique_locklck(mtx, adopt_lock);
++cnt;
}
}
int main(){
thread workThreads[10];
for(int i=0; i<10; i++){
workThreads[i] = thread(work);
}
for(auto& workThread : workThreads){
workThread.join();
}
cout << cnt << endl;
return 0;
}
#include
#include
#include
using namespace std;
mutex mtx;
int cnt = 0;
void work(){
for(int i=0; i<10000; i++){
//注意此时Tag参数为std::defer_lock表明当前线程没有获得锁,
//需要通过lck的lock和unlock来加锁和解锁
unique_lock lck(mtx, defer_lock);
lck.lock();
++cnt;
lck.unlock();
}
}
int main(){
thread workThreads[10];
for(int i=0; i<10; i++){
workThreads[i] = thread(work);
}
for(auto& workThread : workThreads){
workThread.join();
}
cout << cnt << endl;
return 0;
}
(3)期望
C++标准库模型将某种一次性事件称为“期望” (future)。当一个线程需要等待一个特定的一次性事件时,在某种程度上来说它就需要知道这个事件在未来的表现形式。之后,这个线程会周期性(较短的周期)的等待或检查,事件是否触发;在检查期间也会执行其他任务。另外,在等待任务期间它可以先执行另外一些任务,直到对应的任务触发,而后等待期望的状态会变为“就绪”(ready)。当事件发生时(并且期望状态为就绪),这个“期望”就不能被重置。在C++标准库中,有两种“期望”,使用两种类型模板实现,声明在头文件中: 唯一期望(unique futures)( std::future<> )和共享期望(shared futures)( std::shared_future<> )。这是仿照 std::unique_ptr 和 std::shared_ptr 。 std::future 的实例只能与一个指定事件相关联,而 std::shared_future 的实例就能关联多个事件。虽然,我希望用于线程间的通讯,但是“期望”对象本身并不提供同步访问。当多个线程需要访问一个独立“期望”对象时,他们必须使用互斥量或类似同步机制对访问进行保护。
假如有一个耗时的计算,可以启动一个新线程来执行这个计算,但是这就意味着必须关注如何传回计算的结果。因为std::thread并不提供直接接收返回值的机制,这里就需要std::async函数模板了。当任务的结果你不着急要时,你可以使用 std::async 启动一个异步任务。与 std::thread 对象等待运行方式的不同, std::async 会返回一个 std::future 对象,这个对象持有最终计算出来的结果。当你需要这个值时,你只需要调用这个对象的get()成员函数;并且直到“期望”状态为就绪的情况下,线程才会阻塞;之后,返回计算结果。
std::future 通常由某个Provider创建,你可以把Provider想象成一个异步任务的提供者,Provider在某个线程中设置共享状态的值,与该共享状态相关联的std::future对象调用 get(通常在另外一个线程中) 获取该值,如果共享状态的标志不为ready,则调用std::future::get会阻塞当前的调用者,直到Provider设置了共享状态的值(此时共享状态的标志变为 ready),std::future::get返回异步任务的值或异常(如果发生了异常)。
一个有效(valid)的std::future对象通常由以下三种Provider创建,并和某个共享状态相关联。Provider可以是函数或者类,他们分别是:
☆std::async 函数。
☆std::promise::get_future,get_future 为promise类的成员函数。
☆std::packaged_task::get_future,此时get_future为packaged_task的成员函数。
#include
#include
#include
#include
#include
#include
//求校验和
int getCheckSum(char *data, int data_length){
int check_sum = 0;
while(--data_length >= 0){
check_sum += *data++;
}
return check_sum;
}
int Another_task(){
std::cout << "worker thread ID:" << std::this_thread::get_id() << std::endl;
char *str = "Hello World";
return getCheckSum(str, strlen(str));
}
void do_other_task() {
std::cout << "I am on other task" << std::endl;
}
int main(){
std::cout << "main thread ID:" << std::this_thread::get_id() << std::endl;
std::future <int> result = std::async(Another_task);
do_other_task();
std::cout << "The result is " << result.get() << std::endl;
return 0;
}
运行结果为:
main thread ID:140083442382656
I am on other task
worker thread ID:140083425003264
The result is 1052
因为在linux上运行的代码,所以线程ID变成这个样子了,我暂时不清楚为何是这样,但是从这里可以看出Another_task没有在新线程运行。现在来看一下std::async的原型async(std::launch::async | std::lanuch::deferred, f, args…)第一个参数是线程的创建策略,有2种策略:std::launch::async,在调用async就开始创建线程。std::launch::deferred:延迟加载方式创建线程。调用async时不创建线程,直到调用了future的get或者wait时才创建线程。第二个参数是线程函数,第三个参数是线程函数的参数。因此只需要把上诉代码种的std::future
改成std::future
,就可以网Another_task在新的线程中运行了。
(4)承诺
std::promise对象可以保存某一类型T的值,该值可被future对象读取(可能在另外一个线程中),因此Promise也提供了一种线程同步的手段。在promise对象构造时可以和一个共享状态(通常是std::future)相关联,并可以在相关联的共享状态(std::future)上保存一个类型为T的值。可以通过get_future来获取与该promise对象相关联的future对象,调用该函数后,两个对象共享相同的共享状态(shared state)
关键点1:promise对象是异步的Provider,它可以在某一时刻设置共享状态的值
关键点2:future对象可以异步返回共享状态的值,或者在必要的情况下阻塞调用者并等待共享状态标志变为ready,然后才能获取共享状态的值。
#include
#include
#include
int print_int(std::future <int> &fut){
std::cout << "worker thread ID:" << std::this_thread::get_id() << std::endl;
int x = fut.get(); //获取共享状态的值
std::cout << "value: " << x << std::endl;//打印value:10
}
int main(){
std::cout << "main thread ID:" << std::this_thread::get_id() << std::endl;
std::promise <int> prom; //生成一个std::promise对象
std::future <int> fut = prom.get_future(); //和future关联
std::thread t(print_int, std::ref(fut));
prom.set_value(5);//设置共享状态的值,此处和线程t保持同步
t.join();
return 0;
}
输出结果为:
main thread ID:140547805042496
worker thread ID:140547787663104
value: 5
(5)包装任务
std::packaged_task包装一个可调用的对象,并且允许异步获取该可调用对象产生的结果。std::packaged_task将其包装的可调用对象的执行结果传递给一个std::future对象(该对象通常在另外一个线程中获取std::packaged_task任务的执行结果)。
std::packaged_task对象内部包含2个最基本的元素,一,被包装的任务(stored task),任务(task)是一个可调用的对象,如函数指针,成员函数指针或者函数对象,二,共享状态(shared state),用于保存任务的返回值,可以通过std::future对象来达到异步访问共享状态的效果。
可以通过std::packged_task::get_future来获取与共享状态相关联的std::future对象。在调用该函数后,两个对象共享相同的共享状态,具体解释如下:
关键点1:std::package_task对象是异步Provider,它在某一时刻通过调用被包装的任务来设置共享状态的值。
关键点2:std::future对象是一个异步返回对象,通过它可以获得共享状态的值,当然在必要的时候需要等待共享状态标志变为ready。
std::packaged_task的共享状态的生命周期一直持续到最后一个与之关联的对象被释放或者销毁为止。
#include
#include
#include
#include
int work(int l, int r){
std::cout << "worker thread ID:" << std::this_thread::get_id() << std::endl;
for(int i = l; i < r; i++){
std::cout << i << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(1));
}
std::cout << "Finished" << std::endl;
return r - l;
}
int main(){
std::packaged_task <int(int,int)> task(work); //设置package_task
std::future <int> fut = task.get_future(); //获得和packageTask共享状态相关联的future对象
std::thread t(std::move(task), 0, 10); //创建一个新线程完成计数任务,std::move用于无条件的将其参数转换为右值
int val = fut.get(); //等待任务完成并获取结果
std::cout << "The work lasted for" << val << " seconds" << std::endl;
t.join();
return 0;
}
worker thread ID:139813985527552
0
1
2
3
4
5
6
7
8
9
Finished
The work lasted for 10 seconds
(6)条件变量(condition variable)
C++标准库对条件变量有2套实现:std::condition_variable和std::condition_variable_any。这2个实现都包含在 <conditionvariable> < c o n d i t i o n v a r i a b l e > 头文件中。两者都需要与一个互斥量一起才能工作(互斥量是为了同步);前者仅限于与std::mutex一起工作而后者可以和任何满足最低标准的互斥量一起工作,从而加上_any的后缀。因为std::condition_variable_any更加通用,这就可能从体积,性能,以及系统资源的使用方面产生额外的开销,所以std::condition_variable一般作为首选的类型,当对灵活性有硬性要求时,我们才会考虑std::condition_variable_any。下面的例子使用std::condition_variable去处理之前提到的情况—当有数据需要处理时,如何唤醒休眠中的线程对其处理。
#include
#include
#include
#include
#include
int cnt = 0;
std::mutex mtx;
std::queue <int> dataQueue;
std::condition_variable dataCondition;
void pre_deal(){
for(int i=0; i<10; i++){
std::unique_lock <std::mutex> lck(mtx);
++cnt;
dataQueue.push(cnt);
dataCondition.notify_one();
}
}
void deal() {
while(true){
std::unique_lock <std::mutex> lck(mtx);
dataCondition.wait(lck, []{return !dataQueue.empty();});
int num = dataQueue.front();
std::cout << num << std::endl;
dataQueue.pop();
lck.unlock();
}
}
int main(){
std::thread workerThreadPreparation(pre_deal);
workerThreadPreparation.detach();
std::thread workerThreadProcessing(deal);
workerThreadProcessing.detach();
getchar();
return 0;
}
workerThreadPreparation线程将数据存储到队列中,然后调用std::condition_variable的notify_one()成员函数,对等待的线程(这里为workerThreadProcessing线程)进行通知。这里为workerThreadProcessing线程收到通知后,会调用std::condition_variable的成员函数wait(),判断队列是否为空,如果队列不是空,则取出数据并打印。
在条件变量中,实现了一个生产者和一个消费者的模型,现在这里要实现多个生产者和多个消费者的模型。
#include
#include
#include
#include
#include
int cnt = 0;
int maxSize = 30;
std::mutex mtx;
std::queue <int> dataQueue; //被生产者和消费者共享
std::condition_variable producer, consumer; //条件变量是一种同步机制,要和mutex和lock一起使用
void work_consumer() {
while(true){
std::this_thread::sleep_for(std::chrono::milliseconds(1000)); //消费者比生产者慢
std::unique_lock <std::mutex> lck(mtx);
consumer.wait(lck, []{return dataQueue.size()!=0;});
int num = dataQueue.front();
dataQueue.pop();
std::cout << "consumer " << std::this_thread::get_id() << ": " << num << std::endl;
producer.notify_all(); //通知生产者队列中元素个数小于maxSize
}
}
void work_producer(){
while(true){
std::this_thread::sleep_for(std::chrono::milliseconds(900)); //生产者比消费者快
std::unique_lock <std::mutex> lck(mtx);
producer.wait(lck, []{return dataQueue.size()!=maxSize; }); //生产者阻塞等待,直到队列中元素个数小于maxSize
++cnt;
dataQueue.push(cnt);
std::cout << "producer " << std::this_thread::get_id() << ": " << cnt << std::endl;
consumer.notify_all();//通知消费者当前队列中的元素个数大于0
}
}
int main() {
std::thread consumers[2], producers[2];
//两个生产者和消费者
for(int i=0; i<2; i++){
consumers[i] = std::thread(work_consumer);
producers[i] = std::thread(work_producer);
}
for(int i=0; i<2; i++){
producers[i].join();
consumers[i].join();
}
getchar();
return 0;
}
线程池是致力于减少线程本身的开销对应用所产生的影响而出现的,线程池是具有一定的适应场景的,前提就是线程本身的开销和线程执行任务相比是不可以忽略的。如果线程本身的开销对于线程任务执行开销而言是忽略不计的,那么这个时候线程带来的好处是不明显的,甚至可能会变差,比如FTP服务器和Telet服务器,通常传送时文件的时间很长,开销很大,所以此时我们采用线程池并不是理想的方法,也可以选择”即时创建,即时销毁”的策略。线程池的适合场景如下:
(1)单位时间内处理任务频繁而且任务处理时间短
(2)对实时性要求较高。如果接收到任务后再创建线程,可能满足不了实时的要求,因此必须采用线程池缓冲。
先记录一个别人实现好的ThreadPool头文件:
//
// Created by zxy on 18-8-23.
//
#ifndef ACMTEST_THREADPOOL_H
#define ACMTEST_THREADPOOL_H
#include
#include
#include
#include
#include
#include
#include
#include
#include //标准异常类
class ThreadPool {
public:
ThreadPool(size_t);
template<class F, class... Args>
auto enqueue(F&& f, Args&&... args)
-> std::future<typename std::result_of::type>;
~ThreadPool();
private:
// need to keep track of threads so we can join them
std::vector< std::thread > workers;
// the task queue
std::queue< std::function<void()> > tasks;
// synchronization
std::mutex queue_mutex;
std::condition_variable condition;
bool stop;
};
// the constructor just launches some amount of workers
inline ThreadPool::ThreadPool(size_t threads)
: stop(false)
{
for(size_t i = 0;ithis]
{
for(;;)
{
std::function<void()> task;
{
std::unique_lock<std::mutex> lock(this->queue_mutex);
this->condition.wait(lock,
[this]{ return this->stop || !this->tasks.empty(); });
if(this->stop && this->tasks.empty())
return;
task = std::move(this->tasks.front());
this->tasks.pop();
}
task();
}
}
);
}
// add new work item to the pool
template<class F, class... Args>
auto ThreadPool::enqueue(F&& f, Args&&... args)
-> std::future<typename std::result_of::type>
{
using return_type = typename std::result_of::type;
auto task = std::make_shared< std::packaged_task >(
std::bind(std::forward(f), std::forward(args)...)
);
std::future res = task->get_future();
{
std::unique_lock<std::mutex> lock(queue_mutex);
// don't allow enqueueing after stopping the pool
if(stop)
throw std::runtime_error("enqueue on stopped ThreadPool");
tasks.emplace([task](){ (*task)(); });
}
condition.notify_one();
return res;
}
// the destructor joins all threads
inline ThreadPool::~ThreadPool()
{
{
std::unique_lock<std::mutex> lock(queue_mutex);
stop = true;
}
condition.notify_all();
for(std::thread &worker: workers)
worker.join();
}
#endif //ACMTEST_THREADPOOL_H
然后,以下有2个使用这个头文件的例子:
#include
#include "ThreadPool.h"
int main(){
ThreadPool pool(4);
auto result = pool.enqueue([](int answer){return answer;}, 2);
std::cout << result.get() << std::endl;
return 0;
}
输出2
另外一个例子如下:
#include <iostream>
#include "ThreadPool.h"
void work(){
std::this_thread::sleep_for(std::chrono::milliseconds(100));
std::cout << "worker thread ID:" << std::this_thread::get_id() << std::endl;
}
int main(){
ThreadPool pool(4);
while(1){
pool.enqueue(work);
}
return 0;
}
输出结果截图:
worker thread ID:140178403612416
worker thread ID:140178428790528
worker thread ID:worker thread ID:140178420397824140178412005120
worker thread ID:140178403612416
worker thread ID:140178428790528
worker thread ID:140178420397824
worker thread ID:140178428790528
worker thread ID:140178403612416
worker thread ID:140178412005120
worker thread ID:140178420397824
worker thread ID:140178428790528
worker thread ID:140178403612416
worker thread ID:140178412005120
worker thread ID:140178420397824
worker thread ID:140178428790528
worker thread ID:140178403612416
worker thread ID:140178412005120
worker thread ID:140178420397824
worker thread ID:140178428790528
worker thread ID:140178403612416
worker thread ID:140178412005120
worker thread ID:140178428790528
worker thread ID:140178420397824
worker thread ID:140178412005120
worker thread ID:140178403612416
worker thread ID:140178428790528
worker thread ID:140178420397824
worker thread ID:140178412005120
worker thread ID:140178403612416
worker thread ID:140178428790528
worker thread ID:140178412005120
worker thread ID:140178420397824
worker thread ID:140178403612416
worker thread ID:140178428790528
worker thread ID:140178412005120
worker thread ID:140178420397824
worker thread ID:140178403612416
worker thread ID:140178428790528
worker thread ID:140178420397824
worker thread ID:140178412005120
worker thread ID:140178403612416
worker thread ID:140178428790528
worker thread ID:140178420397824
worker thread ID:140178403612416
worker thread ID:140178412005120
worker thread ID:140178428790528
worker thread ID:140178403612416
worker thread ID:140178420397824
worker thread ID:140178412005120
worker thread ID:140178428790528
worker thread ID:140178420397824
worker thread ID:140178403612416
worker thread ID:140178412005120
worker thread ID:140178428790528
worker thread ID:140178403612416
worker thread ID:140178420397824
worker thread ID:140178412005120
worker thread ID:140178428790528
worker thread ID:140178403612416
worker thread ID:140178420397824
worker thread ID:140178412005120
worker thread ID:140178428790528
worker thread ID:140178403612416
worker thread ID:140178420397824
worker thread ID:140178412005120
worker thread ID:140178428790528
worker thread ID:140178403612416
worker thread ID:140178412005120
worker thread ID:140178420397824
worker thread ID:140178403612416
worker thread ID:140178412005120
worker thread ID:140178420397824
worker thread ID:140178428790528
worker thread ID:140178403612416
worker thread ID:140178412005120
worker thread ID:140178420397824
worker thread ID:140178428790528
worker thread ID:140178403612416
worker thread ID:140178412005120
worker thread ID:140178420397824
worker thread ID:140178428790528
worker thread ID:140178403612416
worker thread ID:140178412005120