Thread t = new Thread(PrintNumbersWithDelay);
t.Start();
t.Join();

但我们在主程序中调用了t.Join方法,该方法允许我们等待直到线程t完成。当线程t完成时,主程序会继续运行。借助该技术可以实现在两个线程间同步。

当主程序和单独的数字打印线程运行时,我们等待6秒后对线程调用了t.Abort方法。这给线程注入了ThreadAbortException方法,导致线程被终结。这非常危险,因为该异常可以在任何时刻发生并可能彻底摧毁应用程序。另外,使用该技术也不一定总能终止线程。目标线程可以通过处理该异常并调用Thread.ResetAbort方法来拒绝被终止。因此并不推荐使用Abort方法来关闭线程。可优先使用一些其他方法,比如提供一个CancellationToken方法来取消线程的执行。

进程会等待所有的前台线程完成后再结束工作,但是如果只剩下后台线程,则会直接结束工作。

线程之间传递参数:

    var threadThree = new Thread(() => CountNumbers());
threadThree.Name = "ThreadThree";
threadThree.Start();
threadThree.Join(); static void CountNumbers(int iterations)
{
for (int i = ; i <= iterations; i++)
{
Sleep(TimeSpan.FromSeconds());
WriteLine($"{CurrentThread.Name} prints {i}");
}
}

当主程序启动时,定义了两个将会抛出异常的线程。其中一个对异常进行了处理,另一个则没有。可以看到第二个异常没有被包裹启动线程的try/catch代码块捕获到。所以如果直接使用线程,一般来说不要在线程中抛出异常,而是在线程代码中使用try/catch代码块。

第一章 线程基础

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

第二章 线程同步

为了利用好这两种方式,可以使用混合模式(hybrid)。混合模式先尝试使用用户模式等待,如果线程等待了足够长的时间,则会切换到阻塞状态以节省CPU资源。

·执行基本的原子操作

而借助于Interlocked类,我们无需锁定任何对象即可获取到正确的结果。Interlocked提供了Increment、Decrement和Add等基本数学操作的原子方法,从而帮助我们在编写Counter类时无需使用锁

class CounterNoLock : CounterBase
{
private int _count; public int Count => _count; public override void Increment()
{
Interlocked.Increment(ref _count);
} public override void Decrement()
{
Interlocked.Decrement(ref _count);
}
}

·使用Mutex类

    static void Main(string[] args)
{
const string MutexName = "CSharpThreadingCookbook"; using (var m = new Mutex(false, MutexName))
{
if (!m.WaitOne(TimeSpan.FromSeconds(), false))
{
WriteLine("Second instance is running!");
}
else
{
WriteLine("Running!");
ReadLine();
m.ReleaseMutex();
}
}
}

当主程序启动时,定义了一个指定名称的互斥量,设置initialOwner标志为false。这意味着如果互斥量已经被创建,则允许程序获取该互斥量。如果没有获取到互斥量,程序则简单地显示Running,等待直到按下了任何键,然后释放该互斥量并退出。
如果再运行同样一个程序,则会在5秒钟内尝试获取互斥量。如果此时在第一个程序中按下了任何键,第二个程序则会开始执行。然而,如果保持等待5秒钟第二个程序将无法获取到该互斥量

注意具名的互斥量是全局的操作系统对象!请务必正确关闭互斥量。最好是使用using代码块来包裹互斥量对象。
该方式可用于在不同的程序中同步线程,可被推广到大量的使用场景中。

·使用SemaphoreSlim类

    static SemaphoreSlim _semaphore = new SemaphoreSlim();

        static void AccessDatabase(string name, int seconds)
{
WriteLine($"{name} waits to access a database");
_semaphore.Wait();
WriteLine($"{name} was granted an access to a database");
Sleep(TimeSpan.FromSeconds(seconds));
WriteLine($"{name} is completed");
_semaphore.Release();
}

每个线程都尝试获取数据库的访问,但是我们借助于信号系统限制了访问数据库的并发数为4个线程。当有4个线程获取了数据库的访问后,其他两个线程需要等待,直到之前线程中的某一个完成工作并调用_semaphore.Release方法来发出信号。

·使用AutoResetEvent类

    private static AutoResetEvent _workerEvent = new AutoResetEvent(false);
private static AutoResetEvent _mainEvent = new AutoResetEvent(false);

AutoResetEvent类采用的是内核时间模式,所以等待时间不能太长。使用ManualResetEventslim类更好,因为它使用的是混合模式。

·使用ManualResetEventSlim类

在前面某小节中,我们使用了一种无法在操作系统层面工作的混合模式。如果我们需要全局事件,则可以使用EventWaitHandle类,其是AutoResetEvent和ManualResetEvent类的基类
·使用CountDownEvent类

本节将描述如何使用CountdownEvent信号类来等待直到一定数量的操作完成。

    static void Main(string[] args)
{
WriteLine("Starting two operations");
var t1 = new Thread(() => PerformOperation("Operation 1 is completed", ));
var t2 = new Thread(() => PerformOperation("Operation 2 is completed", ));
t1.Start();
t2.Start();
_countdown.Wait();
WriteLine("Both operations have been completed.");
_countdown.Dispose(); System.Threading.Thread.Sleep(TimeSpan.FromSeconds());
} static CountdownEvent _countdown = new CountdownEvent(); static void PerformOperation(string message, int seconds)
{
Sleep(TimeSpan.FromSeconds(seconds));
WriteLine(message);
_countdown.Signal();
}

当主程序启动时,创建了一个CountdownEvent实例,在其构造函数中指定了当两个操作完成时会发出信号。然后我们启动了两个线程,当它们执行完成后会发出信号。一旦第二个线程完成,主线程会从等待CountdownEvent的状态中返回并继续执行。针对需要等待多个异步操作完成的情形,使用该方式是非常便利的。
然而这有一个重大的缺点。如果调用_countdown.Signal()没达到指定的次数,那么_countdown.Wait()将一直等待。请确保使用CountdownEvent时,所有线程完成后都要调用Signal方法。

·使用Barrier类

Barrier类用于组织多个线程及时在某个时刻碰面。其提供了一个回调函数,每次线程调用了SignalAndWait方法后该回调函数会被执行。

每个线程将向Barrier发送两次信号,所以会有两个阶段。每次这两个线程调用Signal-AndWait方法时,Barrier将执行回调函数。这在多线程迭代运算中非常有用,可以在每个迭代结束前执行一些计算。当最后一个线程调用SignalAndWait方法时可以在迭代结束时进行交互。

    static void Main(string[] args)
{
var t1 = new Thread(() => PlayMusic("the guitarist", "play an amazing solo", ));
var t2 = new Thread(() => PlayMusic("the singer", "sing his song", )); t1.Start();
t2.Start();
} static Barrier _barrier = new Barrier(, b => WriteLine($"End of phase {b.CurrentPhaseNumber + 1}")); static void PlayMusic(string name, string message, int seconds)
{
for (int i = ; i < ; i++)
{
WriteLine("----------------------------------------------");
Sleep(TimeSpan.FromSeconds(seconds));
WriteLine($"{name} starts to {message}");
Sleep(TimeSpan.FromSeconds(seconds));
WriteLine($"{name} finishes to {message}");
_barrier.SignalAndWait();
}
}

·使用ReaderWriterLockSlim类

使用ReaderWriterLockSlim来创建一个线程安全的机制,在多线程中对一个集合进行读写操作。ReaderWriterLockSlim代表了一个管理资源访问的锁,允许多个线程同时读取,以及独占写。

当主程序启动时,同时运行了三个线程来从字典中读取数据,还有另外两个线程向该字典中写入数据。我们使用ReaderWriterLockSlim类来实现线程安全,该类专为这样的场景而设计。
这里使用两种锁:读锁允许多线程读取数据,写锁在被释放前会阻塞了其他线程的所有操作。获取读锁时还有一个有意思的场景,即从集合中读取数据时,根据当前数据而决定是否获取一个写锁并修改该集合。一旦得到写锁,会阻止阅读者读

取数据,从而浪费大量的时间,因此获取写锁后集合会处于阻塞状态。为了最小化阻塞浪费的时间,可以使用EnterUpgradeableReadLock和ExitUpgradeableReadLock方法。先获取读锁后读取数据。如果发现必须修改底层集合,只需使用EnterWriteLock方法升级锁,然后快速执行一次写操作,最后使用ExitWriteLock释放写锁。
在本例中,我们先生成一个随机数。然后获取读锁并检查该数是否存在于字典的键集合中。如果不存在,将读锁更新为写锁然后将该新键加入到字典中。始终使用try/finally代码块来确保在捕获锁后一定会释放锁,这是一项好的实践。

    static void Main(string[] args)
{
new Thread(Read){ IsBackground = true }.Start();
new Thread(Read){ IsBackground = true }.Start();
new Thread(Read){ IsBackground = true }.Start(); new Thread(() => Write("Thread 1")){ IsBackground = true }.Start();
new Thread(() => Write("Thread 2")){ IsBackground = true }.Start(); Sleep(TimeSpan.FromSeconds());
} static ReaderWriterLockSlim _rw = new ReaderWriterLockSlim();
static Dictionary<int, int> _items = new Dictionary<int, int>(); static void Read()
{
WriteLine("Reading contents of a dictionary");
while (true)
{
try
{
_rw.EnterReadLock();
foreach (var key in _items.Keys)
{
Sleep(TimeSpan.FromSeconds(0.1));
}
}
finally
{
_rw.ExitReadLock();
}
}
} static void Write(string threadName)
{
while (true)
{
try
{
int newKey = new Random().Next();
_rw.EnterUpgradeableReadLock();
if (!_items.ContainsKey(newKey))
{
try
{
_rw.EnterWriteLock();
_items[newKey] = ;
WriteLine($"New key {newKey} is added to a dictionary by a {threadName}");
}
finally
{
_rw.ExitWriteLock();
}
}
Sleep(TimeSpan.FromSeconds(0.1));
}
finally
{
_rw.ExitUpgradeableReadLock();
}
}
}

·使用SpinWait类

本节将描述如何不使用内核模型的方式来使线程等待。另外,我们介绍了SpinWait,它是一个混合同步构造,被设计为使用用户模式等待一段时间,然后切换到内核模式以节省CPU时间。

----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

第三章 使用线程池

在线程池中调用委托

向线程池中放入异步操作

    ThreadPool.QueueUserWorkItem( _ =>
{
WriteLine($"Operation state: {x + y}, {lambdaState}");
WriteLine($"Worker thread id: {CurrentThread.ManagedThreadId}");
Sleep(TimeSpan.FromSeconds());
}, "lambda state");

线程池与并行度

实现一个取消按钮

在线程池中使用等待事件处理器及超时

使用计时器

使用backgroundwork组件

---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

第四章  使用任务并行库

创建任务

Task.Run(() => TaskMethod("Task 3"));

使用任务执行基本的操作

组合任务

    firstTask = new Task<int>(() =>
{
var innerTask = Task.Factory.StartNew(() => TaskMethod("Second Task", ),
TaskCreationOptions.AttachedToParent); innerTask.ContinueWith(t => TaskMethod("Third Task", ),
TaskContinuationOptions.AttachedToParent); return TaskMethod("First Task", );
});

将APM模式转换成任务

    Task<string> task = Task<string>.Factory.FromAsync(
d.BeginInvoke("AsyncTaskThread", Callback,
"a delegate asynchronous call"), d.EndInvoke);

将EAP模式转化成任务

    var tcs = new TaskCompletionSource<int>();

            var worker = new BackgroundWorker();
worker.DoWork += (sender, eventArgs) =>
{
eventArgs.Result = TaskMethod("Background worker", );
}; worker.RunWorkerCompleted += (sender, eventArgs) =>
{
if (eventArgs.Error != null)
{
tcs.SetException(eventArgs.Error);
}
else if (eventArgs.Cancelled)
{
tcs.SetCanceled();
}
else
{
tcs.SetResult((int)eventArgs.Result);
}
}; worker.RunWorkerAsync(); int result = tcs.Task.Result; WriteLine($"Result is: {result}");

实现取消选项

   var cts = new CancellationTokenSource();
var longTask = new Task<int>(() => TaskMethod("Task 1", , cts.Token), cts.Token);

处理任务中的异常

  task = Task.Run(() => TaskMethod("Task 2", ));
int result = task.GetAwaiter().GetResult();
WriteLine($"Result: {result}");
    var t1 = new Task<int>(() => TaskMethod("Task 3", ));
var t2 = new Task<int>(() => TaskMethod("Task 4", ));
var complexTask = Task.WhenAll(t1, t2);

并行运行任务

var whenAllTask = Task.WhenAll(firstTask, secondTask);
var completedTask = Task.WhenAny(tasks).Result;

使用TaskScheduler配置任务的执行

    void ButtonAsyncOK_Click(object sender, RoutedEventArgs e)
{
ContentTextBlock.Text = string.Empty;
Mouse.OverrideCursor = Cursors.Wait;
Task<string> task = TaskMethod(TaskScheduler.FromCurrentSynchronizationContext()); task.ContinueWith(t => Mouse.OverrideCursor = null,
CancellationToken.None,
TaskContinuationOptions.None,
TaskScheduler.FromCurrentSynchronizationContext());
}

------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

第五章 使用c#6.0

使用await操作符获取异步任务结果

在lambda表达式中使用await操作符

    static async Task AsynchronousProcessing()
{
Func<string, Task<string>> asyncLambda = async name => {
await Task.Delay(TimeSpan.FromSeconds());
return
$"Task {name} is running on a thread id {CurrentThread.ManagedThreadId}." +
$" Is thread pool thread: {CurrentThread.IsThreadPoolThread}";
}; string result = await asyncLambda("async lambda"); WriteLine(result);
}

对连续的异步任务使用await操作符

对并行执行的异步任务使用await操作符

static async Task AsynchronousProcessing()
{
Task<string> t1 = GetInfoAsync("Task 1", );
Task<string> t2 = GetInfoAsync("Task 2", ); string[] results = await Task.WhenAll(t1, t2);
foreach (string result in results)
{
WriteLine(result);
}
} static async Task<string> GetInfoAsync(string name, int seconds)
{
await Task.Delay(TimeSpan.FromSeconds(seconds));//使用同一线程
//await Task.Run(() => Thread.Sleep(TimeSpan.FromSeconds(seconds)));//使用不同线程
return
$"Task {name} is running on a thread id {CurrentThread.ManagedThreadId}." +

处理异步操作中的异常

避免使用捕获的同步上下文

static async Task<TimeSpan> TestNoContext()
{
const int iterationsNumber = ;
var sw = new Stopwatch();
sw.Start();
for (int i = ; i < iterationsNumber; i++)
{
var t = Task.Run(() => { });
await t.ConfigureAwait(
continueOnCapturedContext: false);
}
sw.Stop();
return sw.Elapsed;
}

使用 async void方法

设计一个自定义的awaitable类型

对动态类型使用await

            IAwaiter<string> proxy = Impromptu.ActLike(awaiter);

            result.GetAwaiter = (Func<dynamic>) ( () => proxy );

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

第六章 使用并发集合

使用concurrentDictionary

使用concurrentQueue实现一步处理

改变concurrentStack异步处理顺序

使用concurrentBag创建一个可扩展的爬虫

使用concurrentCollection进行异步处理

---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

第七章 使用PLINQ

使用Parallel类

Parallel.Invoke(
() => EmulateProcessing("Task1"),
() => EmulateProcessing("Task2"),
() => EmulateProcessing("Task3")
);

并行化Linq查询

    parallelQuery = from t in GetTypes().AsParallel()
select EmulateProcessing(t); parallelQuery.ForAll(PrintInfo);

调整plinq查询的参数

try
{
parallelQuery
.WithDegreeOfParallelism(Environment.ProcessorCount)
.WithExecutionMode(ParallelExecutionMode.ForceParallelism)
.WithMergeOptions(ParallelMergeOptions.Default)
.WithCancellation(cts.Token)
.ForAll(WriteLine);
}

处理plinq查询中的异常

    try
{
parallelQuery.ForAll(WriteLine);
}
catch (DivideByZeroException)
{
WriteLine("Divided by zero - usual exception handler!");
}
catch (AggregateException e)
{
e.Flatten().Handle(ex =>
{
if (ex is DivideByZeroException)
{
WriteLine("Divided by zero - aggregate exception handler!");
return true;
} return false;
});
}

管理plinq查询中的数据分区

    public class StringPartitioner : Partitioner<string>
{
private readonly IEnumerable<string> _data; public StringPartitioner(IEnumerable<string> data)
{
_data = data;
} public override bool SupportsDynamicPartitions => false; public override IList<IEnumerator<string>> GetPartitions(int partitionCount)
{
var result = new List<IEnumerator<string>>(partitionCount); for (int i = ; i <= partitionCount; i++)
{
result.Add(CreateEnumerator(i, partitionCount));
} return result;
} IEnumerator<string> CreateEnumerator(int partitionNumber, int partitionCount)
{
int evenPartitions = partitionCount / ;
bool isEven = partitionNumber % == ;
int step = isEven ? evenPartitions : partitionCount - evenPartitions;
int startIndex = partitionNumber / + partitionNumber % ; var q = _data
.Where(v => !(v.Length % == ^ isEven) || partitionCount == )
.Skip(startIndex - ); return q
.Where((x, i) => i % step == )
.GetEnumerator(); }
}

为plinq查询创建一个自定义的聚合器

    var parallelAggregator = parallelQuery.Aggregate(
() => new ConcurrentDictionary<char, int>(),
(taskTotal, item) => AccumulateLettersInformation(taskTotal, item),
(total, taskTotal) => MergeAccumulators(total, taskTotal),
total => total);

---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

第八章 RE

将普通集合转换为异步的可观察集合

        o = EnumerableEventSequence().ToObservable()
.SubscribeOn(TaskPoolScheduler.Default);

编写自定义的可观察对象

使用Subject

创建可观察的对象

对可观察的集合使用linq查询

使用RX创建异步操作

第九章 使用异步I/O

异步使用文件

   using (var stream = File.Create("test3.txt", BUFFER_SIZE, FileOptions.Asynchronous))
using (var sw = new StreamWriter(stream))
{
WriteLine($"3. Uses I/O Threads: {stream.IsAsync}");
await sw.WriteAsync(CreateFileContent());
}

编写一个异步的http服务端和客户端

    public async Task Start()
{
_listener.Start(); while (true)
{
var ctx = await _listener.GetContextAsync();
WriteLine("Client connected...");
var response = string.Format(RESPONSE_TEMPLATE, DateTime.Now); using (var sw = new StreamWriter(ctx.Response.OutputStream))
{
await sw.WriteAsync(response);
await sw.FlushAsync();
}
}
}

异步操作数据库

await connection.OpenAsync();
await detachCommand.ExecuteNonQueryAsync();

异步调用wcf服务

第十章 并行编程模式

实现惰性求值的共享状态

    class BCLThreadSafeFactory : IHasValue
{
private ValueToAccess _value; public ValueToAccess Value =>
LazyInitializer.EnsureInitialized(ref _value, Compute);
}
  class LazyWrapper : IHasValue
{
private readonly Lazy<ValueToAccess> _value; public LazyWrapper(Lazy<ValueToAccess> value )
{
_value = value;
} public ValueToAccess Value => _value.Value;
}

使用blockingcollection实现并行管道

使用tpl数据流实现并行管道

使用plinq实现map/reduce模式

第十一章 更多信息

c#多线程编程实战(原书第二版)文摘的更多相关文章

  1. 《模式分类(原书第二版)》pdf格式下载电子书免费下载

    <模式分类(原书第二版)>pdf格式下载电子书免费下载: https://u253469.ctfile.com/fs/253469-302448505 内容简介 <模式分类>( ...

  2. 《数据结构与算法分析:C语言描述_原书第二版》CH2算法分析_课后习题_部分解答

    对于一个初学者来说,作者的Solutions Manual把太多的细节留给了读者,这里尽自己的努力给出部分习题的详解: 不当之处,欢迎指正. 1.  按增长率排列下列函数:N,√2,N1.5,N2,N ...

  3. 《数据结构与算法分析:C语言描述_原书第二版》CH3表、栈和队列_reading notes

    表.栈和队列是最简单和最基本的三种数据结构.基本上,每一个有意义的程序都将明晰地至少使用一种这样的数据结构,比如栈在程序中总是要间接地用到,不管你在程序中是否做了声明. 本章学习重点: 理解抽象数据类 ...

  4. 数据结构算法与应用c++语言描述 原书第二版 答案(更新中

    目录 第一章 C++回顾 函数与参数 1.交换两个整数的不正确代码. 异常 10.抛出并捕捉整型异常. 第一章 C++回顾 函数与参数 1.交换两个整数的不正确代码. //test_1 void sw ...

  5. Java多线程编程实战读书笔记(一)

    多线程的基础概念本人在学习多线程的时候发现一本书——java多线程编程实战指南.整理了一下书中的概念制作成了思维导图的形式.按照书中的章节整理,并添加一些个人的理解.

  6. C#多线程编程实战(二)

    1.1 简介 为了防止一个应用程序控制CPU而导致其他应用程序和操作系统本身永远被挂起这一可能情况,操作系统不得不使用某种方式将物理计算分割为一些虚拟的进程,并给予每个执行程序一定量的计算能力.此外操 ...

  7. 《Go并发编程实战》第2版 紧跟Go的1.8版本号

    文章作者:郝林(<Go并发编程实战 (第2版)>作者) 最终来了! 经过出版社的各位编辑.校对.排版伙伴与我的N轮PK和共同努力,<Go并发编程实战>第2版的全部内容最终全然确 ...

  8. Java多线程编程实战指南(核心篇)读书笔记(五)

    (尊重劳动成果,转载请注明出处:http://blog.csdn.net/qq_25827845/article/details/76730459冷血之心的博客) 博主准备恶补一番Java高并发编程相 ...

  9. Java多线程编程实战指南(核心篇)读书笔记(四)

    (尊重劳动成果,转载请注明出处:http://blog.csdn.net/qq_25827845/article/details/76690961冷血之心的博客) 博主准备恶补一番Java高并发编程相 ...

随机推荐

  1. Codeforces Beta Round #49 (Div. 2)

    Codeforces Beta Round #49 (Div. 2) http://codeforces.com/contest/53 A #include<bits/stdc++.h> ...

  2. 多线程通信(wait和notify)

    线程通信概念: 线程是操作系统中独立的个体,但这些个体如果不经过特殊的处理就不能成为一个整体,线程间的通信就成为整体的必用方式之一.当线程存在通信指挥,系统间的交互性会更强大,在提高CPU利用率的同时 ...

  3. MVC中的七层架构

    工厂模式的七层架构 1.创建Model,实现业务实体. 2.创建IDAL,实现接口. 3.创建DAL,实现接口里的方法. 4.创建DBUtility,数据库操作类5.创建DALFactory,抽象工程 ...

  4. mysql定位慢查询

    mysql定位慢查询 //显示数据库的状态 show status; //显示执行了多少次插入 show status like 'com_insert'; //显示执行了多少次更新 show sta ...

  5. XP下1433端口打不开

    问题:操作系统windows xp:数据库sql server 2000:安装后通过程序无法访问数据,原因是数据库的默认端口1433未打开. 原因:sql server 2000的bug,需要升级补丁 ...

  6. 转录组的组装Stingtie和Cufflinks

    转录组的组装Stingtie和Cufflinks Posted: 十月 18, 2017  Under: Transcriptomics  By Kai  no Comments 首先这两款软件都是用 ...

  7. BZOJ1047或洛谷2216 [HAOI2007]理想的正方形

    BZOJ原题链接 洛谷原题链接 显然可以用数据结构或\(ST\)表或单调队列来维护最值. 这里采用单调队列来维护. 先用单调队列维护每一行的最大值和最小值,区间长为正方形长度. 再用单调队列维护之前维 ...

  8. unbutton 内部title label多行显示

    dateBtn1.titleLabel.lineBreakMode = NSLineBreakByWordWrapping; [dateBtn1 setTitle: @第一行\n第二行" f ...

  9. @1-4使用Xpath解析豆瓣短评

    使用Xpath解析豆瓣短评 Python爬虫(入门+进阶)     DC学院 本节课程主要介绍解析神器Xpath是什么.Xpath如何安装及使用,以及使用实际的例子讲解Xpath如何解析豆瓣短评的网页 ...

  10. mysql 查询 所有 父节点 单表

    SELECT T2.* FROM ( SELECT @r AS _id, ( SELECT @r := parent_id FROM tp_module_rel WHERE REL_ID = _id ...