• 线程本地存储记录


    线程本地存储(TLS)的一些总结

    一、静态线程局部存储

    1.MinGW编译器下

    代码:

    #include<iostream>
    #include<thread>
    #include<windows.h>
    #include<mutex>
    using namespace std;
    
    mutex mu;
    __thread int num = 0;//使用关键字__thread修改的变量
    
    
    void test(int i)
    {
    	num+=i;
    	
    	mu.lock();
    	cout<<"thread "<<this_thread::get_id()<<" num = "<<num<<endl;
    	mu.unlock();
    }
    
    int main()
    {
    	clock_t t1 = clock();
    	thread th[5];
    	for(int i=0;i<5;i++)
    	{
    		th[i] = thread(test,i);
    	}
    	for(int i=0;i<5;i++)
    	{
    		th[i].join();
    	}
    	cout<<"finish main num = "<<num<<endl;
    	clock_t t2 = clock();
    	cout<<"use time = "<<(t2-t1)<<endl;
    	system("pause");
    }
    

    静态存储主要依靠关键字 __thread修饰变量。

    运行结果:
    image

    2.msvc编译器下

    代码类似,只是前面的修饰符变化而已。

    代码:

    #include<iostream>
    #include<thread>
    #include<windows.h>
    #include<mutex>
    using namespace std;
    
    mutex mu;
    //_thread int num = 0;
    _declspec (thread) int num = 0;
    
    
    void test(int i)
    {
    	num += i;
    
    	mu.lock();
    	cout << "thread " << this_thread::get_id() << " num = " << num << endl;
    	mu.unlock();
    	//Sleep(1000);
    }
    
    int main()
    {
    	clock_t t1 = clock();
    	thread th[5];
    	for (int i = 0; i<5; i++)
    	{
    		th[i] = thread(test, i);
    	}
    	for (int i = 0; i<5; i++)
    	{
    		th[i].join();
    	}
    	cout << "finish main num = " << num << endl;
    	clock_t t2 = clock();
    	cout << "use time = " << (t2 - t1) << endl;
    	system("pause");
    	return 0;
    }
    

    运行结果就不贴了,运行结果基本一直,除了线程id为全局id之外。

    二、动态线程局部存储

    代码:

    #include<iostream>
    #include<thread>
    #include<windows.h>
    #include<mutex>
    using namespace std;
    
    mutex mu;
    __declspec (thread) int num = 0;
    DWORD index = 0;
    void test(int i)
    {
    	num+=i;
    	int count = 7*i;
    	TlsSetValue(index,(LPVOID)count);//此函数必须在自己的线程之内操作,才能改变自己的槽
        //这里举个简单的例子,就一个int和LPVOID是一样大的。更小的数据,比如char之类的,直接存储就好了。大一些的就只能存储指针。
    	mu.lock();
      
    	cout<<"thread "<<this_thread::get_id()<<" num = "<<num<<endl;
    	cout<<(int)TlsGetValue(index)<<endl;//在自己的线程之内使用,才有意义。
        __asm {
    		mov	eax, DWORD PTR FS:[0x18]//TEB环境块
    		mov eax, DWORD PTR DS :[eax + 0xE14] //因为index为1,所以是0XE10+0x4
    		mov count,eax
    	}
    	cout << "count = " << count << endl;//操作线程存储槽和函数是一样的
    	mu.unlock();
    }
    
    int main()
    {
    	index = TlsAlloc();
    	clock_t t1 = clock();
    	thread th[5];
    	for(int i=0;i<5;i++)
    	{
    		th[i] = thread(test,i);
    	}
    	for(int i=0;i<5;i++)
    	{
    		th[i].join();
    	}
    	cout<<"finish main num = "<<num<<endl;
    	clock_t t2 = clock();
    	cout<<"use time = "<<(t2-t1)<<endl;
    	TlsFree(index);
    	system("pause");
    }
    

    image

    直接操作线程的TLS存储槽和使用这几个函数是一样的效果。

    三、TLS回调函数。

    代码来源于网上。需要注意的是debug版本的exe可以看见回调函数输出到控制台,release版本的看不见。

    参考至本篇博客:https://www.cnblogs.com/dliv3/p/6489629.html

    代码:

    #include <windows.h>
    
    //告知连接器使用TLS
    #pragma comment(linker, "/INCLUDE:__tls_used")
    
    void print_console(char* szMsg)
    {
    	HANDLE hStdout = GetStdHandle(STD_OUTPUT_HANDLE);
    	//先于主线程调用执行的TLS回调函数中使用printf可能会发生Runtime Error,可直接调用WriteConsole API
    	WriteConsoleA(hStdout, szMsg, strlen(szMsg), NULL, NULL);
    }
    
    void NTAPI TLS_CALLBACK1(PVOID DllHandle, DWORD Reason, PVOID Reserved)
    {
    	char szMsg[80] = { 0, };
    	wsprintfA(szMsg, "TLS_CALLBACK1() : DllHandle = %X, Reason = %d\n", DllHandle, Reason);
    	print_console(szMsg);
    }
    
    void NTAPI TLS_CALLBACK2(PVOID DllHandle, DWORD Reason, PVOID Reserved)
    {
    	char szMsg[80] = { 0, };
    	wsprintfA(szMsg, "TLS_CALLBACK2() : DllHandle = %X, Reason = %d\n", DllHandle, Reason);
    	print_console(szMsg);
    }
    /*
    注册TLS函数
    .CRT$XLX的作用
    CRT表示使用C Runtime 机制
    X表示表示名随机
    L表示TLS Callback section
    X也可以换成B~Y任意一个字符
    */
    #pragma data_seg(".CRT$XLX")
    //存储回调函数地址
    PIMAGE_TLS_CALLBACK pTLS_CALLBACKs[] = { TLS_CALLBACK1, TLS_CALLBACK2, 0 };
    #pragma data_seg()
    
    DWORD WINAPI ThreadProc(LPVOID lParam)
    {
    	print_console("ThreadProc() start\n");
    
    	print_console("ThreadProc() end\n");
    
    	return 0;
    }
    
    int main(void)
    {
    	HANDLE hThread = NULL;
    
    	print_console("main() start\n");
    	//创建子线程
    	hThread = CreateThread(NULL, 0, ThreadProc, NULL, 0, NULL);
    	//等待子线程结束
    	WaitForSingleObject(hThread, 60 * 1000);
    	CloseHandle(hThread);
    
    	print_console("main() end\n");
    	system("pause");
    	return 0;
    }
    
    

    image

    回调函数标准写法:

    BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
    
    回调函数执行原因:
    
    #define DLL_PROCESS_ATTACH 1 	//进程启动
    #define DLL_THREAD_ATTACH 2		//线程启动
    #define DLL_THREAD_DETACH 3		//线程结束
    #define DLL_PROCESS_ATTACH 0	//进程结束
    
    

    四:WindowsPE权威指南关于线程本地存储的实验。

    代码:

    	.386
    	.model flat,stdcall
    	option casemap:none
    
    include windows.inc
    include user32.inc
    includelib user32.lib
    include kernel32.inc
    includelib kernel32.lib
    
    	.data
    szText db 'HelloWorldPE',0,0,0,0
    
    ;所谓构造,就是自己指定0x18字节的数据,作为TLS目录表 IMAGE_TLS_DIRECTORY32
    TLS_DIRR dd offset Tls1
    		 dd offset Tls2
    		 dd offset Tls3
    		 dd offset TlsCallBack
    		 dd 0
    		 dd 0
    Tls1     dd 0
    Tls2 	 dd 0
    Tls3 	 dd 0
    TlsCallBack dd	offset TLS
    			dd 0
    			dd 0
    	.data?
    TLSCalled db ?
    	.code
    start:
    	invoke ExitProcess,NULL
    	RET
    TLS:
    cmp byte ptr [TLSCalled],1 ;因为回调函数会多次调用,和上面Cpp的调用结果一样,这里只是为了在创建的时候有信息框弹出。
    je @exit
    mov byte ptr [TLSCalled],1
    invoke MessageBox,NULL,addr szText,NULL,MB_OK
    
    @exit:
    RET
    	end start
    

    这么编译,PE的数据目录表本来是没有TLS表的,所以需要我们手动修改exe

    image

    从.text节往上数,然后修改即可。

    修改完毕,再运行就有信息框了。

  • 相关阅读:
    开发中学习英语
    eclipse 常用快捷键
    eclipse 查看快捷键
    沟通的方式——大道至简第四章读后感
    java练习题——类与对象
    团队与管理方法——大道至简第三章读后感
    java程序——两数的加减乘除
    java练习题
    java程序——从命令行接收多个数字,求和之后输出结果
    李冰烧山——大道至简第二章读后感
  • 原文地址:https://www.cnblogs.com/dayq/p/16063657.html
Copyright © 2020-2023  润新知